File Coverage

lib/Table/BoxFormat.pm
Criterion Covered Total %
statement 178 193 92.2
branch 42 60 70.0
condition 1 3 33.3
subroutine 16 16 100.0
pod 6 6 100.0
total 243 278 87.4


line stmt bran cond sub pod time code
1             package Table::BoxFormat;
2 7     7   540358 use Moo;
  7         78383  
  7         34  
3 7     7   13367 use MooX::Types::MooseLike::Base qw(:all);
  7         46255  
  7         2496  
4              
5             =head1 NAME
6              
7             Table::BoxFormat - Parsing the tabular data format generated by database SELECTs
8              
9             =head1 VERSION
10              
11             Version 0.01
12              
13             =cut
14              
15             our $VERSION = '0.01';
16             my $DEBUG = 0; # TODO revise before shipping
17 7     7   101 use 5.10.0; # time to start saying 'say'
  7         26  
18 7     7   3612 use utf8::all;
  7         367357  
  7         40  
19 7     7   11248 use Carp;
  7         16  
  7         397  
20 7     7   47 use Data::Dumper;
  7         16  
  7         372  
21 7     7   3289 use Table::BoxFormat::Unicode::CharClasses ':all'; # IsHor IsCross IsDelim
  7         15  
  7         1008  
22              
23 7     7   4849 use Text::CSV;
  7         124776  
  7         2633  
