File Coverage

blib/lib/Sql/Textify.pm
Criterion Covered Total %
statement 141 187 75.4
branch 11 42 26.1
condition 3 13 23.0
subroutine 20 24 83.3
pod 2 6 33.3
total 177 272 65.0


line stmt bran cond sub pod time code
1             package Sql::Textify;
2            
3 2     2   1224 use 5.006;
  2         6  
4 2     2   8 use strict;
  2         3  
  2         32  
5 2     2   7 use warnings;
  2         3  
  2         45  
6 2     2   7 use Carp qw(croak);
  2         3  
  2         92  
7 2     2   2165 use DBI;
  2         29451  
  2         126  
8 2     2   772 use HTML::Entities;
  2         9803  
  2         3920  
9            
10             =head1 NAME
11            
12             Sql::Textify - Run SQL queries and get the result in text format (markdown, html)
13            
14             =head1 VERSION
15            
16             Version 0.06
17            
18             =cut
19            
20             our $VERSION = '0.06';
21             $VERSION = eval $VERSION;
22             our @EXPORT_OK = qw(textify);
23            
24             =head1 SYNOPSIS
25            
26             use Sql::Textify;
27             my $t = Sql::Textify->new;
28             my $text = $t->textify( $sql );
29            
30             use Sql::Textify;
31             my $t = Sql::Textify->new(
32             conn => 'dbi:connection:string',
33             username => 'username',
34             password => 'password',
35             format => 'markdown',
36             );
37             my $text = $t->textify( $sql );
38            
39             =head1 SYNTAX
40            
41             This module executes SQL queries and produces text output (markdown, html).
42             Connection details, username and password can be specified in a C-style multiline
43             comment inside the SQL query:
44            
45             /*
46             conn="dbi:SQLite:dbname=test.sqlite3"
47             username="myusername"
48             password="mypassword"
49             */
50             select * fom gardens;
51            
52             or they can be specified using the constructor:
53            
54             my $t = new Sql::Textify(
55             conn => 'dbi:SQLite:dbname=test.sqlite3',
56             username => 'myusername',
57             password => 'mypassword'
58             );
59             my $text = $t->textify('select * from gardens');
60            
61             multiple queries can be separated by C<< ; >> and also insert/update/create/etc. queries are
62             supported. If the query doesn't return any row the string C<< 0 rows >> will be returned.
63            
64             =head1 OPTIONS
65            
66             Sql::Textify supports a number of options to its processor which control
67             the behaviour of the output document.
68            
69             The options for the processor are:
70            
71             =over
72            
73             =item format
74            
75             markdown (default), html
76            
77             =item layout
78            
79             table (default), record
80            
81             =item conn
82            
83             Specify the DBI connection string.
84            
85             =item username
86            
87             Specify the database username.
88            
89             =item password
90            
91             Specify the database password.
92            
93             =item maxwidth
94            
95             Set a maximum width for the columns when in markdown format mode. If any column contains
96             a string longer than maxwidth it will be cropped.
97            
98             =back
99            
100             =head1 METHODS
101            
102             =head2 new
103            
104             Sql::Textify constructor, see OPTIONS sections for more information.
105            
106             =cut
107            
108             sub new {
109 1     1 1 605 my ($class, %p) = @_;
110 1         3 my $self = {};
111            
112 1         4 $self->{format} = sanitize_string({ value => $p{format}, regexp => 'html|markdown', default => 'markdown' });
113 1         7 $self->{layout} = sanitize_string({ value => $p{layout}, regexp => 'table|record', default => 'table' });
114            
115 1         3 $self->{username} = $p{username};
116 1         2 $self->{password} = $p{password};
117 1         2 $self->{conn} = $p{conn};
118            
119 1   33     6 bless $self, ref($class) || $class;
120 1         3 return $self;
121             }
122            
123             =head2 textify
124            
125             The main function as far as the outside world is concerned. See the SYNTAX
126             for details on use.
127            
128             =cut
129            
130             sub textify {
131 0     0 1 0 my ( $self, $sql ) = @_;
132            
133 0         0 $self->_GetParametersFromSql($sql);
134 0   0     0 $self->{dbh} = DBI->connect($self->{conn}, $self->{username}, $self->{password}) || die $DBI::errstr;
135            
136 0         0 return $self->_Textify($sql);
137             }
138            
139             sub _GetParametersFromSql {
140 0     0   0 my ($self, $sql) = @_;
141            
142             # FIXME: values from SQL string will take precedence
143             # FIXME: the following regexps will usually work on most cases, but are over-simplified
144            
145 0 0       0 if ($sql =~ /conn=\"([^\""]*)\"\s/) { $self->{conn} = $1; }
  0         0  
