File Coverage

blib/lib/Perinci/Sub/Gen/AccessTable/DBI.pm
Criterion Covered Total %
statement 106 121 87.6
branch 53 76 69.7
condition 14 29 48.2
subroutine 13 13 100.0
pod 1 1 100.0
total 187 240 77.9


line stmt bran cond sub pod time code
1             package Perinci::Sub::Gen::AccessTable::DBI;
2              
3             our $DATE = '2015-02-18'; # DATE
4             our $VERSION = '0.12'; # VERSION
5              
6 1     1   35939 use 5.010001;
  1         3  
  1         33  
7 1     1   3 use strict;
  1         2  
  1         24  
8 1     1   4 use warnings;
  1         1  
  1         29  
9 1     1   3 use experimental 'smartmatch';
  1         2  
  1         7  
10 1     1   77 use Log::Any '$log';
  1         2  
  1         10  
11              
12 1     1   536 use Function::Fallback::CoreOrPP qw(clone);
  1         467  
  1         58  
13 1     1   475 use Locale::TextDomain::UTF8 'Perinci-Sub-Gen-AccessTable-DBI';
  1         13681  
  1         7  
14 1     1   10544 use DBI;
  1         3  
  1         63  
15 1     1   776 use Perinci::Sub::Gen::AccessTable qw(gen_read_table_func);
  1         20909  
  1         82  