24              
25             =encoding utf8
26              
27             =head1 SYNOPSIS
28              
29             use Table::BoxFormat;
30             # Reading input from a "dbox" temp file
31             my $dbx = Table::BoxFormat->new( input_file => '/tmp/select_result.dbox' );
32             my $data = $self->data; # array of arrays, header in first row
33              
34             # Input dbox from a string
35             my $dbx = Table::BoxFormat->new( input_data => $dboxes_string );
36             my $data = $self->data; # array of arrays, header in first row
37              
38             # input from dbox file, output directly to a tsv file
39             my $dbx = Table::BoxFormat->new();
40             $dbx->output_to_tsv( '/tmp/select_result.dbox', '/tmp/select_result.tsv' );
41              
42             # input dbox from a string, output directly to a tsv file
43             $dbx = Table::BoxFormat->new( input_data => $dbox_string );
44             $dbx->output_to_tsv( $output_tsv_file );
45              
46              
47             =head1 DESCRIPTION
48              
49             Table::BoxFormat is a module to work with data in the tabular text
50             format(s) commonly used in database client shells (postgresql's
51             "psql", mysql's "mysql", or sqlite's "sqlite3"),
52             where a SELECT will typical display data in a form such as this (mysql):
53              
54             +-----+------------+---------------+-------------+
55             | id | date | type | amount |
56             +-----+------------+---------------+-------------+
57             | 11 | 2010-09-01 | factory | 146035.00 |
58             | 15 | 2011-01-01 | factory | 191239.00 |
59             | 16 | 2010-09-01 | marketing | 467087.00 |
60             | 17 | 2010-10-01 | marketing | 409430.00 |
61             +-----+------------+---------------+-------------+
62              
63             Or this (postgresql's "ascii" form):
64              
65             id | date | type | amount
66             ----+------------+-----------+--------
67             1 | 2010-09-01 | factory | 146035
68             4 | 2011-01-01 | factory | 191239
69             6 | 2010-09-01 | marketing | 467087
70             7 | 2010-10-01 | marketing | 409430
71              
72             These formats are human-readable, but not suitable for other
73             purposes such as feeding to a graphics program, or inserting into
74             another database table.
75              
76             This code presumes these text tables of "data boxes" are either
77             stored in a string or saved to a file.
78              
79             This code works with at least three different
80             formats: mysql, psql and unicode psql.
81              
82             =head2 implementation notes
83              
84             The main method here is L, which works by first
85             looking for a horizontal ruler line near the top of the data,
86             for example:
87              
88             +-----+------------+---------------+-------------+
89             ----+------------+-----------+--------
90             ────┼────────────┼───────────┼────────
91              
92             These ruler lines are used to identify the boundary columns,
93             afterwhich the header and data lines are treated as fixed-width
94             fields. Leading and trailing whitespace are stripped from each
95             value.
96              
97             An earlier (now deprecated) method named L takes an
98             opposite approach, ignoring the horizontal rules entirely and
99             doing regular expression matches looking for data delimiters on
100             each line. In comparison, the L should run faster and
101             be able to handle strings with delimiter characters embedded in
102             them.
103              
104              
105             =head1 METHODS
106              
107             =over
108              
109             =cut
110              
111             =item new
112              
113             Creates a new Table::BoxFormat object.
114              
115             Takes a list of attribute/setting pairs as an argument.
116              
117             =over
118              
119             =item input_encoding
120              
121             Default's to "UTF-8". Change to suit text encoding (e.g. "ISO-8859-1").
122             Must work as a perl ":encoding(...)" layer.
123              
124             =item output_encoding
125              
126             Like L. Default: "UTF-8".
127              
128             =item input_file
129              
130             File to input data from. Can be supplied later, e.g. when
131             L is called. Only required if L was
132             not defined directly. (( TODO change this: make it required ? ))
133              
134             =item input_data
135              
136             SQL SELECT output in the fixed-width-plus-delimiter form discussed above.
137              
138             =item the parsing regular expressions (type: RegexpRef)
139              
140             =over
141              
142             =item separator_rule
143              
144             The column separators (vertical bar)
145              
146             =item ruler_line_rule
147              
148             Matches the Horizontal ruler lines (typically just under the
149             header line)
150              
151             =item cross_rule
152              
153             Match cross marks the horizontal bars typically use to mark
154             column boundaries.
155              
156             =item left_edge_rule
157              
158             Left border delimiters (we strip these before processing).
159              
160             =item right_edge_rule
161              
162             Right border delimiters (we strip these before processing).
163              
164             =back
165              
166             =back
167              
168             =cut
169              
170             # encodings default to utf-8 (might need to change, e.g. ISO-8859-1)
171             has input_encoding => ( is => 'rw', isa => Str, default => 'UTF-8' );
172             has output_encoding => ( is => 'rw', isa => Str, default => 'UTF-8' );
173              
174             # input file name (can skip if input_data is defined directly, or if file provided later)
175             has input_file => ( is => 'rw', isa => Str, default => "" );
176              
177             # can define input data directly, or alternately slurp it in from a file
178             # TODO better to avoid slurping, work line-at-a-time?
179             has input_data => ( is => 'rw', isa => Str,
180             default =>
181             sub { my $self = shift;
182             $self->slurp_input_data; },
183             lazy => 1 );
184              
185             has data => ( is => 'rw', isa => ArrayRef,
186             default =>
187             sub { my $self = shift;
188             $self->read_dbox(); },
189             lazy => 1 );
190              
191             has header => ( is => 'rw', isa => ArrayRef, default => sub{ [] } );
192              
193             has format => ( is => 'rw', isa => Str, default => sub{ '' } );
194              
195             # info about format/style of last data read
196             has meta => ( is => 'rw', isa => HashRef, default => sub{ {} } );
197              
198             has separator_rule => ( is => 'rw', isa => RegexpRef,
199             default =>
200             sub{ qr{
201             \s+ # require leading whitespace
202             \p{IsDelim}
203             {1,1} # just one
204             \s+ # require trailing whitespace
205             }xms } );
206              
207             # horizontal dashes plus crosses or whitespace
208             has ruler_line_rule => ( is => 'rw', isa => RegexpRef,
209             default =>
210             sub{
211             qr{ ^
212             [ \p{IsHor} \s ] +
213             $
214             }x
215             }
216             );
217              
218             has cross_rule => ( is => 'rw', isa => RegexpRef,
219             default =>
220             sub{ qr{
221             \p{IsCross}
222             {1,1} # just one
223             }xms } );
224              
225             # To match table borders (e.g. mysql-style)
226             has left_edge_rule => ( is => 'rw', isa => RegexpRef,
227             default => sub{ qr{ ^ \s* [\|] }xms } );
228              
229             has right_edge_rule => ( is => 'rw', isa => RegexpRef,
230             default => sub{ qr{ [\|] \s* $ }xms } );
231              
232              
233              
234             =item slurp_input_data
235              
236             Example usage:
237              
238             $self->slurp_input_data( $input_file_name );
239              
240             =cut
241              
242             sub slurp_input_data {
243 13     13 1 23 my $self = shift;
244              
245             # the input file can be defined at the object level, or supplied as an argument
246             # if it's an argument, the given value will be stored in the object level
247 13         23 my $input_file;
248 13 50       40 if ( $_[0] ) {
249 0         0 $input_file = shift;
250 0         0 $self->input_file( $input_file );
251             } else {
252 13         216 $input_file = $self->input_file;
253             }
254              
255 13 50       103 croak "Need an input file to read a dbox from" unless( $input_file );
256 13         210 my $input_encoding = $self->input_encoding;
257 13 50       103 unless ( $input_file ) {
258 0         0 croak
259             "Needs either an input data file name ('input_file'), " .
260             "or a multiline string ('input_data') ";
261             }
262 13         41 my $in_enc = "<:encoding($input_encoding)";
263 7 50   7   51 open my $fh, $in_enc, $input_file or croak "$!";
  7         15  
  7         49  
  13         481  
264 13         7183 local $/; # localized slurp mode
265 13         519 my $data = <$fh>;
266              
267             # # strip leading trailing ws, including blank lines.
268             # $data =~ s{ \A [\s]* }{}xms;
269             # $data =~ s{ [\s]* \z}{}xms;
270              
271 13         780 return $data;
272             }
273              
274              
275              
276              
277             =item read_dbox
278              
279             Given data in tabular boxes from a multiline string,
280             convert it into an array of arrays.
281              
282             my $data =
283             $bxs->read_dbox();
284              
285             Converts the boxdata from the object's input_data into an array
286             of arrays, with the field names included in the first row.
287              
288             As a side-effect, copies the header (first row of returned data)
289             in the object's L
, and puts some format metadata in the object's L.
290              
291             =cut
292              
293             # Uses the header ruler cross locations to identify the column boundaries,
294             # then treats the data as fixed-width fields, to handle the case
295             # of strings with embedded separator characters.
296             sub read_dbox {
297 10     10 1 277 my $self = shift;
298              
299             # the input file can be defined at the object level, or supplied as an argument
300             # if it's an argument, the given value will be stored in the object level
301 10         22 my $input_file;
302 10 50       36 if( $_[0] ) {
303 0         0 $input_file = shift;
304 0         0 $self->input_file( $input_file );
305             }
306              
307 10         176 my $input_data = $self->input_data;
308 10         613 my $ruler_line_rule = $self->ruler_line_rule;
309              
310 10         219 my $left_edge_rule = $self->left_edge_rule;
311 10         214 my $right_edge_rule = $self->right_edge_rule;
312              
313 10         164 my @lines = split /\n/, $input_data;
314              
315             # look for a header ruler line
316             # (first or third line for mysql, second line for postgres),
317 10         33 my (@pos, $format, $first_data, $header_loc, $ruler, @data);
318             RULERSCAN:
319 10         42 foreach my $i ( 1 .. 2 ) { # ruler lines are always near top
320 11         27 my $line = $lines[ $i ];
321              
322 11 100       266 if( $line =~ m{ $ruler_line_rule }x ) { ## TODO rename this pattern?
323 10         918 ( $format, $header_loc, $first_data, @pos ) = $self->analyze_ruler( $line, $i );
324 10         38 last RULERSCAN;
325             }
326             }
327              
328 10 50       52 unless( $format ) {
329 0         0 croak "no horizontal rule line found: is this really db output data box format?"
330             }
331              
332             # read data (with header) now that we know where things are
333 10         21 my $last_data = $#lines;
334 10 100       40 $last_data -= 1 if $format eq 'mysql'; # to skip that ruler line at bottom
335 10         32 foreach my $i ( $header_loc, $first_data .. $last_data ) {
336 72         164 my $line = $lines[ $i ];
337              
338 72 100       178 if( $format eq 'mysql' ) {
339             # convert to postqres-style lines by trimming the borders
340 8         68 $line =~ s{ $left_edge_rule }{}xms;
341 8         62 $line =~ s{ $right_edge_rule }{}xms;
342             }
343              
344 72         159 my @vals;
345 72         163 my $beg = 0;
346 72         124 foreach my $pos ( @pos ) {
347 276         2942 binmode STDERR, ":encoding(UTF-8)" || die "binmode on STDERR failed";
348 276 50       12597 ($DEBUG) && printf STDERR "beg: %6d, pos: %6d, line: %s\n", $beg, $pos, $line;
349 276         716 my $val =
350             substr( $line, $beg, ($pos-$beg) ); # TODO why not use unpack?
351             # strip leading and trailing spaces
352 276         1035 $val =~ s/^\s+//;
353 276         856 $val =~ s/\s+$//;
354 276         619 push @vals, $val;
355 276         512 $beg = $pos + 1;
356             }
357              
358             # array_of_array format (header in first line)
359 72         233 push @data, \@vals;
360             }
361 10         23 my @header;
362 10 50       63 @header = @{ $data[0] } if @data;
  10         34  
363 10         275 $self->header( \@header );
364              
365 10         651 $self->format( $format );
366              
367 10         502 return \@data;
368             }
369              
370              
371              
372             =item analyze_ruler
373              
374             Internal method that analyzes the given ruler line and location
375             to determine column widths and the dbox format.
376              
377             Returns an ordered list like so:
378              
379             format:
380             'mysql', 'postgres', 'postgres_unicode', 'sqlite'
381              
382             header location:
383             a row number: 0 or 1
384              
385             first_data:
386             the row number where data begins: 2 or 3
387              
388             positions:
389             a list of column boundary positions
390              
391              
392             Example usage:
393              
394             ( $format, $header_loc, $first_data, @pos ) = $self->analyze_ruler( $line, $i );
395              
396             =cut
397              
398             sub analyze_ruler {
399 10     10 1 25 my $self = shift;
400 10         19 my $ruler = shift;
401 10         17 my $ruler_loc = shift;
402              
403 10         211 my $cross_rule = $self->cross_rule;
404              
405 10         86 my ( $format, $header_loc, $first_data, @pos );
406              
407 10 100       51 if ( $ruler_loc == 2 ) {
    50          
408 1         15 $format = 'mysql';
409 1         2 $header_loc = 1;
410 1         9 $first_data = 3;
411             } elsif ( $ruler_loc == 1 ) {
412 9         29 $header_loc = 0;
413 9         18 $first_data = 2;
414 9 100       99 if ( $ruler =~ $cross_rule ) {
415 8         540 $format = 'postgres';
416             } else {
417 1         80 $format = 'sqlite';
418             }
419             }
420              
421 10 100       66 if ( $format eq 'mysql' ) {
422 1 50 33     23 unless (
423             $ruler =~ s{ ^ $cross_rule }{}xms &&
424             $ruler =~ s{ $cross_rule $ }{}xms
425             ) {
426 0         0 warn "mysql format, but ruler line was not terminated by crosses"
427             }
428             }
429              
430             # TODO identifying the cross could be combined with match to find horizontal rule
431 10         187 my %cross_candidates =
432             ( "\N{PLUS SIGN}" => 'ascii', # ye olde '+'
433             "\N{BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL}" => 'unicode', # newfangled '┼'
434             " " => 'spaces',
435             );
436              
437 10         21 my $cross;
438 10         57 foreach my $candy ( keys %cross_candidates ) {
439 30         82 my $pat = '\\' . $candy; # need to backwhack the + char
440              
441 30 100       85 if ( $candy eq ' ' ) {
442 10         67 $pat = qr{ \s{1,2} }x;
443             }
444              
445 30 100       471 if ( $ruler =~ m{ $pat }x ) {
446 10         26 $cross = $candy;
447             # $meta{ encoding } = $cross_candidates{ $candy };
448              
449 10 100       50 $format .= '_' . 'unicode' if $cross_candidates{ $candy } eq 'unicode';
450             }
451             }
452              
453             # 'index' be dumb:
454             # o it can't use a regexp: it's limited to character matches.
455             # o returns -1 on failure (what's wrong with undef?)
456 10         29 my $pos = 0;
457 10         56 while ( ( $pos = index( $ruler, $cross, $pos ) ) > -1 ) {
458 31         61 push @pos, $pos;
459 31         91 $pos++;
460             }
461 10         25 push @pos, length( $ruler ); # treat the eol as another column boundary
462              
463             # cleanup @pos: on immediately consecutive entries can drop the second one
464 10         25 my $last = 0;
465 10         22 my @newpos = ();
466 10         32 foreach my $i ( 0 .. $#pos ) {
467 41         60 my $this = $pos[ $i ];
468 41 100       103 push @newpos, $this
469             unless ( ($this-$last) == 1 );
470 41         71 $last = $this;
471             }
472 10         28 @pos = @newpos;
473              
474 10         62 return ( $format, $header_loc, $first_data, @pos );
475             }
476              
477              
478              
479             =item read_simple
480              
481             This is DEPRECATED. See L.
482              
483             Given data in tabular boxes from a multiline string,
484             convert it into an array of arrays.
485              
486             my $data =
487             $bxs->read_simple();
488              
489             Goes through the boxdata slurped into the object field input_data,
490             returns it as an array of arrays, including the field names in
491             the first row.
492              
493             As a side-effect, stores the header (first row of boxdata)
494             in the object's L
.
495              
496             =cut
497              
498             # Early appoach: does regexp parsing of separator characters on each line
499             sub read_simple {
500 3     3 1 117 my $self = shift;
501              
502             # the input file can be defined at the object level, or supplied as an argument
503             # if it's an argument, the given value will be stored in the object level
504 3         5 my $input_file;
505 3 50       10 if( $_[0] ) {
506 0         0 $input_file = shift;
507 0         0 $self->input_file( $input_file );
508             }
509              
510 3         50 my $input_data = $self->input_data;
511             # my $ruler_line_rule = $self->ruler_line_rule;
512 3         168 my $separator_rule = $self->separator_rule;
513              
514             # before we split on delimiters, trim the left and right borders (if any)
515             # (converts mysql-style lines into psql-style lines)
516 3         59 my $left_edge_rule = $self->left_edge_rule;
517 3         59 my $right_edge_rule = $self->right_edge_rule;
518              
519 3         54 $input_data =~ s{ ^ \s* }{}xmsg;
520 3         154 $input_data =~ s{ \s* $ }{}xmsg;
521              
522 3         55 $input_data =~ s{ $left_edge_rule }{}xmsg;
523 3         48 $input_data =~ s{ $right_edge_rule }{}xmsg;
524              
525             # Here we just look for lines with delimiters on them (skipping
526             # anything else) and then split the lines on the delimiters,
527             # trimming whitespace from the boundaries of all values
528              
529 3         28 my @lines = split /\n/, $input_data;
530              
531 3         7 my ( @data );
532 3         11 for my $i ( 0 .. $#lines ) {
533 29         955 my $line = $lines[ $i ];
534              
535             # when there's at least one delim, we assume it's a data line
536 29 100       190 if( $line =~ /$separator_rule/xms ) {
537              
538 7     7   102183 no warnings 'uninitialized';
  7         19  
  7         6614  
539              
540             # need this for the whitespace not adjacent to delimiters...
541 24         214 $line =~ s/^\s+//; # strip leading spaces
542 24         96 $line =~ s/\s+$//; # strip trailing spaces (if any)
543              
544             # Note: split pattern also eats bracketing whitespace
545 24         138 my @vals =
546             split /$separator_rule/, $line;
547              
548             # array_of_array format (header treated like any other vals)
549 24         52 push @data, \@vals;
550             }
551              
552 29         47 my @header;
553 29 100       52 @header = @{ $data[0] } if @data;
  28         79  
554 29         510 $self->header( \@header );
555             }
556 3         117 return \@data;
557             }
558              
559             =item output_to_tsv
560              
561             A convenience method that runs L and writes the data
562             to a tsv file specified by the given argument.
563              
564             Returns a reference to the data (array of arrays).
565              
566             Example usage:
567              
568             $dbx->output_to_tsv( $input_dbox_file, $output_tsv_file );
569              
570             Or:
571              
572             $dbx = Table::BoxFormat->new( input_file => $input_dbox_file );
573             $dbx->output_to_tsv( $output_tsv_file );
574              
575             Or:
576              
577             $dbx = Table::BoxFormat->new( input_data => $dbox_string );
578             $dbx->output_to_tsv( $output_tsv_file );
579              
580             =cut
581              
582             # TODO if no output_file is supplied as argument, could fall back
583             # to using the input_file with extension changed to "tsv".
584             sub output_to_tsv {
585 2     2 1 73 my $self = shift;
586              
587 2         5 my $input_file;
588 2 50       11 if( scalar( @_ ) == 2 ) {
589 0         0 $input_file = shift;
590 0         0 $self->input_file( $input_file );
591             }
592              
593 2         5 my $output_file = shift;
594 2 50       8 unless( $output_file ) {
595 0         0 croak("output_to_tsv requires the output_file.");
596             }
597 2         38 my $output_encoding = $self->output_encoding;
598              
599             # my $data = $self->read_dbox;
600 2         46 my $data = $self->data;
601              
602 2         77 my $out_enc = ">:encoding($output_encoding)";
603 2 50       217 open my $fh, $out_enc, $output_file or die "$!";
604              
605 2         143 for my $i ( 0 .. $#{ $data } ) {
  2         17  
606 16         35 my $line = join "\t", @{ $data->[ $i ] };
  16         33  
607 16         26 print { $fh } $line, "\n";
  16         48  
608             }
609             # return 1;
610 2         235 return $data;
611             }
612              
613              
614             =item output_to_csv
615              
616             A convenience method that runs L and writes the data
617             to a csv file specified by the given argument.
618              
619             Example usage:
620              
621             $dbx->output_to_csv( $input_dbox_file, $output_csv_file );
622              
623             Or:
624              
625             $dbx = Table::BoxFormat->new( input_file => $input_dbox_file );
626             $dbx->output_to_csv( $output_csv_file );
627              
628             Or:
629              
630             $dbx = Table::BoxFormat->new( input_data => $dbox_string );
631             $dbx->output_to_csv( $output_csv_file );
632              
633             =cut
634              
635             # TODO if no output_file is supplied as argument, could fall back
636             # to using the input_file with extension changed to "csv".
637              
638             sub output_to_csv {
639 1     1 1 37 my $self = shift;
640              
641             # my $in_enc = "<:encoding($input_encoding)";
642              
643 1         2 my $input_file;
644 1 50       6 if( scalar( @_ ) == 2 ) {
645 0         0 $input_file = shift;
646 0         0 $self->input_file( $input_file );
647             }
648              
649 1 50       10 my $csv = Text::CSV->new ( { binary => 1 } ) # should set binary attribute.
650             or die "Cannot use CSV: ".Text::CSV->error_diag ();
651              
652 1         196 my $output_file = shift;
653 1 50       4 unless( $output_file ) {
654 0         0 croak("output_to_csv requires the output_file.");
655             }
656 1         22 my $output_encoding = $self->output_encoding;
657              
658             # my $data = $self->read_dbox;
659 1         26 my $data = $self->data;
660              
661 1         42 my $out_enc = ">:encoding($output_encoding)";
662 1 50       128 open my $fh, $out_enc, $output_file or die "$!";
663              
664 1         69 for my $i ( 0 .. $#{ $data } ) {
  1         5  
665             ### my $line = join "\t", @{ $data->[ $i ] };
666 8         12 my @columns = @{ $data->[ $i ] };
  8         22  
667 8         26 my $status = $csv->combine(@columns); # combine columns into a string
668 8         157 my $line = $csv->string(); # get the combined string
669              
670 8         48 print { $fh } $line, "\n";
  8         25  
671             }
672 1         151 return 1;
673             }
674              
675              
676              
677              
678             =back
679              
680             =head1 AUTHOR
681              
682             Joseph Brenner, Edoom@kzsu.stanford.eduE,
683             05 Jun 2016
684              
685             =head1 LIMITATIONS
686              
687             =head2 memory limited
688              
689             As implemented, this presumes the entire data set can be held in memory.
690             Future versions may be more stream-oriented: there's no technical reason
691             this couldn't be done.
692              
693             =head2 what you get is what you get
694              
695             This code is only guaranteed to cover input formats from mysql, psql
696             and some from sqlite3. It may work with other databases, but
697             hasn't been tested.
698              
699             At present it is not easily extensible (implementing a plugin
700             system ala DBI/DBD seemed like overkill).
701              
702             =head2 sqlite3
703              
704             This code does not support the default output from sqlite3,
705             only a variation with these settings:
706              
707             .header on
708             .mode column
709              
710             While sqlite3 is very flexible, unfortunately the default output
711             does not seem very useable:
712              
713             SELECT * from expensoids;
714             |2010-09-01|factory|146035.0
715             |2010-11-01|factory|218866.0
716             |2011-01-01|factory|191239.0
717             |2010-10-01|marketing|409430.0
718              
719             This is separated by the traditional ascii vertical bar, but
720             without the usual bracketing spaces, and without any attempt at
721             using fixed width columns. Somewhat oddly, the left edge has a
722             vertical bar, but the right edge does not, but worse there's
723             no header that provides column labels.
724              
725             If I were actually working with sqlite a lot I would turn on
726             the header display and switch to fixed-width columns:
727              
728             .header on
729             .mode column
730              
731             That yields output that looks like this:
732              
733             id date type amount
734             ---------- ---------- ---------- ----------
735             1 2010-09-01 factory 146035.0
736             2 2010-10-01 factory 208816.0
737             3 2010-11-01 factory 218866.0
738              
739             That's very similar to the psql format using "\pset border 0"
740             (which has one space column breaks instead of two):
741             both are supported by L using the L
742             routine.
743              
744             =head1 COPYRIGHT AND LICENSE
745              
746             Copyright (C) 2016 by Joseph Brenner
747              
748             This program is free software; you can redistribute it and/or modify it
749             under the terms of either: the GNU General Public License as published
750             by the Free Software Foundation; or the Artistic License.
751              
752             See http://dev.perl.org/licenses/ for more information.
753              
754             =cut
755              
756             1;