File Coverage

blib/lib/SQL/Wizard.pm
Criterion Covered Total %
statement 191 194 98.4
branch 69 92 75.0
condition 5 10 50.0
subroutine 51 52 98.0
pod 26 36 72.2
total 342 384 89.0


line stmt bran cond sub pod time code
1             package SQL::Wizard;
2              
3 14     14   1382761 use strict;
  14         25  
  14         500  
4 14     14   124 use warnings;
  14         25  
  14         691  
5 14     14   66 use Carp;
  14         19  
  14         1024  
6              
7             our $VERSION = '0.09';
8              
9 14     14   7212 use SQL::Wizard::Renderer;
  14         47  
  14         729  
10 14     14   6095 use SQL::Wizard::Expr::Column;
  14         35  
  14         480  
11 14     14   79 use SQL::Wizard::Expr::Value;
  14         20  
  14         238  
12 14     14   4896 use SQL::Wizard::Expr::Raw;
  14         60  
  14         382  
13 14     14   5214 use SQL::Wizard::Expr::Func;
  14         37  
  14         388  
14 14     14   5781 use SQL::Wizard::Expr::Case;
  14         35  
  14         390  
15 14     14   5425 use SQL::Wizard::Expr::Join;
  14         33  
  14         390  
16 14     14   5686 use SQL::Wizard::Expr::Select;
  14         41  
  14         461  
17 14     14   5434 use SQL::Wizard::Expr::Insert;
  14         41  
  14         533  
18 14     14   5126 use SQL::Wizard::Expr::Update;
  14         35  
  14         386  
19 14     14   5526 use SQL::Wizard::Expr::Delete;
  14         46  
  14         420  
20 14     14   5189 use SQL::Wizard::Expr::CTE;
  14         40  
  14         35634  
