File Coverage

blib/lib/DBIx/PreQL.pm
Criterion Covered Total %
statement 146 166 87.9
branch 66 94 70.2
condition 24 45 53.3
subroutine 19 23 82.6
pod 1 1 100.0
total 256 329 77.8


line stmt bran cond sub pod time code
1             package DBIx::PreQL;
2 3     3   3752 use strict;
  3         7  
  3         147  
3 3     3   21 use warnings;
  3         6  
  3         129  
4 3     3   34 use Carp qw< croak carp >;
  3         7  
  3         266  
5             require overload; # Just for Overloaded()
6              
7 3     3   18 use constant TAG_ALWAYS => '*';
  3         5  
  3         226  
8 3     3   17 use constant TAG_NEVER => '#';
  3         5  
  3         161  
9 3     3   17 use constant TAG_IF_ALL_EXIST => '&';
  3         5  
  3         139  
10 3     3   30 use constant TAG_IF_ANY_EXIST => '|';
  3         12  
  3         284  
11 3     3   17 use constant TAG_IF_PREFIXED => qr/^([&|])(?![&|])(\S*)$/;
  3         6  
  3         10037  
12              
13             # Substitution types:
14             # !ifset!, !~ifnotset!
15             # ?param?, ?@array_ref?
16             # ?=same_as?, ?!distinct?
17             # ?"literal_sql?
18              
19              
20             sub _parse_data {
21 30     30   1791 my( $data ) = @_;
22              
23 30 100 66     440 croak "data must be a hash reference"
24             unless ! defined($data)
25             || ref($data) eq 'HASH';
26              
27 28   50     88 return $data || {};
28             }
29              
30              
31             sub _parse_wanted {
32 30     30   3135 my( $w ) = @_;
33              
34 30 100       120 return if ! defined $w;
35              
36 12 100       47 return $w if ref $w eq 'CODE';
37              
38 6 100       89 if( ref $w eq 'ARRAY' ) {
39 5         8 my %w; @w{@$w} = (1) x @$w;
  5         51  
40              
41 5     20   30 return sub { return $w{$_[0]} };
  20         100  
42             }
43              
44 1         197 croak "wanted must be an array ref or code ref";
45             }
46              
47              
48             sub _split_query {
49 27     27   41 my( $q ) = @_;
50              
51 27 50 33     730 croak "No query specified"
52             if ! defined($q) || ! length($q);
53              
54 43         929 my @lines = grep /\S/, # Remove blank lines
55             map {
56             # Split into lines, removing trailing whitespace:
57 27 100       81 split /[^\S\n]*\n/, $_
58             } ref $q eq 'ARRAY' ? @$q : ($q);
59 27         50 my $indent;
60 27         47 for( @lines ) {
61             next # Lines with nothing after the tag have no indentation
62 161 50       1372 if ! /^(\s*\S+\s+)/;
63 161         404 my $l = length( $1 ); # Measure how much the SQL is indented
64 161 100 66     914 $indent = $l # Track the least amount of indentation
65             if ! $indent || $l < $indent;
66             }
67 27         115 return( $indent, @lines );
68             }
69              
70              
71             sub _parse_line {
72 154     154   268 my( $line, $indent ) = @_;
73              
74 154 50       1001 my( $pre, $tag, $post, $sql ) = $line =~ /^(\s*)(\S+)(\s*)(.*)$/
75             or die "Impossible: SQL template line w/o tag:\n$line\nfound";
76              
77 154         663 my $context = ":\n $tag $sql\n"; # Used for error messages.
78             # Preserve indentation (using minimal spaces):
79 154         408 $indent = ' ' x( length("$pre$tag$post") - $indent );
80              
81              
82 154         182 my $prefix = '';
83 154 100       691 if( $tag =~ TAG_IF_PREFIXED ) { # '&', '|', '&FOO', or '|BAR' tag:
84 61         152 ( $prefix, $tag ) = ( $1, $2 );
85              
86             # Note that '&*' and '|*' act like '&' and '|' (respectively)
87 61 50       153 $tag = TAG_ALWAYS
88             if '' eq $tag;
89             }
90              
91 154         940 return $prefix, $tag, $indent, $sql, $context;
92             }
93              
94             sub _find_named_placeholders {
95 151     151   213 my( $sql, $data ) = @_;
96              
97 151         157 my( @pholders, @found, @missing );
98 151         677 while( $sql =~ /\?[\@=!"]?(\w+)\?/g ) {
99 76         138 my $name = $1;
100 76         137 push @pholders, $name;
101              
102 76 100       171 if( defined $data->{$name} ) {
103 56         197 push @found, $name;
104             }
105             else {
106 20         70 push @missing, $name
107             }
108             }
109              
110             return {
111 151         633 all => \@pholders,
112             found => \@found,
113             missing => \@missing,
114             };
115             }
116              
117             sub _find_dependencies {
118 151     151   267 my( $sql, $data ) = @_;
119              
120 151         162 my( @deps, @found, @lost );
121 151         547 while( $sql =~ /!(~?)(\w+)!/g ) {
122 44         93 my( $negated, $name ) = ( $1, $2 );
123              
124 44         70 push @deps, $name;
125              
126 44         158 my $found = defined $data->{$name};
127 44 50       85 $found = ! $found
128             if $negated;
129              
130 44 100       80 if( $found ) {
131 21         66 push @found, $name;
132             } else {
133 23         75 push @lost, $name;
134             }
135             }
136              
137             return {
138 151         603 all => \@deps,
139             found => \@found,
140             missing => \@lost,
141             };
142             }
143              
144             sub _select_line {
145 154     154   252 my( $line, $base_indent, $data, $want, $known_tags ) = @_;
146              
147 154         278 my( $prefix, $tag, $indent, $sql, $context ) = _parse_line( $line, $base_indent );
148              
149             return
150 154 100       587 if TAG_NEVER eq $tag;
151              
152 151         262 my $nph = _find_named_placeholders( $sql, $data );
153 151         397 my $deps = _find_dependencies( $sql, $data );
154              
155             return
156 151 100 100     2222 if @{$nph->{missing}}
  151         563  
157             && $prefix; # $tag starts with '&' or '|'
158              
159 137 50       614 croak "Missing tag?$context" # Catch "* SELECT\n *\n* FROM\n"
160             if $sql !~ /\S/; # ^ missing tag
161              
162             #TAG_IF_ALL_EXIST, {
163             # die_msg => "No parameters nor dependency markers specified",
164             # die_unless => [ $nph->{all}, $deps->{all} ],
165             # skip_if => [ $deps->{missing} ],
166             #},
167             #
168             #TAG_IF_ANY_EXIST, {
169             # die_msg => "No dependency markers specified",
170             # die_unless => [ $deps->{all} ],
171             # skip_unless => [ $deps->{found} ],
172             #},
173             #TAG_ALWAYS, {
174             # die_msg => "Dependency markers ({$deps->{all}}) used with wrong tag type"
175             # die_if => [ $deps->{all} ],
176             #}
177              
178              
179              
180              
181 137 100       397 if( TAG_IF_ALL_EXIST eq $prefix ) {
    100          
182 37         110 croak "No parameters nor dependency markers specified$context"
183 21         229 if ! @{$nph->{all}}
184 37 100 66     43 && ! @{$deps->{all}};
185             return
186 36 100       44 if @{$deps->{missing}};
  36         345  
187             } elsif( TAG_IF_ANY_EXIST eq $prefix ) {
188 10         243 croak "No dependency markers specified$context"
189 10 100       15 if ! @{$deps->{all}};
190             return
191 9 100       10 if ! @{$deps->{found}};
  9         59  
192             } else {
193 0         0 croak "Dependency markers (@{$deps->{all}}) used with wrong tag type$context"
  90         225  
194 90 50       95 if @{$deps->{all}};
195              
196 90 100       220 if( TAG_ALWAYS ne $tag ) { # Handle custom tags:
197 39 50       64 if( $known_tags ) {
198 0 0       0 croak "Unknown tag found$context"
199             if ! $known_tags->{$tag}++;
200             } else {
201 39 50 33     591 croak "Missing tag?$context"
202             if $tag =~ /,$/
203             || grep( $_ eq uc $tag, qw<
204             SELECT FROM LEFT RIGHT INNER OUTER JOIN USING ON
205             WHERE AND OR
206             ORDER GROUP BY LIMIT OFFSET HAVING
207             UNION ALL DISTINCT
208             INSERT UPDATE ALTER CREATE DROP
209             WITH OVER BETWEEN
210             > );
211             }
212 39 100       236 croak "No wanted function provided when using custom tag$context"
213             if ! $want;
214             return
215 38 100       83 if ! $want->( $tag, $data );
216             }
217             }
218              
219 2         341 croak "Missing named place-holders (@{$nph->{missing}})$context"
  107         330  
220 107 100       172 if @{$nph->{missing}};
221              
222 105         1734 return( $indent . $sql, $context );
223             }
224              
225              
226             sub _bad_type {
227 0     0   0 my( $sigil, $name, $val, $context ) = @_;
228              
229 0         0 my $type = ref($val);
230 0         0 croak "Invalid type ($type) for ?$sigil$name?$context";
231             }
232              
233             {
234             #SIGIL => { VALTYPE => sub { param => ?, repl => ? } }
235             # sub takes args $name, $value.
236             # returns kvp list of param and repl to override defaults.
237             my %NPH_HANDLER = (
238             '' => { '' => sub { param => [$_[0]] },
239             SCALAR => sub { repl => ${$_[1]} },
240             },
241             '@' => { ARRAY => sub { param => [$_[0]] } },
242             '"' => { '' => sub { repl => $_[1] },
243             SCALAR => sub { repl => ${$_[1]} },
244             },
245             '=' => { '' => sub { param => [$_[0]], repl => '= ?' },
246             SCALAR => sub { repl => "= ${$_[1]}" },
247             NULL => sub { repl => 'IS NULL' },
248             },
249             '!' => { '' => sub { param => [$_[0]], repl => '<> ?' },
250             SCALAR => sub { repl => "<> ${$_[1]}" },
251             NULL => sub { repl => 'IS NOT NULL' },
252             },
253             );
254              
255             sub _process_named_placeholder {
256 50     50   75 my ($sigil, $name, $value) = @_;
257              
258 50   50     186 my $type = ref $value || '';
259 50 50 33     111 $type = 'NULL'
260             if $type && _is_null( $type );
261              
262             return
263 50 50 33     252 unless exists( $NPH_HANDLER{$sigil} )
264             && exists( $NPH_HANDLER{$sigil}{$type} );
265              
266 50         86 my $handler = $NPH_HANDLER{$sigil}{$type};
267              
268 50         113 return { repl => '?', param => [], $handler->($name, $value) };
269             }
270              
271             }
272              
273             sub _substitute_line {
274 105     105   242 my( $sql, $context, $data ) = @_;
275              
276             # Remove dependency markers and extra whitespace:
277             # "a !b! !c!" => "a"
278             # "a!b!c" => "ac"
279             # "a !b! c" or "a !b!c" => "a c"
280 105         263 $sql =~ s/(\s*)!~?\w+!(?=(\s*)(\S?))/
281 21 50 66     190 $1 && !$2 && length $3 ? ' ' : '';
282             /ge;
283              
284             # Replace named parameters with computed text
285             # Fill param array.
286 105         116 my @params;
287 105         271 $sql =~ s{\?([\@=!"]?)(\w+)\?}{
288 50         113 my( $sigil, $name ) = ( $1, $2 );
289 50         91 my $value = $data->{$name};
290              
291 50 50       169 my $np = _process_named_placeholder( $sigil, $name, $value )
292             or _bad_type( $sigil, $name, $value, $context );
293              
294 50         79 push @params, @{$np->{param}};
  50         240  
295              
296 50         204 $np->{repl};
297             }ge;
298              
299 105         361 return( $sql, @params );
300             }
301              
302              
303             sub build_query {
304 27     27 1 41663 my( $class, %a ) = @_;
305 27 50       91 croak "You must call build_query() as a class method"
306             if $class ne __PACKAGE__;
307              
308 27         97 my $data = _parse_data( delete $a{data} );
309 27         78 my $want = _parse_wanted( delete $a{wanted} );
310 27         78 my @query_lines = _split_query( delete $a{query} );
311 27         54 my $indent = shift @query_lines;
312 27         45 my $keep_keys = delete $a{keep_keys};
313 27         40 my $known_tags = delete $a{known_tags};
314 27   50     56 $known_tags &&= { map { $_ => 1 } @$known_tags };
  0         0  
315              
316 27         69 my @unexpected_keys = sort keys %a;
317 27 50       55 croak "Unexpected arguments given to build_query(): @unexpected_keys"
318             if @unexpected_keys;
319              
320 27         30 my @query; # Accumulate lines in loop
321             my @binds; # Build list in loop
322              
323 27         43 for ( @query_lines ) {
324             # Determine if we should include this line:
325 154 100       459 my( $line, $context ) = _select_line(
326             $_, $indent, $data, $want, $known_tags,
327             )
328             or next;
329              
330 105         307 ( $line, my @params ) = _substitute_line( $line, $context, $data );
331              
332             # Remove trailing comma if in front of 'FROM':
333 105 100 100     564 $query[-1] =~ s/,\s*$//
334             if @query && $line =~ /^\s*FROM\b/i;
335              
336             # Remove leading 'AND' if behind 'WHERE':
337 105 100 100     1044 $line =~ s/^(\s*)AND\b/$1 /
338             if @query && $query[-1] =~ /\bWHERE\s*$/i;
339              
340 105         366 push @query, $line;
341 105         238 push @binds, @params;
342             }
343              
344 22         74 my $query = join "\n", @query;
345              
346 22 100       49 if( ! $keep_keys ) {
347             $_ = $data->{$_}
348 17         59 for @binds;
349             }
350              
351 22 50       48 if( $known_tags ) {
352 0         0 my @unused = grep 1==$known_tags->{$_}, sort keys %$known_tags;
353 0 0       0 carp "Some known tags never used (@unused)"
354             if @unused;
355             }
356              
357 22         188 return $query, @binds;
358             }
359              
360              
361             sub _is_ref {
362 0     0     my( $val ) = @_;
363 0 0 0       return 0
364             if ! ref $val
365             || overload::Method( $val, '""' );
366 0           return 1;
367             }
368              
369              
370             sub _is_a {
371 0     0     my( $val, $type ) = @_;
372 0 0         return 0
373             if ! _is_ref( $val );
374 0           my $isa = eval { $val->isa($type) };
  0            
375 0 0         $isa = UNIVERSAL::isa( $val, $type )
376             if ! defined $isa;
377 0           return $isa;
378             }
379              
380              
381             sub _is_null {
382 0     0     my( $val ) = @_;
383 0 0 0       return 1
384             if _is_a( $val, 'SCALAR' )
385             && $$val =~ /^\s*NULL\s*$/i;
386 0           return 0;
387             }
388              
389              
390             1;
391              
392             __END__