16 1     1   9 use Perinci::Sub::Util qw(gen_modified_sub);
  1         2  
  1         1108  
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   787 require Data::Sah::Normalize;
26 36         1365 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             );
69             sub gen_read_dbi_table_func {
70 6     6 1 459334 my %args = @_;
71              
72             # XXX schema
73 6         18 my $table_name = $args{table_name}; delete $args{table_name};
  6         16  
74 6 50       23 $table_name or return [400, "Please specify table_name"];
75 6         12 my $dbh = $args{dbh}; delete $args{dbh};
  6         13  
76 6 50       24 $dbh or return [400, "Please specify dbh"];
77              
78             # duplicate and make each field's schema normalized
79 6         30 my $table_spec = clone($args{table_spec});
80 6         169 for my $fspec (values %{$table_spec->{fields}}) {
  6         26  
81 36   50     538 $fspec->{schema} //= 'any';
82 36         56 $fspec->{schema} = __parse_schema($fspec->{schema});
83             }
84              
85             my $table_data = sub {
86 80     80   185390 my $query = shift;
87              
88 80         563 my ($db) = $dbh->get_info(17);
89 80 50       842 unless ($db =~ /\A(SQLite|mysql|Pg)\z/) {
90 0         0 $log->warnf("$label Database is not supported: %s", $db);
91             }
92              
93             # function to quote identifier, e.g. `col` or "col"
94             my $qi = sub {
95 318 50       1113 if ($db =~ /SQLite|mysql/) { return "`$_[0]`" }
  318         1123  
96 0         0 return qq("$_[0]");
97 80         356 };
98              
99 80         122 my $fspecs = $table_spec->{fields};
100 80         247 my @fields = keys %$fspecs;
101 480 50       1137 my @searchable_fields = grep {
102 80         124 !defined($fspecs->{$_}{searchable}) || $fspecs->{$_}{searchable}
103             } @fields;
104              
105 80         77 my $filtered;
106             my @wheres;
107             # XXX case_insensitive_search & word_search not yet observed
108 80         100 my $q = $query->{q};
109 80 100 66     193 if (defined($q) && @searchable_fields) {
110 12   33     82 push @wheres, "(".
111 2         3 join(" OR ", map {$qi->($fspecs->{$_}{db_field}//$_)." LIKE ".
112             $dbh->quote("%$q%")}
113             @searchable_fields).
114             ")";
115             }
116 80 50       160 if ($args{custom_search}) {
117 0         0 $filtered = 0; # perigen-acctbl will be doing custom_search
118             }
119 80 50       137 if ($args{custom_filter}) {
120 0         0 $filtered = 0; # perigen-acctbl will be doing custom_search
121             }
122 80         102 for my $filter (@{$query->{filters}}) {
  80         144  
123 24         41 my ($f, $ftype, $op, $opn) = @$filter;
124 24   33     116 my $qdbf = $qi->($fspecs->{$f}{db_field} // $f);
125 24         112 my $qopn = $dbh->quote($opn);
126 24 100       335 if ($op eq 'truth') { push @wheres, $qdbf
  5 50       14  
    50          
    50          
    100          
    50          
    50          
    50          
    100          
    50          
    100          
    50          
    100          
    50          
    100          
    100          
    100          
    100          
    50          
    0          
127 0         0 } elsif ($op eq '~~') { $filtered = 0 # not supported
128 0         0 } elsif ($op eq '!~~') { $filtered = 0 # not supported
129 0         0 } elsif ($op eq 'eq') { push @wheres, "$qdbf = $qopn"
130 6         13 } elsif ($op eq '==') { push @wheres, "$qdbf = $qopn"
131 0         0 } elsif ($op eq 'ne') { push @wheres, "$qdbf <> $qopn"
132 0         0 } elsif ($op eq '!=') { push @wheres, "$qdbf <> $qopn"
133 0         0 } elsif ($op eq 'ge') { push @wheres, "$qdbf >= $qopn"
134 4         13 } elsif ($op eq '>=') { push @wheres, "$qdbf >= $qopn"
135 0         0 } elsif ($op eq 'gt') { push @wheres, "$qdbf > $qopn"
136 1         5 } elsif ($op eq '>' ) { push @wheres, "$qdbf > $qopn"
137 0         0 } elsif ($op eq 'le') { push @wheres, "$qdbf <= $qopn"
138 3         10 } elsif ($op eq '<=') { push @wheres, "$qdbf <= $qopn"
139 0         0 } elsif ($op eq 'lt') { push @wheres, "$qdbf < $qopn"
140 1         4 } elsif ($op eq '<' ) { push @wheres, "$qdbf < $qopn"
141 1         5 } elsif ($op eq '=~') { $filtered = 0 # not supported
142 1         3 } elsif ($op eq '!~') { $filtered = 0 # not supported
143 1         3 } elsif ($op eq 'pos') { $filtered = 0 # different substr funcs
144 1         3 } elsif ($op eq '!pos') { $filtered = 0 # different substr funcs
145 0         0 } elsif ($op eq 'call') { $filtered = 0 # not supported
146             } else {
147 0         0 die "BUG: Unknown op $op";
148             }
149             }
150 80   100     266 $filtered //= 1;
151              
152 80         73 my $sorted;
153             my @orders;
154 80 100       153 if ($query->{random}) {
  30 100       92  
155 50         73 push @orders, "RANDOM()";
156             } elsif (@{$query->{sorts}}) {
157 5         6 for my $s (@{$query->{sorts}}) {
  5         11  
158 6         10 my ($f, $op, $desc) = @$s;
159 6 100 33     35 push @orders, $qi->($fspecs->{$f}{db_field} // $f).
160             ($desc == -1 ? " DESC" : "");
161             }
162             }
163 80   50     226 $sorted //= 1;
164              
165 80         56 my $paged;
166 80         80 my $limit = "";
167 80         160 my ($ql, $qs) = ($query->{result_limit}, $query->{result_start}-1);
168 80 100 66     309 if (defined($ql) || $qs > 0) {
169 2 0 33     15 $limit = join(
    50          
    100          
170             "",
171             "LIMIT ".($ql // ($db eq 'Pg' ? "ALL":"999999999")),
172             ($qs > 1 ? ($db eq 'mysql' ? ",$qs" : " OFFSET $qs") : "")
173             );
174             }
175 80   50     195 $paged //= 1;
176              
177 98   33     301 my $sql = join(
178             "",
179             "SELECT ",
180 80         115 join(",", map {$qi->($fspecs->{$_}{db_field}//$_)." AS ".$qi->($_)}
181 80 100       93 @{$query->{requested_fields}}).
    100          
182             " FROM ".$qi->($table_name),
183             (@wheres ? " WHERE ".join(" AND ", @wheres) : ""),
184             (@orders ? " ORDER BY ".join(",", @orders) : ""),
185             $limit,
186             );
187 80         285 $log->tracef("$label SQL=%s", $sql);
188              
189 80         415 my $sth = $dbh->prepare($sql);
190 80 50       10198 $sth->execute or die "Can't query: ".$sth->errstr;
191 80         149 my @r;
192 80         1560 while (my $row = $sth->fetchrow_hashref) { push @r, $row }
  275         3073  
193              
194 80         1604 {data=>\@r, paged=>$paged, filtered=>$filtered, sorted=>$sorted,
195             fields_selected=>0, # XXX i'm lazy to handle detail=0
196             };
197 6         156 };
198              
199 6         37 @_ = (%args, table_data => $table_data);
200 6         34 goto &gen_read_table_func;
201             }
202              
203             1;
204             # ABSTRACT: Generate function (and its metadata) to read DBI table
205              
206             __END__