146 0 0       0 if ($sql =~ /username=\"([^\""]*)\"\s/) { $self->{username} = $1; }
  0         0  
147 0 0       0 if ($sql =~ /password=\"([^\""]*)\"\s/) { $self->{password} = $1; }
  0         0  
148 0 0       0 if ($sql =~ /maxwidth=\"([^\""]*)\"\s/) { $self->{maxwidth} = $1; }
  0         0  
149 0 0       0 if ($sql =~ /format=\"([^\""]*)\"\s/) { $self->{format} = $1; }
  0         0  
150 0 0       0 if ($sql =~ /layout=\"([^\""]*)\"\s/) { $self->{layout} = $1; }
  0         0  
151             }
152            
153             sub _Do_Sql {
154 0     0   0 my ($self, $sql_query) = @_;
155 0         0 my %r;
156            
157 0   0     0 my $qry = $self->{dbh}->prepare($sql_query) || die "````\n", $sql_query, "\n````\n\n", ">", $DBI::errstr, "\n";
158 0 0       0 $qry->execute() || die "````\n", $sql_query, "\n````\n\n", ">", $DBI::errstr;
159 0         0 my $rows = $qry->fetchall_arrayref();
160 0         0 $qry->finish();
161            
162 0         0 $r{fields} = $qry->{NAME};
163 0         0 $r{rows} = $rows;
164            
165 0 0       0 return wantarray ? %r : \%r;
166             }
167            
168             sub _Textify {
169 0     0   0 my ($self, $sql) = @_;
170            
171 0         0 my $result;
172            
173             # strip C-style comments from source query
174             # regexp from http://learn.perl.org/faq/perlfaq6.html#How-do-I-use-a-regular-expression-to-strip-C-style-comments-from-a-file
175            
176 0 0       0 $sql =~ s#/\*[^*]*\*+([^/*][^*]*\*+)*/|("(\\.|[^"\\])*"|'(\\.|[^'\\])*'|.[^/"'\\]*)#defined $2 ? $2 : ""#gse;
  0         0  
177            
178 0         0 foreach my $sql_query (split /;\s*/, $sql) {
179 0         0 my $records = $self->_Do_Sql($sql_query);
180 0         0 $result .= $self->_Do_Format($records);
181             }
182            
183 0         0 return $result;
184             }
185            
186             sub _Do_Format {
187 10     10   4371 my ($self, $records) = @_;
188            
189             my %m = (
190             "markdown" => {
191 3     3   7 "table" => sub { _Do_Sql_Markdown(@_) },
192 2     2   5 "record" => sub { _Do_Sql_Markdown_Record(@_) }
193             },
194             "html" => {
195 3     3   6 "table" => sub { _Do_Sql_Html(@_) },
196 2     2   4 "record" => sub { _Do_Sql_Html_Record(@_) }
197             },
198 10         83 );
199            
200 10 50       30 if ($m{ $self->{format} }->{ $self->{layout} }) {
201 10         42 return $m{ $self->{format} }->{ $self->{layout} }->($self, $records);
202             } else {
203 0         0 die("Wrong format/layout (".$self->{format}."/".$self->{layout});
204             }
205             }
206            
207             sub _Do_Sql_Markdown {
208 3     3   4 my ($self, $records) = @_;
209            
210 3         5 my $max_width = $self->{maxwidth};
211 3         4 my $max_format = '';
212            
213 3         3 my $result = '';
214            
215 3 50 33     8 if (($max_width) && ($max_width> 0))
216             {
217 0         0 $max_format = ".$max_width";
218             }
219            
220 3         3 my @width = map { min(length(quote_markdown($_)), $max_width) } @{$records->{fields}};
  6         9  
  3         6  
221            
222 3 50       3 if (scalar @{ $records->{rows} }>0) {
  3         8  
223            
224 3         3 foreach my $row ( @{ $records->{rows} }) {
  3         5  
225             # <- for each row, and for each column -> calculate the maximum width
226 4         6 foreach my $i (0 .. (scalar @{ $row }-1)) {
  4         9  
227 8         10 $width[$i] = max($width[$i], length(quote_markdown($row->[$i])));
228             }
229             }
230             # then "strip" the width to the $max_width
231 3         5 @width = map { min($_, $max_width) } @width;
  6         9  
232            
233             # create format string
234 3         8 my $f = join(' | ', map { "%-".$_.$max_format."s"} @width) . "\n";
  6         17  
235            
236             # print header
237 3         5 $result .= "\n" . sprintf( $f, map { quote_markdown($_) } @{$records->{fields}} );
  6         7  
  3         4  
238            
239             # print sub header -|-
240 3         6 $result .= join("-|-", map { '-'x$_ } @width ) . "\n";
  6         15  
241            
242             # print rows
243 3         6 foreach my $row (@{ $records->{rows} }) {
  3         5  
244 4         5 $result .= sprintf( $f, map { quote_markdown($_) } @{$row} );
  8         11  
  4         5  
245             }
246             } else {
247 0         0 $result .= "0 rows\n";
248             }
249            
250 3         37 return $result;
251             }
252            
253             sub _Do_Sql_Markdown_Record {
254 2     2   4 my ($self, $records) = @_;
255            
256 2         5 my $max_width = $self->{maxwidth};
257            
258 2         3 my $max_format = '';
259 2         4 my $nr = 1;
260            
261 2         4 my $result = '';
262            
263 2 50 33     7 if (($max_width) && ($max_width > 0))
264             {
265 0         0 $max_format = ".$max_width";
266             }
267            
268             # rows
269 2         4 foreach my $row (@{ $records->{rows} }) {
  2         5  
270 3         9 $result .= "# Record $nr\n\n";
271            
272 3         5 my @width = (min(length('Column'),$max_width),min(length('Value'),$max_width));
273            
274 3         5 foreach my $i (0 .. (scalar @{ $row }-1)) {
  3         6  
275 6         9 $width[0]=max($width[0], length(quote_markdown($records->{fields}[$i])));
276 6         15 $width[1]=max($width[1], length(quote_markdown($row->[$i])),$max_width);
277             }
278 3         4 $width[0] = min($width[0], $max_width);
279 3         4 $width[1] = min($width[1], $max_width);
280            
281             # %-x.ys %s string, - left-justify, x minimum widht, y maximum width
282 3         5 my $f = join(' | ', map { "%-".$_.$max_format."s"} @width) . "\n";
  6         16  
283            
284 3         13 $result .= sprintf $f, ("Column", "Value");
285            
286 3         10 $result .= '-'x$width[0] . '-|-' . '-'x$width[1] . "\n";
287            
288 3         4 foreach my $i (0 .. (scalar @{ $row }-1)) {
  3         7  
289 6         12 $result .= sprintf $f, (quote_markdown($records->{fields}[$i]), quote_markdown($row->[$i]));
290             }
291            
292 3         5 $result .= "\n";
293 3         6 $nr++;
294             }
295            
296 2 50       6 if ($nr == 1) { # no rows returned, empty rowset or create/update query
297 0         0 $result .= "0 rows\n";
298             }
299            
300 2         20 return $result;
301             }
302            
303             sub _Do_Sql_Html {
304 3     3   4 my ($self, $records) = @_;
305 3         4 my $result;
306            
307 3 50       4 if (scalar @{ $records->{rows} }>0) {
  3         7  
308            
309 3         4 $result = "\n"; \n"; '} @{$records->{fields}}) . "\n"; \n"; \n"; \n"; '} @{ $row }) . "\n"; \n"; \n";
310 3         6 $result .= "
311            
312 3         4 $result .= join("\n", map { ' ' . encode_entities($_) . '
  6         64  
  3         5  
313            
314 3         47 $result .= "
315 3         3 $result .= "
316            
317 3         4 foreach my $row (@{ $records->{rows} }) {
  3         6  
318 4         5 $result .= "
319 4         4 $result .= join("\n", map { ' ' . encode_entities($_) . '
  8         56  
  4         5  
320 4         58 $result .= "
321             }
322            
323 3         3 $result .= "
324 3         4 $result .= "
\n\n";
325             } else {
326 0         0 $result .= "

\n0 rows

\n\n";
327             }
328            
329 3         24 return $result;
330             }
331            
332             sub _Do_Sql_Html_Record {
333 2     2   4 my ($self, $records) = @_;
334 2         2 my $result;
335 2         2 my $nr = 1;
336            
337 2 50       3 if (scalar @{ $records->{rows} }>0) {
  2         6  
338 2         3 foreach my $row (@{ $records->{rows} }) {
  2         4  
339 3         6 $result .= "

Record $nr

\n\n";
340            
341 3         4 $result .= "\n"; \n"; \n"; \n"; \n";
342            
343 3         3 foreach my $i (0 .. (scalar @{ $row }-1)) {
  3         6  
344 6         7 $result .= "
345 6         14 $result .= " " . encode_entities($records->{fields}[$i]) . "
346 6         66 $result .= " " . encode_entities($row->[$i]) . "
347 6         61 $result .= "
348             }
349            
350 3         5 $result .= "
\n\n";
351            
352 3         4 $nr++;
353             }
354             } else {
355 0         0 $result .= "

\n0 rows

\n\n";
356             }
357            
358 2         15 return $result;
359             }
360            
361             # internal functions
362            
363             sub sanitize_string {
364 2     2 0 3 my $p = shift;
365            
366 2 50       7 return $p->{default} unless $p->{value};
367 0 0       0 return $p->{value} if $p->{value} =~ /^$p->{regexp}$/;
368 0         0 croak("Invalid value *$p->{value}* provided.");
369             }
370            
371             sub quote_markdown {
372             # there's not a standard way to quote markdown
373 52     52 0 61 my $s = shift;
374            
375 52 50       72 if (defined $s) {
376             # quote |
377 52         74 $s =~ s/\|/\\\|/g;
378            
379             # replace non-printable characters with space
380 52         69 $s =~ s/[^[:print:]]/ /g;
381             } else {
382 0         0 $s = '';
383             }
384 52         117 return $s;
385             }
386            
387             sub max ($$) {
388             # if second parameter is defined then return max(p1, p2) otherwise return p1
389 20 50   20 0 29 if ($_[1]) {
390 20         32 $_[$_[0] < $_[1]];
391             } else {
392 0         0 $_[0];
393             }
394             }
395            
396             sub min ($$) {
397             # if second parameter is defined then return min(p1, p2) otherwise return p1
398 24 50   24 0 32 if ($_[1]) {
399 0         0 $_[$_[0] > $_[1]];
400             } else {
401 24         35 $_[0];
402             }
403             }
404            
405             =head1 AUTHOR
406            
407             Federico, Thiella, C<< >>
408            
409             =head1 BUGS
410            
411             Please report any bugs or feature requests to C, or through
412             the web interface at L. I will be notified, and then you'll
413             automatically be notified of progress on your bug as I make changes.
414            
415            
416            
417            
418             =head1 SUPPORT
419            
420             You can find documentation for this module with the perldoc command.
421            
422             perldoc Sql::Textify
423            
424            
425             You can also look for information at:
426            
427             =over 4
428            
429             =item * RT: CPAN's request tracker (report bugs here)
430            
431             L
432            
433             =item * AnnoCPAN: Annotated CPAN documentation
434            
435             L
436            
437             =item * CPAN Ratings
438            
439             L
440            
441             =item * Search CPAN
442            
443             L
444            
445             =back
446            
447            
448             =head1 ACKNOWLEDGEMENTS
449            
450            
451             =head1 LICENSE AND COPYRIGHT
452            
453             Copyright 2017 Federico, Thiella.
454            
455             This program is free software; you can redistribute it and/or modify it
456             under the terms of the the Artistic License (2.0). You may obtain a
457             copy of the full license at:
458            
459             L
460            
461             Any use, modification, and distribution of the Standard or Modified
462             Versions is governed by this Artistic License. By using, modifying or
463             distributing the Package, you accept this license. Do not use, modify,
464             or distribute the Package, if you do not accept this license.
465            
466             If your Modified Version has been derived from a Modified Version made
467             by someone other than you, you are nevertheless required to ensure that
468             your Modified Version complies with the requirements of this license.
469            
470             This license does not grant you the right to use any trademark, service
471             mark, tradename, or logo of the Copyright Holder.
472            
473             This license includes the non-exclusive, worldwide, free-of-charge
474             patent license to make, have made, use, offer to sell, sell, import and
475             otherwise transfer the Package with respect to any patent claims
476             licensable by the Copyright Holder that are necessarily infringed by the
477             Package. If you institute patent litigation (including a cross-claim or
478             counterclaim) against any party alleging that the Package constitutes
479             direct or contributory patent infringement, then this Artistic License
480             to you shall terminate on the date that such litigation is filed.
481            
482             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
483             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
484             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
485             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
486             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
487             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
488             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
489             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
490            
491            
492             =cut
493            
494             1; # End of Sql::Textify