21              
22             sub new {
23 14     14 1 2088091 my ($class, %args) = @_;
24             my $self = bless {
25             dialect => $args{dialect} || 'ansi',
26 14   50     255 renderer => SQL::Wizard::Renderer->new(dialect => $args{dialect} || 'ansi'),
      50        
27             }, $class;
28 14         63 $self;
29             }
30              
31             ## Expression primitives
32              
33             sub col {
34 42     42 1 20724 my ($self, $name) = @_;
35             SQL::Wizard::Expr::Column->new(
36             name => $name,
37             _renderer => $self->{renderer},
38 42         220 );
39             }
40              
41             sub val {
42 29     29 1 8071 my ($self, $value) = @_;
43             SQL::Wizard::Expr::Value->new(
44             value => $value,
45             _renderer => $self->{renderer},
46 29         123 );
47             }
48              
49             sub raw {
50 5     5 1 6158 my ($self, $sql, @bind) = @_;
51             SQL::Wizard::Expr::Raw->new(
52             sql => $sql,
53             bind => \@bind,
54             _renderer => $self->{renderer},
55 5         35 );
56             }
57              
58             sub func {
59 34     34 1 26133 my ($self, $name, @args) = @_;
60 34 50       220 confess "func name must be a word (\\w+), got '$name'" unless $name =~ /^\w+$/;
61             # Coerce plain strings/values: strings in func args are column refs
62             my @coerced = map {
63 34         71 ref $_ ? $_ : SQL::Wizard::Expr::Column->new(
64             name => $_,
65             _renderer => $self->{renderer},
66             )
67 31 100       190 } @args;
68             SQL::Wizard::Expr::Func->new(
69             name => $name,
70             args => \@coerced,
71             _renderer => $self->{renderer},
72 34         184 );
73             }
74              
75             ## Query builders
76              
77             sub select {
78 125     125 0 64836 my ($self, %args) = @_;
79 125         568 SQL::Wizard::Expr::Select->from_args(%args, _renderer => $self->{renderer});
80             }
81              
82             sub insert {
83 7     7 0 1879 my ($self, %args) = @_;
84 7 50       16 confess "insert requires -into" unless $args{'-into'};
85 7         9 my %node;
86 7 50       19 $node{into} = $args{'-into'} if $args{'-into'};
87 7 100       17 $node{values} = $args{'-values'} if $args{'-values'};
88 7 100       14 $node{columns} = $args{'-columns'} if $args{'-columns'};
89 7 100       41 $node{select} = $args{'-select'} if $args{'-select'};
90 7 100       10 $node{on_conflict} = $args{'-on_conflict'} if $args{'-on_conflict'};
91 7 100       11 $node{on_duplicate} = $args{'-on_duplicate'} if $args{'-on_duplicate'};
92 7 100       12 $node{returning} = $args{'-returning'} if $args{'-returning'};
93             # Coerce hash values to Value nodes for bind params
94 7 100       16 if (ref $node{values} eq 'HASH') {
    100          
95 5         7 for my $k (keys %{$node{values}}) {
  5         12  
96 9         11 my $v = $node{values}{$k};
97 9 100       13 next if ref $v;
98             $node{values}{$k} = SQL::Wizard::Expr::Value->new(
99             value => $v,
100             _renderer => $self->{renderer},
101 8         23 );
102             }
103             } elsif (ref $node{values} eq 'ARRAY') {
104             # Multi-row: coerce each cell
105 1         32 for my $row (@{$node{values}}) {
  1         3  
106 2         4 for my $i (0 .. $#$row) {
107 4 50       7 next if ref $row->[$i];
108             $row->[$i] = SQL::Wizard::Expr::Value->new(
109             value => $row->[$i],
110             _renderer => $self->{renderer},
111 4         7 );
112             }
113             }
114             }
115             SQL::Wizard::Expr::Insert->new(
116             %node,
117             _renderer => $self->{renderer},
118 7         23 );
119             }
120              
121             sub update {
122 6     6 1 5816 my ($self, %args) = @_;
123 6 50       14 confess "update requires -table" unless $args{'-table'};
124 6 50       11 confess "update requires -set" unless $args{'-set'};
125 6         6 my %node;
126 6 50       15 $node{table} = $args{'-table'} if $args{'-table'};
127 6 50       14 $node{set} = $args{'-set'} if $args{'-set'};
128 6 100       11 $node{where} = $args{'-where'} if $args{'-where'};
129 6 100       11 $node{from} = $args{'-from'} if $args{'-from'};
130 6 100       8 $node{limit} = $args{'-limit'} if defined $args{'-limit'};
131 6 100       10 $node{returning} = $args{'-returning'} if $args{'-returning'};
132             # Coerce set values
133 6 50       37 if (ref $node{set} eq 'HASH') {
134 6         7 for my $k (keys %{$node{set}}) {
  6         14  
135 7         10 my $v = $node{set}{$k};
136 7 100       14 next if ref $v;
137             $node{set}{$k} = SQL::Wizard::Expr::Value->new(
138             value => $v,
139             _renderer => $self->{renderer},
140 4         45 );
141             }
142             }
143             SQL::Wizard::Expr::Update->new(
144             %node,
145             _renderer => $self->{renderer},
146 6         24 );
147             }
148              
149             sub delete {
150 6     6 1 3778 my ($self, %args) = @_;
151 6 50       13 confess "delete requires -from" unless $args{'-from'};
152 6         9 my %node;
153 6 50       13 $node{from} = $args{'-from'} if $args{'-from'};
154 6 100       13 $node{where} = $args{'-where'} if $args{'-where'};
155 6 100       10 $node{using} = $args{'-using'} if $args{'-using'};
156 6 100       10 $node{returning} = $args{'-returning'} if $args{'-returning'};
157             SQL::Wizard::Expr::Delete->new(
158             %node,
159             _renderer => $self->{renderer},
160 6         27 );
161             }
162              
163             sub truncate {
164 0     0 0 0 my ($self, %args) = @_;
165 0 0       0 confess "truncate requires -table" unless $args{'-table'};
166             SQL::Wizard::Expr::Raw->new(
167             sql => '',
168             bind => [],
169             _truncate => $args{'-table'},
170             _renderer => $self->{renderer},
171 0         0 );
172             }
173              
174             ## Join helpers
175              
176             sub join {
177 7     7 0 3573 my ($self, $table, $on) = @_;
178             SQL::Wizard::Expr::Join->new(
179             type => 'JOIN',
180             table => $table,
181             on => $on,
182             _renderer => $self->{renderer},
183 7         50 );
184             }
185              
186             sub left_join {
187 3     3 0 806 my ($self, $table, $on) = @_;
188             SQL::Wizard::Expr::Join->new(
189             type => 'LEFT JOIN',
190             table => $table,
191             on => $on,
192             _renderer => $self->{renderer},
193 3         10 );
194             }
195              
196             sub right_join {
197 1     1 0 651 my ($self, $table, $on) = @_;
198             SQL::Wizard::Expr::Join->new(
199             type => 'RIGHT JOIN',
200             table => $table,
201             on => $on,
202             _renderer => $self->{renderer},
203 1         5 );
204             }
205              
206             sub full_join {
207 1     1 0 569 my ($self, $table, $on) = @_;
208             SQL::Wizard::Expr::Join->new(
209             type => 'FULL OUTER JOIN',
210             table => $table,
211             on => $on,
212             _renderer => $self->{renderer},
213 1         4 );
214             }
215              
216             sub cross_join {
217 1     1 0 548 my ($self, $table) = @_;
218             SQL::Wizard::Expr::Join->new(
219             type => 'CROSS JOIN',
220             table => $table,
221             _renderer => $self->{renderer},
222 1         4 );
223             }
224              
225             ## CASE expressions
226              
227             sub case {
228 5     5 0 12 my ($self, @args) = @_;
229 5         8 my ($whens, $else) = $self->_parse_case_args(@args);
230             SQL::Wizard::Expr::Case->new(
231             whens => $whens,
232             ($else ? (else => $else) : ()),
233             _renderer => $self->{renderer},
234 5 100       49 );
235             }
236              
237             sub case_on {
238 1     1 0 3 my ($self, $operand, @args) = @_;
239 1         2 my ($whens, $else) = $self->_parse_case_args(@args);
240             SQL::Wizard::Expr::Case->new(
241             operand => $operand,
242             whens => $whens,
243             ($else ? (else => $else) : ()),
244             _renderer => $self->{renderer},
245 1 50       4 );
246             }
247              
248             sub _parse_case_args {
249 6     6   10 my ($self, @args) = @_;
250 6         7 my @whens;
251             my $else;
252 6         12 for my $arg (@args) {
253 14 100 33     37 if (ref $arg eq 'ARRAY') {
    50          
254             # [$q->when(...)] — a when clause
255 9         13 push @whens, @$arg;
256             } elsif (ref $arg eq 'HASH' && exists $arg->{_else}) {
257 5         7 $else = $arg->{_else};
258             }
259             }
260 6         14 return (\@whens, $else);
261             }
262              
263             sub when {
264 9     9 1 3620 my ($self, $condition, $then) = @_;
265             # Coerce then value
266 9 100       29 $then = $self->val($then) unless ref $then;
267 9         58 return { condition => $condition, then => $then };
268             }
269              
270             sub else {
271 5     5 1 10 my ($self, $value) = @_;
272 5 100       15 $value = $self->val($value) unless ref $value;
273 5         16 return { _else => $value };
274             }
275              
276             ## Condition helpers
277              
278             sub exists {
279 2     2 1 30 my ($self, $subquery) = @_;
280             SQL::Wizard::Expr::Raw->new(
281             sql => 'EXISTS',
282             bind => [],
283             _subquery => $subquery,
284             _renderer => $self->{renderer},
285 2         18 );
286             }
287              
288             sub not_exists {
289 1     1 1 8 my ($self, $subquery) = @_;
290             SQL::Wizard::Expr::Raw->new(
291             sql => 'NOT EXISTS',
292             bind => [],
293             _subquery => $subquery,
294             _renderer => $self->{renderer},
295 1         8 );
296             }
297              
298             sub any {
299 2     2 1 15 my ($self, $subquery) = @_;
300             SQL::Wizard::Expr::Raw->new(
301             sql => 'ANY',
302             bind => [],
303             _subquery => $subquery,
304             _renderer => $self->{renderer},
305 2         13 );
306             }
307              
308             sub all {
309 2     2 1 16 my ($self, $subquery) = @_;
310             SQL::Wizard::Expr::Raw->new(
311             sql => 'ALL',
312             bind => [],
313             _subquery => $subquery,
314             _renderer => $self->{renderer},
315 2         18 );
316             }
317              
318             sub between {
319 1     1 1 816 my ($self, $col, $lo, $hi) = @_;
320 1 50       9 $col = $self->col($col) unless ref $col;
321 1 50       8 $lo = $self->val($lo) unless ref $lo;
322 1 50       5 $hi = $self->val($hi) unless ref $hi;
323             SQL::Wizard::Expr::Raw->new(
324             sql => 'BETWEEN',
325             bind => [],
326             _between => { col => $col, lo => $lo, hi => $hi },
327             _renderer => $self->{renderer},
328 1         37 );
329             }
330              
331             sub not_between {
332 1     1 1 1679 my ($self, $col, $lo, $hi) = @_;
333 1 50       8 $col = $self->col($col) unless ref $col;
334 1 50       7 $lo = $self->val($lo) unless ref $lo;
335 1 50       4 $hi = $self->val($hi) unless ref $hi;
336             SQL::Wizard::Expr::Raw->new(
337             sql => 'NOT BETWEEN',
338             bind => [],
339             _not_between => { col => $col, lo => $lo, hi => $hi },
340             _renderer => $self->{renderer},
341 1         11 );
342             }
343              
344             sub compare {
345 4     4 1 2125 my ($self, $left, $op, $right) = @_;
346 4 100 66     23 $left = $self->col($left) if (!ref $left && $left =~ /^[\w.]+$/);
347 4 50       8 $left = $self->val($left) if (!ref $left);
348 4 100       9 $right = $self->val($right) if (!ref $right);
349             SQL::Wizard::Expr::Raw->new(
350             sql => '',
351             bind => [],
352             _compare => { left => $left, op => $op, right => $right },
353             _renderer => $self->{renderer},
354 4         21 );
355             }
356              
357             ## Function shortcuts
358              
359             sub cast {
360 1     1 1 1648 my ($self, $expr, $type) = @_;
361 1 50       8 $expr = $self->col($expr) unless ref $expr;
362             SQL::Wizard::Expr::Raw->new(
363             sql => "CAST",
364             bind => [],
365             _cast => { expr => $expr, type => $type },
366             _renderer => $self->{renderer},
367 1         10 );
368             }
369              
370 1     1 1 3 sub coalesce { my $self = shift; $self->func('COALESCE', @_) }
  1         6  
