File Coverage

blib/lib/Perinci/Sub/Gen/AccessTable/DBI.pm
Criterion Covered Total %
statement 103 118 87.2
branch 53 76 69.7
condition 14 29 48.2
subroutine 12 12 100.0
pod 1 1 100.0
total 183 236 77.5


line stmt bran cond sub pod time code
1             package Perinci::Sub::Gen::AccessTable::DBI;
2              
3 1     1   65806 use 5.010001;
  1         4  
  1         33  
4 1     1   6 use strict;
  1         3  
  1         30  
5 1     1   4 use warnings;
  1         2  
  1         31  
6 1     1   880 use experimental 'smartmatch';
  1         882  
  1         8  
7 1     1   62 use Log::Any '$log';
  1         2  
  1         9  
8              
9 1     1   2866 use Locale::TextDomain::UTF8 'Perinci-Sub-Gen-AccessTable-DBI';
  1         31468  
  1         8  
10 1     1   18122 use Data::Clone;
  1         912  
  1         69  
11 1     1   12 use DBI;
  1         2  
  1         39  
12 1     1   1508 use Perinci::Sub::Gen::AccessTable qw(gen_read_table_func);
  1         40893  
  1         2055  
13              
14             require Exporter;
15             our @ISA = qw(Exporter);
16             our @EXPORT_OK = qw(gen_read_dbi_table_func);
17              
18             our $VERSION = '0.11'; # VERSION
19              
20             our %SPEC;
21             my $label = "(gen_read_dbi_table_func)";
22              
23             sub __parse_schema {
24 36     36   3388 require Data::Sah;
25 36         53183 Data::Sah::normalize_schema($_[0]);
26             }
27              
28             my $spec = clone $Perinci::Sub::Gen::AccessTable::SPEC{gen_read_table_func};
29             $spec->{summary} = 'Generate function (and its metadata) to read DBI table';
30             $spec->{description} = <<'_';
31              
32             The generated function acts like a simple single table SQL SELECT query,
33             featuring filtering, ordering, and paging, but using arguments as the 'query
34             language'. The generated function is suitable for exposing a table data from an
35             API function. Please see Perinci::Sub::Gen::AccessTable's documentation for more
36             details on what arguments the generated function will accept.
37              
38             _
39             delete $spec->{args}{table_data};
40             $spec->{args}{table_name} = {
41             req => 1,
42             schema => 'str*',
43             summary => 'DBI table name',
44             };
45             $spec->{args}{table_spec}{description} = <<'_';
46              
47             Just like Perinci::Sub::Gen::AccessTable's table_spec, except that each field
48             specification can have a key called `db_field` to specify the database field (if
49             different). Currently this is required. Future version will be able to generate
50             table_spec from table schema if table_spec is not specified.
51              
52             _
53             $spec->{args}{dbh} = {
54             schema => 'obj*',
55             summary => 'DBI database handle',
56             };
57             $SPEC{gen_read_dbi_table_func} = $spec;
58             sub gen_read_dbi_table_func {
59 6     6 1 358763 my %args = @_;
60              
61             # XXX schema
62 6         16 my $table_name = $args{table_name}; delete $args{table_name};
  6         16  
63 6 50       28 $table_name or return [400, "Please specify table_name"];
64 6         12 my $dbh = $args{dbh}; delete $args{dbh};
  6         15  
65 6 50       19 $dbh or return [400, "Please specify dbh"];
66              
67             # duplicate and make each field's schema normalized
68 6         155 my $table_spec = clone($args{table_spec});
69 6         21 for my $fspec (values %{$table_spec->{fields}}) {
  6         284  
70 36   50     3566 $fspec->{schema} //= 'any';
71 36         86 $fspec->{schema} = __parse_schema($fspec->{schema});
72             }
73              
74             my $table_data = sub {
75 80     80   322831 my $query = shift;
76              
77 80         738 my ($db) = $dbh->get_info(17);
78 80 50       932 unless ($db =~ /\A(SQLite|mysql|Pg)\z/) {
79 0         0 $log->warnf("$label Database is not supported: %s", $db);
80             }
81              
82             # function to quote identifier, e.g. `col` or "col"
83             my $qi = sub {
84 318 50       7469 if ($db =~ /SQLite|mysql/) { return "`$_[0]`" }
  318         2420  
85 0         0 return qq("$_[0]");
86 80         547 };
87              
88 80         161 my $fspecs = $table_spec->{fields};
89 80         284 my @fields = keys %$fspecs;
90 480 50       1777 my @searchable_fields = grep {
91 80         164 !defined($fspecs->{$_}{searchable}) || $fspecs->{$_}{searchable}
92             } @fields;
93              
94 80         118 my $filtered;
95             my @wheres;
96             # XXX case_insensitive_search & word_search not yet observed
97 80         151 my $q = $query->{q};
98 80 100 66     224 if (defined($q) && @searchable_fields) {
99 12   33     135 push @wheres, "(".
100 2         8 join(" OR ", map {$qi->($fspecs->{$_}{db_field}//$_)." LIKE ".
101             $dbh->quote("%$q%")}
102             @searchable_fields).
103             ")";
104             }
105 80 50       217 if ($args{custom_search}) {
106 0         0 $filtered = 0; # perigen-acctbl will be doing custom_search
107             }
108 80 50       165 if ($args{custom_filter}) {
109 0         0 $filtered = 0; # perigen-acctbl will be doing custom_search
110             }
111 80         83 for my $filter (@{$query->{filters}}) {
  80         186  
112 24         53 my ($f, $ftype, $op, $opn) = @$filter;
113 24   33     145 my $qdbf = $qi->($fspecs->{$f}{db_field} // $f);
114 24         140 my $qopn = $dbh->quote($opn);
115 24 100       451 if ($op eq 'truth') { push @wheres, $qdbf
  5 50       20  
    50          
    50          
    100          
    50          
    50          
    50          
    100          
    50          
    100          
    50          
    100          
    50          
    100          
    100          
    100          
    100          
    50          
    0          
116 0         0 } elsif ($op eq '~~') { $filtered = 0 # not supported
117 0         0 } elsif ($op eq '!~~') { $filtered = 0 # not supported
118 0         0 } elsif ($op eq 'eq') { push @wheres, "$qdbf = $qopn"
119 6         188 } elsif ($op eq '==') { push @wheres, "$qdbf = $qopn"
120 0         0 } elsif ($op eq 'ne') { push @wheres, "$qdbf <> $qopn"
121 0         0 } elsif ($op eq '!=') { push @wheres, "$qdbf <> $qopn"
122 0         0 } elsif ($op eq 'ge') { push @wheres, "$qdbf >= $qopn"
123 4         18 } elsif ($op eq '>=') { push @wheres, "$qdbf >= $qopn"
124 0         0 } elsif ($op eq 'gt') { push @wheres, "$qdbf > $qopn"
125 1         5 } elsif ($op eq '>' ) { push @wheres, "$qdbf > $qopn"
126 0         0 } elsif ($op eq 'le') { push @wheres, "$qdbf <= $qopn"
127 3         12 } elsif ($op eq '<=') { push @wheres, "$qdbf <= $qopn"
128 0         0 } elsif ($op eq 'lt') { push @wheres, "$qdbf < $qopn"
129 1         5 } elsif ($op eq '<' ) { push @wheres, "$qdbf < $qopn"
130 1         4 } elsif ($op eq '=~') { $filtered = 0 # not supported
131 1         6 } elsif ($op eq '!~') { $filtered = 0 # not supported
132 1         3 } elsif ($op eq 'pos') { $filtered = 0 # different substr funcs
133 1         4 } elsif ($op eq '!pos') { $filtered = 0 # different substr funcs
134 0         0 } elsif ($op eq 'call') { $filtered = 0 # not supported
135             } else {
136 0         0 die "BUG: Unknown op $op";
137             }
138             }
139 80   100     453 $filtered //= 1;
140              
141 80         95 my $sorted;
142             my @orders;
143 80 100       175 if ($query->{random}) {
  30 100       92  
144 50         72 push @orders, "RANDOM()";
145             } elsif (@{$query->{sorts}}) {
146 5         7 for my $s (@{$query->{sorts}}) {
  5         12  
147 6         12 my ($f, $op, $desc) = @$s;
148 6 100 33     36 push @orders, $qi->($fspecs->{$f}{db_field} // $f).
149             ($desc == -1 ? " DESC" : "");
150             }
151             }
152 80   50     271 $sorted //= 1;
153              
154 80         81 my $paged;
155 80         100 my $limit = "";
156 80         186 my ($ql, $qs) = ($query->{result_limit}, $query->{result_start}-1);
157 80 100 66     323 if (defined($ql) || $qs > 0) {
158 2 0 33     22 $limit = join(
    50          
    100          
159             "",
160             "LIMIT ".($ql // ($db eq 'Pg' ? "ALL":"999999999")),
161             ($qs > 1 ? ($db eq 'mysql' ? ",$qs" : " OFFSET $qs") : "")
162             );
163             }
164 80   50     234 $paged //= 1;
165              
166 98   33     368 my $sql = join(
167             "",
168             "SELECT ",
169 80         142 join(",", map {$qi->($fspecs->{$_}{db_field}//$_)." AS ".$qi->($_)}
170 80 100       108 @{$query->{requested_fields}}).
    100          
171             " FROM ".$qi->($table_name),
172             (@wheres ? " WHERE ".join(" AND ", @wheres) : ""),
173             (@orders ? " ORDER BY ".join(",", @orders) : ""),
174             $limit,
175             );
176 80         373 $log->tracef("$label SQL=%s", $sql);
177              
178 80         2145 my $sth = $dbh->prepare($sql);
179 80 50       15143 $sth->execute or die "Can't query: ".$sth->errstr;
180 80         151 my @r;
181 80         1871 while (my $row = $sth->fetchrow_hashref) { push @r, $row }
  275         4179  
182              
183 80         1903 {data=>\@r, paged=>$paged, filtered=>$filtered, sorted=>$sorted,
184             fields_selected=>0, # XXX i'm lazy to handle detail=0
185             };
186 6         620 };
187              
188 6         48 @_ = (%args, table_data => $table_data);
189 6         751 goto &gen_read_table_func;
190             }
191              
192             1;
193             # ABSTRACT: Generate function (and its Rinci metadata) to access DBI table data
194              
195             __END__