File Coverage

blib/lib/SQL/Wizard.pm
Criterion Covered Total %
statement 186 189 98.4
branch 64 86 74.4
condition 3 7 42.8
subroutine 50 51 98.0
pod 25 35 71.4
total 328 368 89.1


line stmt bran cond sub pod time code
1             package SQL::Wizard;
2              
3 14     14   1650908 use strict;
  14         34  
  14         526  
4 14     14   135 use warnings;
  14         31  
  14         783  
5 14     14   95 use Carp;
  14         42  
  14         1255  
6              
7             our $VERSION = '0.08';
8              
9 14     14   8323 use SQL::Wizard::Renderer;
  14         55  
  14         806  
10 14     14   7480 use SQL::Wizard::Expr::Column;
  14         44  
  14         499  
11 14     14   87 use SQL::Wizard::Expr::Value;
  14         23  
  14         268  
12 14     14   5920 use SQL::Wizard::Expr::Raw;
  14         35  
  14         419  
13 14     14   6011 use SQL::Wizard::Expr::Func;
  14         37  
  14         422  
14 14     14   6295 use SQL::Wizard::Expr::Case;
  14         46  
  14         521  
15 14     14   5933 use SQL::Wizard::Expr::Join;
  14         42  
  14         445  
16 14     14   6566 use SQL::Wizard::Expr::Select;
  14         66  
  14         524  
17 14     14   6386 use SQL::Wizard::Expr::Insert;
  14         40  
  14         405  
18 14     14   6067 use SQL::Wizard::Expr::Update;
  14         40  
  14         433  
19 14     14   6623 use SQL::Wizard::Expr::Delete;
  14         45  
  14         530  
20 14     14   7486 use SQL::Wizard::Expr::CTE;
  14         53  
  14         40600  
