File Coverage

blib/lib/Perinci/Sub/Gen/AccessTable/DBI.pm
Criterion Covered Total %
statement 105 120 87.5
branch 53 76 69.7
condition 14 29 48.2
subroutine 13 13 100.0
pod 1 1 100.0
total 186 239 77.8


line stmt bran cond sub pod time code
1             package Perinci::Sub::Gen::AccessTable::DBI;
2              
3             our $DATE = '2017-07-10'; # DATE
4             our $VERSION = '0.18'; # VERSION
5              
6 1     1   48028 use 5.010001;
  1         6  
7 1     1   8 use strict;
  1         3  
  1         32  
8 1     1   10 use warnings;
  1         3  
  1         47  
9 1     1   8 use experimental 'smartmatch';
  1         4  
  1         14  
10 1     1   4505 use Log::ger;
  1         115  
  1         7  
11              
12 1     1   1994 use Function::Fallback::CoreOrPP qw(clone);
  1         692  
  1         92  
13 1     1   473 use Locale::TextDomain::UTF8 'Perinci-Sub-Gen-AccessTable-DBI';
  1         16096  
  1         12  
14 1     1   13113 use DBI;
  1         4  
  1         70  
15 1     1   976 use Perinci::Sub::Gen::AccessTable qw(gen_read_table_func);
  1         25655  
  1         101  
16 1     1   11 use Perinci::Sub::Util qw(gen_modified_sub);
  1         3  
  1         1075  