371 1     1 1 1729 sub greatest { my $self = shift; $self->func('GREATEST', @_) }
  1         6  
372 1     1 1 940 sub least { my $self = shift; $self->func('LEAST', @_) }
  1         5  
373 2     2 1 586 sub now { my $self = shift; $self->func('NOW') }
  2         8  
374              
375             ## Boolean operators
376              
377             sub and {
378 1     1 1 1715 my ($self, @conds) = @_;
379             SQL::Wizard::Expr::Raw->new(
380             sql => 'AND',
381             bind => [],
382             _logic => { op => 'AND', conds => \@conds },
383             _renderer => $self->{renderer},
384 1         11 );
385             }
386              
387             sub or {
388 1     1 1 1735 my ($self, @conds) = @_;
389             SQL::Wizard::Expr::Raw->new(
390             sql => 'OR',
391             bind => [],
392             _logic => { op => 'OR', conds => \@conds },
393             _renderer => $self->{renderer},
394 1         10 );
395             }
396              
397             sub not {
398 1     1 1 1674 my ($self, $cond) = @_;
399             SQL::Wizard::Expr::Raw->new(
400             sql => 'NOT',
401             bind => [],
402             _not => $cond,
403             _renderer => $self->{renderer},
404 1         9 );
405             }
406              
407             ## CTEs
408              
409             sub with {
410 3     3 1 11 my ($self, @args) = @_;
411 3         6 my @ctes;
412 3         9 while (@args) {
413 4         7 my $name = shift @args;
414 4         7 my $query = shift @args;
415 4         17 push @ctes, { name => $name, query => $query };
416             }
417             SQL::Wizard::Expr::CTE->new(
418             ctes => \@ctes,
419             _renderer => $self->{renderer},
420 3         19 );
421             }
422              
423             sub with_recursive {
424 1     1 1 5 my ($self, @args) = @_;
425 1         3 my @ctes;
426 1         6 while (@args) {
427 1         3 my $name = shift @args;
428 1         2 my $query = shift @args;
429 1         7 push @ctes, { name => $name, query => $query };
430             }
431             SQL::Wizard::Expr::CTE->new(
432             ctes => \@ctes,
433             recursive => 1,
434             _renderer => $self->{renderer},
435 1         7 );
436             }
437              
438             1;