21              
22             sub new {
23 14     14 1 2572797 my ($class, %args) = @_;
24             my $self = bless {
25             dialect => $args{dialect} || 'ansi',
26 14   50     368 renderer => SQL::Wizard::Renderer->new(dialect => $args{dialect} || 'ansi'),
      50        
27             }, $class;
28 14         60 $self;
29             }
30              
31             ## Expression primitives
32              
33             sub col {
34 37     37 1 21773 my ($self, $name) = @_;
35             SQL::Wizard::Expr::Column->new(
36             name => $name,
37             _renderer => $self->{renderer},
38 37         245 );
39             }
40              
41             sub val {
42 26     26 1 5004 my ($self, $value) = @_;
43             SQL::Wizard::Expr::Value->new(
44             value => $value,
45             _renderer => $self->{renderer},
46 26         128 );
47             }
48              
49             sub raw {
50 5     5 1 6051 my ($self, $sql, @bind) = @_;
51             SQL::Wizard::Expr::Raw->new(
52             sql => $sql,
53             bind => \@bind,
54             _renderer => $self->{renderer},
55 5         39 );
56             }
57              
58             sub func {
59 31     31 1 25967 my ($self, $name, @args) = @_;
60 31 50       267 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 31         76 ref $_ ? $_ : SQL::Wizard::Expr::Column->new(
64             name => $_,
65             _renderer => $self->{renderer},
66             )
67 28 100       199 } @args;
68             SQL::Wizard::Expr::Func->new(
69             name => $name,
70             args => \@coerced,
71             _renderer => $self->{renderer},
72 31         207 );
73             }
74              
75             ## Query builders
76              
77             sub select {
78 119     119 0 91044 my ($self, %args) = @_;
79 119         715 SQL::Wizard::Expr::Select->from_args(%args, _renderer => $self->{renderer});
80             }
81              
82             sub insert {
83 7     7 0 2724 my ($self, %args) = @_;
84 7 50       24 confess "insert requires -into" unless $args{'-into'};
85 7         15 my %node;
86 7 50       27 $node{into} = $args{'-into'} if $args{'-into'};
87 7 100       27 $node{values} = $args{'-values'} if $args{'-values'};
88 7 100       19 $node{columns} = $args{'-columns'} if $args{'-columns'};
89 7 100       58 $node{select} = $args{'-select'} if $args{'-select'};
90 7 100       19 $node{on_conflict} = $args{'-on_conflict'} if $args{'-on_conflict'};
91 7 100       19 $node{on_duplicate} = $args{'-on_duplicate'} if $args{'-on_duplicate'};
92 7 100       19 $node{returning} = $args{'-returning'} if $args{'-returning'};
93             # Coerce hash values to Value nodes for bind params
94 7 100       27 if (ref $node{values} eq 'HASH') {
    100          
95 5         35 for my $k (keys %{$node{values}}) {
  5         20  
96 9         19 my $v = $node{values}{$k};
97 9 100       22 next if ref $v;
98             $node{values}{$k} = SQL::Wizard::Expr::Value->new(
99             value => $v,
100             _renderer => $self->{renderer},
101 8         40 );
102             }
103             } elsif (ref $node{values} eq 'ARRAY') {
104             # Multi-row: coerce each cell
105 1         4 for my $row (@{$node{values}}) {
  1         4  
106 2         6 for my $i (0 .. $#$row) {
107 4 50       11 next if ref $row->[$i];
108             $row->[$i] = SQL::Wizard::Expr::Value->new(
109             value => $row->[$i],
110             _renderer => $self->{renderer},
111 4         13 );
112             }
113             }
114             }
115             SQL::Wizard::Expr::Insert->new(
116             %node,
117             _renderer => $self->{renderer},
118 7         40 );
119             }
120              
121             sub update {
122 6     6 1 5874 my ($self, %args) = @_;
123 6 50       25 confess "update requires -table" unless $args{'-table'};
124 6 50       45 confess "update requires -set" unless $args{'-set'};
125 6         12 my %node;
126 6 50       39 $node{table} = $args{'-table'} if $args{'-table'};
127 6 50       23 $node{set} = $args{'-set'} if $args{'-set'};
128 6 100       17 $node{where} = $args{'-where'} if $args{'-where'};
129 6 100       18 $node{from} = $args{'-from'} if $args{'-from'};
130 6 100       18 $node{limit} = $args{'-limit'} if defined $args{'-limit'};
131 6 100       17 $node{returning} = $args{'-returning'} if $args{'-returning'};
132             # Coerce set values
133 6 50       22 if (ref $node{set} eq 'HASH') {
134 6         8 for my $k (keys %{$node{set}}) {
  6         26  
135 7         15 my $v = $node{set}{$k};
136 7 100       21 next if ref $v;
137             $node{set}{$k} = SQL::Wizard::Expr::Value->new(
138             value => $v,
139             _renderer => $self->{renderer},
140 4         25 );
141             }
142             }
143             SQL::Wizard::Expr::Update->new(
144             %node,
145             _renderer => $self->{renderer},
146 6         35 );
147             }
148              
149             sub delete {
150 6     6 1 5146 my ($self, %args) = @_;
151 6 50       16 confess "delete requires -from" unless $args{'-from'};
152 6         7 my %node;
153 6 50       18 $node{from} = $args{'-from'} if $args{'-from'};
154 6 100       11 $node{where} = $args{'-where'} if $args{'-where'};
155 6 100       9 $node{using} = $args{'-using'} if $args{'-using'};
156 6 100       11 $node{returning} = $args{'-returning'} if $args{'-returning'};
157             SQL::Wizard::Expr::Delete->new(
158             %node,
159             _renderer => $self->{renderer},
160 6         26 );
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 7811 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         54 );
184             }
185              
186             sub left_join {
187 3     3 0 825 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         11 );
194             }
195              
196             sub right_join {
197 1     1 0 549 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         4 );
204             }
205              
206             sub full_join {
207 1     1 0 601 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         5 );
214             }
215              
216             sub cross_join {
217 1     1 0 607 my ($self, $table) = @_;
218             SQL::Wizard::Expr::Join->new(
219             type => 'CROSS JOIN',
220             table => $table,
221             _renderer => $self->{renderer},
222 1         5 );
223             }
224              
225             ## CASE expressions
226              
227             sub case {
228 5     5 0 10 my ($self, @args) = @_;
229 5         13 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 5 my ($self, $operand, @args) = @_;
239 1         4 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       8 );
246             }
247              
248             sub _parse_case_args {
249 6     6   10 my ($self, @args) = @_;
250 6         9 my @whens;
251             my $else;
252 6         12 for my $arg (@args) {
253 14 100 33     51 if (ref $arg eq 'ARRAY') {
    50          
254             # [$q->when(...)] — a when clause
255 9         18 push @whens, @$arg;
256             } elsif (ref $arg eq 'HASH' && exists $arg->{_else}) {
257 5         10 $else = $arg->{_else};
258             }
259             }
260 6         20 return (\@whens, $else);
261             }
262              
263             sub when {
264 9     9 1 7412 my ($self, $condition, $then) = @_;
265             # Coerce then value
266 9 100       31 $then = $self->val($then) unless ref $then;
267 9         55 return { condition => $condition, then => $then };
268             }
269              
270             sub else {
271 5     5 1 11 my ($self, $value) = @_;
272 5 100       17 $value = $self->val($value) unless ref $value;
273 5         23 return { _else => $value };
274             }
275              
276             ## Condition helpers
277              
278             sub exists {
279 2     2 1 19 my ($self, $subquery) = @_;
280             SQL::Wizard::Expr::Raw->new(
281             sql => 'EXISTS',
282             bind => [],
283             _subquery => $subquery,
284             _renderer => $self->{renderer},
285 2         19 );
286             }
287              
288             sub not_exists {
289 1     1 1 7 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 13 my ($self, $subquery) = @_;
300             SQL::Wizard::Expr::Raw->new(
301             sql => 'ANY',
302             bind => [],
303             _subquery => $subquery,
304             _renderer => $self->{renderer},
305 2         8 );
306             }
307              
308             sub all {
309 2     2 1 13 my ($self, $subquery) = @_;
310             SQL::Wizard::Expr::Raw->new(
311             sql => 'ALL',
312             bind => [],
313             _subquery => $subquery,
314             _renderer => $self->{renderer},
315 2         8 );
316             }
317              
318             sub between {
319 1     1 1 945 my ($self, $col, $lo, $hi) = @_;
320 1 50       29 $col = $self->col($col) unless ref $col;
321 1 50       10 $lo = $self->val($lo) unless ref $lo;
322 1 50       9 $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         12 );
329             }
330              
331             sub not_between {
332 1     1 1 1927 my ($self, $col, $lo, $hi) = @_;
333 1 50       9 $col = $self->col($col) unless ref $col;
334 1 50       9 $lo = $self->val($lo) unless ref $lo;
335 1 50       6 $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         13 );
342             }
343              
344             ## Function shortcuts
345              
346             sub cast {
347 1     1 1 1975 my ($self, $expr, $type) = @_;
348 1 50       9 $expr = $self->col($expr) unless ref $expr;
349             SQL::Wizard::Expr::Raw->new(
350             sql => "CAST",
351             bind => [],
352             _cast => { expr => $expr, type => $type },
353             _renderer => $self->{renderer},
354 1         10 );
355             }
356              
357 1     1 1 2 sub coalesce { my $self = shift; $self->func('COALESCE', @_) }
  1         4  