17              
18             require Exporter;
19             our @ISA = qw(Exporter);
20             our @EXPORT_OK = qw(gen_read_dbi_table_func);
21              
22             my $label = "(gen_read_dbi_table_func)";
23              
24             sub __parse_schema {
25 36     36   8678 require Data::Sah::Normalize;
26 36         6588 Data::Sah::Normalize::normalize_schema($_[0]);
27             }
28              
29             gen_modified_sub(
30             output_name => 'gen_read_dbi_table_func',
31             install_sub => 0,
32             base_name => 'Perinci::Sub::Gen::AccessTable::gen_read_table_func',
33             summary => 'Generate function (and its metadata) to read DBI table',
34             description => <<'_',
35              
36             The generated function acts like a simple single table SQL SELECT query,
37             featuring filtering, ordering, and paging, but using arguments as the 'query
38             language'. The generated function is suitable for exposing a table data from an
39             API function. Please see Perinci::Sub::Gen::AccessTable's documentation for more
40             details on what arguments the generated function will accept.
41              
42             _
43             remove_args => ['table_data'],
44             add_args => {
45             table_name => {
46             req => 1,
47             schema => 'str*',
48             summary => 'DBI table name',
49             },
50             dbh => {
51             schema => 'obj*',
52             summary => 'DBI database handle',
53             },
54             },
55             modify_args => {
56             table_spec => sub {
57             my $as = shift;
58             $as->{description} = <<'_';
59              
60             Just like Perinci::Sub::Gen::AccessTable's table_spec, except that each field
61             specification can have a key called `db_field` to specify the database field (if
62             different). Currently this is required. Future version will be able to generate
63             table_spec from table schema if table_spec is not specified.
64              
65             _
66             },
67             },
68             modify_meta => sub {
69             my $meta = shift;
70             push @{ $meta->{'x.dynamic_generator_modules'} }, __PACKAGE__;
71             },
72             );
73             sub gen_read_dbi_table_func {
74 6     6 1 571764 my %args = @_;
75              
76             # XXX schema
77 6         22 my $table_name = $args{table_name}; delete $args{table_name};
  6         24  
78 6 50       1439 $table_name or return [400, "Please specify table_name"];
79 6         24 my $dbh = $args{dbh}; delete $args{dbh};
  6         18  
80 6 50       25 $dbh or return [400, "Please specify dbh"];
81              
82             # duplicate and make each field's schema normalized
83 6         37 my $table_spec = clone($args{table_spec});
84 6         261 for my $fspec (values %{$table_spec->{fields}}) {
  6         35  
85 36   50     778 $fspec->{schema} //= 'any';
86 36         107 $fspec->{schema} = __parse_schema($fspec->{schema});
87             }
88              
89             my $table_data = sub {
90 80     80   382558 my $query = shift;
91              
92 80         843 my ($db) = $dbh->get_info(17);
93 80 50       1409 unless ($db =~ /\A(SQLite|mysql|Pg)\z/) {
94 0         0 log_warn("$label Database is not supported: %s", $db);
95             }
96              
97             # function to quote identifier, e.g. `col` or "col"
98             my $qi = sub {
99 318 50       1735 if ($db =~ /SQLite|mysql/) { return "`$_[0]`" }
  318         1770  
100 0         0 return qq("$_[0]");
101 80         534 };
102              
103 80         261 my $fspecs = $table_spec->{fields};
104 80         435 my @fields = keys %$fspecs;
105             my @searchable_fields = grep {
106 80         252 !defined($fspecs->{$_}{searchable}) || $fspecs->{$_}{searchable}
107 480 50       1764 } @fields;
108              
109 80         215 my $filtered;
110             my @wheres;
111             # XXX case_insensitive_search & word_search not yet observed
112 80         213 my $q = $query->{query};
113 80 100 66     311 if (defined($q) && @searchable_fields) {
114             push @wheres, "(".
115 2   33     8 join(" OR ", map {$qi->($fspecs->{$_}{db_field}//$_)." LIKE ".
  12         226  
116             $dbh->quote("%$q%")}
117             @searchable_fields).
118             ")";
119             }
120 80 50       324 if ($args{custom_search}) {
121 0         0 $filtered = 0; # perigen-acctbl will be doing custom_search
122             }
123 80 50       267 if ($args{custom_filter}) {
124 0         0 $filtered = 0; # perigen-acctbl will be doing custom_search
125             }
126 80         172 for my $filter (@{$query->{filters}}) {
  80         282  
127 24         83 my ($f, $ftype, $op, $opn) = @$filter;
128 24   33     184 my $qdbf = $qi->($fspecs->{$f}{db_field} // $f);
129 24         212 my $qopn = $dbh->quote($opn);
130 24 100       670 if ($op eq 'truth') { push @wheres, $qdbf
  5 50       19  
    50          
    50          
    100          
    50          
    50          
    50          
    100          
    50          
    100          
    50          
    100          
    50          
    100          
    100          
    100          
    100          
    50          
    0          
131 0         0 } elsif ($op eq '~~') { $filtered = 0 # not supported
132 0         0 } elsif ($op eq '!~~') { $filtered = 0 # not supported
133 0         0 } elsif ($op eq 'eq') { push @wheres, "$qdbf = $qopn"
134 6         26 } elsif ($op eq '==') { push @wheres, "$qdbf = $qopn"
135 0         0 } elsif ($op eq 'ne') { push @wheres, "$qdbf <> $qopn"
136 0         0 } elsif ($op eq '!=') { push @wheres, "$qdbf <> $qopn"
137 0         0 } elsif ($op eq 'ge') { push @wheres, "$qdbf >= $qopn"
138 4         26 } elsif ($op eq '>=') { push @wheres, "$qdbf >= $qopn"
139 0         0 } elsif ($op eq 'gt') { push @wheres, "$qdbf > $qopn"
140 1         7 } elsif ($op eq '>' ) { push @wheres, "$qdbf > $qopn"
141 0         0 } elsif ($op eq 'le') { push @wheres, "$qdbf <= $qopn"
142 3         21 } elsif ($op eq '<=') { push @wheres, "$qdbf <= $qopn"
143 0         0 } elsif ($op eq 'lt') { push @wheres, "$qdbf < $qopn"
144 1         9 } elsif ($op eq '<' ) { push @wheres, "$qdbf < $qopn"
145 1         5 } elsif ($op eq '=~') { $filtered = 0 # not supported
146 1         6 } elsif ($op eq '!~') { $filtered = 0 # not supported
147 1         5 } elsif ($op eq 'pos') { $filtered = 0 # different substr funcs
148 1         4 } elsif ($op eq '!pos') { $filtered = 0 # different substr funcs
149 0         0 } elsif ($op eq 'call') { $filtered = 0 # not supported
150             } else {
151 0         0 die "BUG: Unknown op $op";
152             }
153             }
154 80   100     488 $filtered //= 1;
155              
156 80         192 my $sorted;
157             my @orders;
158 80 100       248 if ($query->{random}) {
    100          
159 50         117 push @orders, "RANDOM()";
160 30         126 } elsif (@{$query->{sorts}}) {
161 5         15 for my $s (@{$query->{sorts}}) {
  5         24  
162 6         24 my ($f, $op, $desc) = @$s;
163 6 100 33     75 push @orders, $qi->($fspecs->{$f}{db_field} // $f).
164             ($desc == -1 ? " DESC" : "");
165             }
166             }
167 80   50     425 $sorted //= 1;
168              
169 80         156 my $paged;
170 80         185 my $limit = "";
171 80         259 my ($ql, $qs) = ($query->{result_limit}, $query->{result_start}-1);
172 80 100 66     438 if (defined($ql) || $qs > 0) {
173 2 0 33     23 $limit = join(
    50          
    100          
174             "",
175             "LIMIT ".($ql // ($db eq 'Pg' ? "ALL":"999999999")),
176             ($qs > 1 ? ($db eq 'mysql' ? ",$qs" : " OFFSET $qs") : "")
177             );
178             }
179 80   50     423 $paged //= 1;
180              
181             my $sql = join(
182             "",
183             "SELECT ",
184 98   33     611 join(",", map {$qi->($fspecs->{$_}{db_field}//$_)." AS ".$qi->($_)}
185 80 100       174 @{$query->{requested_fields}}).
  80 100       240  
186             " FROM ".$qi->($table_name),
187             (@wheres ? " WHERE ".join(" AND ", @wheres) : ""),
188             (@orders ? " ORDER BY ".join(",", @orders) : ""),
189             $limit,
190             );
191 80         473 log_trace("$label SQL=%s", $sql);
192              
193 80         646 my $sth = $dbh->prepare($sql);
194 80 50       13086 $sth->execute or die "Can't query: ".$sth->errstr;
195 80         304 my @r;
196 80         1988 while (my $row = $sth->fetchrow_hashref) { push @r, $row }
  275         4002  
197              
198 80         2379 {data=>\@r, paged=>$paged, filtered=>$filtered, sorted=>$sorted,
199             fields_selected=>0, # XXX i'm lazy to handle detail=0
200             };
201 6         348 };
202              
203 6         49 @_ = (%args, table_data => $table_data);
204 6         54 goto &gen_read_table_func;
205             }
206              
207             1;
208             # ABSTRACT: Generate function (and its metadata) to read DBI table
209              
210             __END__
211              
212             =pod
213              
214             =encoding UTF-8
215              
216             =head1 NAME
217              
218             Perinci::Sub::Gen::AccessTable::DBI - Generate function (and its metadata) to read DBI table
219              
220             =head1 VERSION
221              
222             This document describes version 0.18 of Perinci::Sub::Gen::AccessTable::DBI (from Perl distribution Perinci-Sub-Gen-AccessTable-DBI), released on 2017-07-10.
223              
224             =head1 SYNOPSIS
225              
226             Your database table C<countries>:
227              
228             | id | eng_name | ind_name |
229             |----+--------------------------+-----------------|
230             | cn | China | Cina |
231             | id | Indonesia | Indonesia |
232             | sg | Singapore | Singapura |
233             | us | United States of America | Amerika Serikat |
234              
235             In list_countries.pl:
236              
237             #!perl
238             use strict;
239             use warnings;
240             use Perinci::CmdLine;
241             use Perinci::Sub::Gen::AccessTable::DBI qw(gen_read_dbi_table_func);
242              
243             our %SPEC;
244              
245             my $res = gen_read_dbi_table_func(
246             name => 'list_countries',
247             summary => 'func summary', # opt
248             description => 'func description', # opt
249             dbh => ...,
250             table_name => 'countries',
251             table_spec => {
252             summary => 'List of countries',
253             fields => {
254             id => {
255             schema => 'str*',
256             summary => 'ISO 2-letter code for the country',
257             index => 0,
258             sortable => 1,
259             },
260             eng_name => {
261             schema => 'str*',
262             summary => 'English name',
263             index => 1,
264             sortable => 1,
265             },
266             ind_name => {
267             schema => 'str*',
268             summary => 'Indonesian name',
269             index => 2,
270             sortable => 1,
271             },
272             },
273             pk => 'id',
274             },
275             );
276             die "Can't generate function: $res->[0] - $res->[1]" unless $res->[0] == 200;
277              
278             Perinci::CmdLine->new(url=>'/main/list_countries')->run;
279              
280             Now you can do:
281              
282             # list all countries, by default only PK field is shown
283             $ list_countries.pl --format=text-simple
284             cn
285             id
286             sg
287             us
288              
289             # show as json, randomize order
290             $ list_countries.pl --format=json --random
291             ["id","us","sg","cn"]
292              
293             # only list countries which contain 'Sin', show all fields (--detail)
294             $ list_countries.pl --q=Sin --detail
295             .----------------------------.
296             | eng_name | id | ind_name |
297             +-----------+----+-----------+
298             | Singapore | sg | Singapura |
299             '-----------+----+-----------+
300              
301             # show only certain fields, limit number of records, return in YAML format
302             $ list_countries.pl --fields '[id, eng_name]' --result-limit 2 --format=yaml
303             - 200
304             - OK
305             -
306             - id: cn
307             eng_name: China
308             - id: id
309             eng_name: Indonesia
310              
311             =head1 DESCRIPTION
312              
313             This module is just like L<Perinci::Sub::Gen::AccessTable>, except that table
314             data source is from DBI. gen_read_dbi_table_func() accept mostly the same
315             arguments as gen_read_table_func(), except: 'table_name' instead of
316             'table_data', and 'dbh'.
317              
318             Supported databases: SQLite, MySQL, PostgreSQL.
319              
320             Early versions tested on: SQLite.
321              
322             =head1 CAVEATS
323              
324             It is often not a good idea to expose your database schema directly as API.
325              
326             =head1 FUNCTIONS
327              
328              
329             =head2 gen_read_dbi_table_func
330              
331             Usage:
332              
333             gen_read_dbi_table_func(%args) -> [status, msg, result, meta]
334              
335             Generate function (and its metadata) to read DBI table.
336              
337             The generated function acts like a simple single table SQL SELECT query,
338             featuring filtering, ordering, and paging, but using arguments as the 'query
339             language'. The generated function is suitable for exposing a table data from an
340             API function. Please see Perinci::Sub::Gen::AccessTable's documentation for more
341             details on what arguments the generated function will accept.
342              
343             This function is not exported by default, but exportable.
344              
345             Arguments ('*' denotes required arguments):
346              
347             =over 4
348              
349             =item * B<case_insensitive_search> => I<bool> (default: 1)
350              
351             Decide whether generated function will perform case-insensitive search.
352              
353             =item * B<custom_filters> => I<hash>
354              
355             Supply custom filters.
356              
357             A hash of filter name and definitions. Filter name will be used as generated
358             function's argument and must not clash with other arguments. Filter definition
359             is a hash containing these keys: I<meta> (hash, argument metadata), I<code>,
360             I<fields> (array, list of table fields related to this field).
361              
362             Code will be called for each record to be filtered and will be supplied ($r, $v,
363             $opts) where $v is the filter value (from the function argument) and $r the
364             hashref record value. $opts is currently empty. Code should return true if
365             record satisfies the filter.
366              
367             =item * B<custom_search> => I<code>
368              
369             Supply custom searching for generated function.
370              
371             Code will be supplied ($r, $q, $opts) where $r is the record (hashref), $q is
372             the search term (from the function argument 'q'), and $opts is {ci=>0|1}. Code
373             should return true if record matches search term.
374              
375             =item * B<dbh> => I<obj>
376              
377             DBI database handle.
378              
379             =item * B<default_arg_values> => I<hash>
380              
381             Specify defaults for generated function's arguments.
382              
383             Can be used to supply default filters, e.g.
384              
385             # limit years for credit card expiration date
386             { "year.min" => $curyear, "year.max" => $curyear+10, }
387              
388             =item * B<default_detail> => I<bool>
389              
390             Supply default 'detail' value for function arg spec.
391              
392             =item * B<default_fields> => I<str>
393              
394             Supply default 'fields' value for function arg spec.
395              
396             =item * B<default_random> => I<bool>
397              
398             Supply default 'random' value in generated function's metadata.
399              
400             =item * B<default_result_limit> => I<int>
401              
402             Supply default 'result_limit' value in generated function's metadata.
403              
404             =item * B<default_sort> => I<array[str]>
405              
406             Supply default 'sort' value in generated function's metadata.
407              
408             =item * B<default_with_field_names> => I<bool>
409              
410             Supply default 'with_field_names' value in generated function's metadata.
411              
412             =item * B<description> => I<str>
413              
414             Generated function's description.
415              
416             =item * B<detail_aliases> => I<hash>
417              
418             =item * B<enable_field_selection> => I<bool> (default: 1)
419              
420             Decide whether generated function will support field selection (the `fields` argument).
421              
422             =item * B<enable_filtering> => I<bool> (default: 1)
423              
424             Decide whether generated function will support filtering (the FIELD, FIELD.is, FIELD.min, etc arguments).
425              
426             =item * B<enable_ordering> => I<bool> (default: 1)
427              
428             Decide whether generated function will support ordering (the `sort` & `random` arguments).
429              
430             =item * B<enable_paging> => I<bool> (default: 1)
431              
432             Decide whether generated function will support paging (the `result_limit` & `result_start` arguments).
433              
434             =item * B<enable_random_ordering> => I<bool> (default: 1)
435              
436             Decide whether generated function will support random ordering (the `random` argument).
437              
438             Ordering must also be enabled (C<enable_ordering>).
439              
440             =item * B<enable_search> => I<bool> (default: 1)
441              
442             Decide whether generated function will support searching (argument q).
443              
444             Filtering must also be enabled (C<enable_filtering>).
445              
446             =item * B<extra_args> => I<hash>
447              
448             Extra arguments for the generated function.
449              
450             =item * B<fields_aliases> => I<hash>
451              
452             =item * B<hooks> => I<hash>
453              
454             Supply hooks.
455              
456             You can instruct the generated function to execute codes in various stages by
457             using hooks. Currently available hooks are: C<before_parse_query>,
458             C<after_parse_query>, C<before_fetch_data>, C<after_fetch_data>, C<before_return>.
459             Hooks will be passed the function arguments as well as one or more additional
460             ones. All hooks will get C<_stage> (name of stage) and C<_func_res> (function
461             arguments, but as hash reference so you can modify it). C<after_parse_query> and
462             later hooks will also get C<_parse_res> (parse result). C<before_fetch_data> and
463             later will also get C<_query>. C<after_fetch_data> and later will also get
464             C<_data>. C<before_return> will also get C<_func_res> (the enveloped response to be
465             returned to user).
466              
467             Hook should return nothing or a false value on success. It can abort execution
468             of the generated function if it returns an envelope response (an array). On that
469             case, the function will return with this return value.
470              
471             =item * B<install> => I<bool> (default: 1)
472              
473             Whether to install generated function (and metadata).
474              
475             By default, generated function will be installed to the specified (or caller's)
476             package, as well as its generated metadata into %SPEC. Set this argument to
477             false to skip installing.
478              
479             =item * B<langs> => I<array[str]> (default: ["en_US"])
480              
481             Choose language for function metadata.
482              
483             This function can generate metadata containing text from one or more languages.
484             For example if you set 'langs' to ['en_US', 'id_ID'] then the generated function
485             metadata might look something like this:
486              
487             {
488             v => 1.1,
489             args => {
490             random => {
491             summary => 'Random order of results', # English
492             "summary.alt.lang.id_ID" => "Acak urutan hasil", # Indonesian
493             ...
494             },
495             ...
496             },
497             ...
498             }
499              
500             =item * B<name> => I<str>
501              
502             Generated function's name, e.g. `myfunc`.
503              
504             =item * B<package> => I<str>
505              
506             Generated function's package, e.g. `My::Package`.
507              
508             This is needed mostly for installing the function. You usually don't need to
509             supply this if you set C<install> to false.
510              
511             If not specified, caller's package will be used by default.
512              
513             =item * B<query_aliases> => I<hash>
514              
515             =item * B<random_aliases> => I<hash>
516              
517             =item * B<result_limit_aliases> => I<hash>
518              
519             =item * B<result_start_aliases> => I<hash>
520              
521             =item * B<sort_aliases> => I<hash>
522              
523             =item * B<summary> => I<str>
524              
525             Generated function's summary.
526              
527             =item * B<table_name>* => I<str>
528              
529             DBI table name.
530              
531             =item * B<table_spec>* => I<hash>
532              
533             Table specification.
534              
535             Just like Perinci::Sub::Gen::AccessTable's table_spec, except that each field
536             specification can have a key called C<db_field> to specify the database field (if
537             different). Currently this is required. Future version will be able to generate
538             table_spec from table schema if table_spec is not specified.
539              
540             =item * B<with_field_names_aliases> => I<hash>
541              
542             =item * B<word_search> => I<bool> (default: 0)
543              
544             Decide whether generated function will perform word searching instead of string searching.
545              
546             For example, if search term is 'pine' and field value is 'green pineapple',
547             search will match if word_search=false, but won't match under word_search.
548              
549             This will not have effect under 'custom_search'.
550              
551             =back
552              
553             Returns an enveloped result (an array).
554              
555             First element (status) is an integer containing HTTP status code
556             (200 means OK, 4xx caller error, 5xx function error). Second element
557             (msg) is a string containing error message, or 'OK' if status is
558             200. Third element (result) is optional, the actual result. Fourth
559             element (meta) is called result metadata and is optional, a hash
560             that contains extra information.
561              
562             Return value: A hash containing generated function, metadata (hash)
563              
564             =head1 FAQ
565              
566             =head1 HOMEPAGE
567              
568             Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Sub-Gen-AccessTable-DBI>.
569              
570             =head1 SOURCE
571              
572             Source repository is at L<https://github.com/perlancar/perl-Perinci-Sub-Gen-AccessTable-DBI>.
573              
574             =head1 BUGS
575              
576             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Sub-Gen-AccessTable-DBI>
577              
578             When submitting a bug or request, please include a test-file or a
579             patch to an existing test-file that illustrates the bug or desired
580             feature.
581              
582             =head1 SEE ALSO
583              
584             L<Perinci::Sub::Gen::AccessTable>
585              
586             =head1 AUTHOR
587              
588             perlancar <perlancar@cpan.org>
589              
590             =head1 COPYRIGHT AND LICENSE
591              
592             This software is copyright (c) 2017, 2016, 2015, 2014, 2013, 2012 by perlancar@cpan.org.
593              
594             This is free software; you can redistribute it and/or modify it under
595             the same terms as the Perl 5 programming language system itself.
596              
597             =cut