358 1     1 1 1138 sub greatest { my $self = shift; $self->func('GREATEST', @_) }
  1         4  
359 1     1 1 670 sub least { my $self = shift; $self->func('LEAST', @_) }
  1         5  
360 2     2 1 785 sub now { my $self = shift; $self->func('NOW') }
  2         10  
361              
362             ## Boolean operators
363              
364             sub and {
365 1     1 1 1936 my ($self, @conds) = @_;
366             SQL::Wizard::Expr::Raw->new(
367             sql => 'AND',
368             bind => [],
369             _logic => { op => 'AND', conds => \@conds },
370             _renderer => $self->{renderer},
371 1         15 );
372             }
373              
374             sub or {
375 1     1 1 1919 my ($self, @conds) = @_;
376             SQL::Wizard::Expr::Raw->new(
377             sql => 'OR',
378             bind => [],
379             _logic => { op => 'OR', conds => \@conds },
380             _renderer => $self->{renderer},
381 1         14 );
382             }
383              
384             sub not {
385 1     1 1 1925 my ($self, $cond) = @_;
386             SQL::Wizard::Expr::Raw->new(
387             sql => 'NOT',
388             bind => [],
389             _not => $cond,
390             _renderer => $self->{renderer},
391 1         10 );
392             }
393              
394             ## CTEs
395              
396             sub with {
397 3     3 1 8 my ($self, @args) = @_;
398 3         3 my @ctes;
399 3         7 while (@args) {
400 4         4 my $name = shift @args;
401 4         5 my $query = shift @args;
402 4         15 push @ctes, { name => $name, query => $query };
403             }
404             SQL::Wizard::Expr::CTE->new(
405             ctes => \@ctes,
406             _renderer => $self->{renderer},
407 3         13 );
408             }
409              
410             sub with_recursive {
411 1     1 1 5 my ($self, @args) = @_;
412 1         4 my @ctes;
413 1         5 while (@args) {
414 1         2 my $name = shift @args;
415 1         4 my $query = shift @args;
416 1         7 push @ctes, { name => $name, query => $query };
417             }
418             SQL::Wizard::Expr::CTE->new(
419             ctes => \@ctes,
420             recursive => 1,
421             _renderer => $self->{renderer},
422 1         7 );
423             }
424              
425             1;