File Coverage

blib/lib/SQL/Maker/Select.pm
Criterion Covered Total %
statement 205 210 97.6
branch 87 98 88.7
condition 29 49 59.1
subroutine 33 33 100.0
pod 14 23 60.8
total 368 413 89.1


line stmt bran cond sub pod time code
1             use strict;
2 19     19   1527 use warnings;
  19         52  
  19         562  
3 19     19   99 use utf8;
  19         40  
  19         527  
4 19     19   9016 use SQL::Maker::Condition;
  19         229  
  19         100  
5 19     19   8783 use SQL::Maker::Util;
  19         54  
  19         600  
6 19     19   123 use Class::Accessor::Lite (
  19         41  
  19         867  
7             new => 0,
8 19         154 wo => [qw/distinct for_update/],
9             rw => [qw/prefix/],
10             ro => [qw/quote_char name_sep new_line strict/],
11             );
12 19     19   1451 use Scalar::Util ();
  19         3709  
13 19     19   2793  
  19         47  
  19         51268  
14             if (@_==1) {
15             return $_[0]->{offset};
16 7 50   7 1 24 } else {
17 0         0 $_[0]->{offset} = $_[1];
18             return $_[0];
19 7         18 }
20 7         17 }
21              
22             if (@_==1) {
23             $_[0]->{limit};
24             } else {
25 22 50   22 1 71 $_[0]->{limit} = $_[1];
26 0         0 return $_[0];
27             }
28 22         66 }
29 22         52  
30             my $class = shift;
31             my %args = @_ == 1 ? %{$_[0]} : @_;
32             my $self = bless {
33             select => +[],
34 209     209 0 413455 distinct => 0,
35 209 50       853 select_map => +{},
  0         0  
36 209         1867 select_map_reverse => +{},
37             from => +[],
38             joins => +[],
39             index_hint => +{},
40             group_by => +[],
41             order_by => +[],
42             prefix => 'SELECT ',
43             new_line => "\n",
44             strict => 0,
45             %args
46             }, $class;
47              
48             return $self;
49             }
50              
51             my $self = shift;
52 209         944  
53             SQL::Maker::Condition->new(
54             quote_char => $self->{quote_char},
55             name_sep => $self->{name_sep},
56 42     42 0 1463 strict => $self->{strict},
57             );
58             }
59              
60             my $self = shift;
61             my @bind;
62 42         244 push @bind, @{$self->{subqueries}} if $self->{subqueries};
63             push @bind, $self->{where}->bind if $self->{where};
64             push @bind, $self->{having}->bind if $self->{having};
65             return wantarray ? @bind : \@bind;
66 194     194 1 9033 }
67 194         269  
68 194 100       506 my ($self, $term, $col) = @_;
  15         33  
69 194 100       676  
70 194 100       443 push @{ $self->{select} }, $term;
71 194 100       1157 if ($col) {
72             $col ||= $term;
73             $self->{select_map_reverse}->{$col} = $term;
74             }
75 209     209 1 608 return $self;
76             }
77 209         288  
  209         486  
78 209 100       455 my ($self, $table, $alias) = @_;
79 38   33     122 if ( Scalar::Util::blessed( $table ) and $table->can('as_sql') ) {
80 38         96 push @{ $self->{subqueries} }, $table->bind;
81             push @{$self->{from}}, [ \do{ '(' . $table->as_sql . ')' }, $alias ];
82 209         572 }
83             else {
84             push @{$self->{from}}, [$table, $alias];
85             }
86 177     177 1 539 return $self;
87 177 100 66     643 }
88 6         14  
  6         21  
89 6         11 my ($self, $table_ref, $joins) = @_;
  6         10  
  6         16  
90             my ($table, $alias) = ref($table_ref) eq 'ARRAY' ? @$table_ref : ($table_ref);
91              
92 171         235 if ( Scalar::Util::blessed( $table ) and $table->can('as_sql') ) {
  171         484  
93             push @{ $self->{subqueries} }, $table->bind;
94 177         450 $table = \do{ '(' . $table->as_sql . ')' };
95             }
96              
97             if ( Scalar::Util::blessed( $joins->{table} ) and $joins->{table}->can('as_sql') ) {
98 44     44 1 365 push @{ $self->{subqueries} }, $joins->{table}->bind;
99 44 100       129 $joins->{table} = \do{ '(' . $joins->{table}->as_sql . ')' };
100             }
101 44 100 66     186  
102 7         18 push @{ $self->{joins} }, {
  7         25  
103 7         14 table => [ $table, $alias ],
  7         18  
104             joins => $joins,
105             };
106 44 100 66     163 return $self;
107 2         5 }
  2         9  
108 2         5  
  2         8  
109             my ($self, $table, $hint) = @_;
110              
111 44         71 my ($type, $list);
  44         167  
112              
113             if (ref $hint eq 'HASH') {
114             # { type => '...', list => ['foo'] }
115 44         172 $type = $hint->{type} || 'USE';
116             $list = ref($hint->{list}) eq 'ARRAY' ? $hint->{list} : [ $hint->{list} ];
117             } else {
118             # ['foo, 'bar'] or just 'foo'
119 14     14 1 77 $type = 'USE';
120             $list = ref($hint) eq 'ARRAY' ? $hint : [ $hint ];
121 14         22 }
122              
123 14 100       50 $self->{index_hint}->{$table} = {
124             type => $type,
125 8   50     30 list => $list,
126 8 50       26 };
127              
128             return $self;
129 6         15 }
130 6 100       21  
131             my ($self, $label) = @_;
132              
133 14         49 return $$label if ref $label eq 'SCALAR';
134             SQL::Maker::Util::quote_identifier($label, $self->{quote_char}, $self->{name_sep})
135             }
136              
137             my $self = shift;
138 14         43 my $sql = '';
139             my $new_line = $self->new_line;
140            
141             if (@{ $self->{select} }) {
142 774     774   1267 $self->{select_map} = { reverse %{$self->{select_map_reverse}} };
143             $sql .= $self->{prefix};
144 774 100       1659 $sql .= 'DISTINCT ' if $self->{distinct};
145             $sql .= join(', ', map {
146 724         1750 my $alias = $self->{select_map}->{$_};
147             if (!$alias) {
148             $self->_quote($_)
149 267     267 1 764 } elsif ($alias && $_ =~ /(?:^|\.)\Q$alias\E$/) {
150 267         432 $self->_quote($_)
151 267         668 } else {
152             $self->_quote($_) . ' AS ' . $self->_quote($alias)
153 267 100       1587 }
  267         652  
154 216         308 } @{ $self->{select} }) . $new_line;
  216         640  
155 216         503 }
156 216 100       451  
157             $sql .= 'FROM ';
158 280         517  
159 280 100 66     1213 ## Add any explicit JOIN statements before the non-joined tables.
    100          
160 233         454 if ($self->{joins} && @{ $self->{joins} }) {
161             my $initial_table_written = 0;
162 30         86 for my $j (@{ $self->{joins} }) {
163             my ($table, $join) = map { $j->{$_} } qw( table joins );
164 17         53 $table = $self->_add_index_hint(@$table); ## index hint handling
165             $sql .= $table unless $initial_table_written++;
166 216         337 $sql .= ' ' . uc($join->{type}) if $join->{type};
  216         438  
167             $sql .= ' JOIN ' . $self->_quote($join->{table});
168             $sql .= ' ' . $self->_quote($join->{alias}) if $join->{alias};
169 267         544  
170             if ( defined $join->{condition} ) {
171             if (ref $join->{condition} && ref $join->{condition} eq 'ARRAY') {
172 267 100 66     678 $sql .= ' USING ('. join(', ', map { $self->_quote($_) } @{ $join->{condition} }) . ')';
  267         858  
173 37         73 }
174 37         58 elsif (ref $join->{condition} && ref $join->{condition} eq 'HASH') {
  37         90  
175 46         82 my @conds;
  92         208  
176 46         234 for my $key (keys %{ $join->{condition} }) {
177 46 100       421 push @conds, $self->_quote($key) . ' = ' . $self->_quote($join->{condition}{$key});
178 46 100       172 }
179 46         118 $sql .= ' ON ' . join(' AND ', @conds);
180 46 100       173 }
181             else {
182 46 100       131 $sql .= ' ON ' . $join->{condition};
183 44 100 100     271 }
    100 66        
184 3         8 }
  5         11  
  3         8  
185             }
186             $sql .= ', ' if @{ $self->{from} };
187 3         9 }
188 3         5  
  3         15  
189 4         33 if ($self->{from} && @{ $self->{from} }) {
190             $sql .= join ', ',
191 3         15 map { $self->_add_index_hint($_->[0], $_->[1]) }
192             @{ $self->{from} };
193             }
194 38         134  
195             $sql .= $new_line;
196             $sql .= $self->as_sql_where() if $self->{where};
197              
198 37 100       67 $sql .= $self->as_sql_group_by if $self->{group_by};
  37         103  
199             $sql .= $self->as_sql_having if $self->{having};
200             $sql .= $self->as_sql_order_by if $self->{order_by};
201 267 100 66     584  
  267         779  
202             $sql .= $self->as_sql_limit if defined $self->{limit};
203 241         573  
204 231         343 $sql .= $self->as_sql_for_update;
  231         437  
205             $sql =~ s/${new_line}+$//;
206              
207 267         491 return $sql;
208 267 100       844 }
209              
210 267 50       1418 my $self = shift;
211 267 100       757  
212 267 50       775 my $n = $self->{limit};
213             return '' unless defined $n;
214 267 100       846  
215             die "Non-numerics in limit clause ($n)" if $n =~ /\D/;
216 262         728 return sprintf "LIMIT %d%s" . $self->new_line, $n,
217 262         2682 ($self->{offset} ? " OFFSET " . int($self->{offset}) : "");
218             }
219 262         1365  
220             my ($self, $col, $type) = @_;
221             push @{$self->{order_by}}, [$col, $type];
222             return $self;
223 23     23 0 42 }
224              
225 23         38 my ($self) = @_;
226 23 50       52  
227             my @attrs = @{$self->{order_by}};
228 23 100       175 return '' unless @attrs;
229              
230 18 100       72 return 'ORDER BY '
231             . join(', ', map {
232             my ($col, $type) = @$_;
233             if (ref $col) {
234 48     48 1 183 $$col
235 48         82 } else {
  48         124  
236 48         107 $type ? $self->_quote($col) . " $type" : $self->_quote($col)
237             }
238             } @attrs)
239             . $self->new_line;
240 267     267 0 450 }
241              
242 267         369 my ($self, $group, $order) = @_;
  267         543  
243 267 100       753 push @{$self->{group_by}}, $order ? $self->_quote($group) . " $order" : $self->_quote($group);
244             return $self;
245             }
246              
247 39         84 my ($self,) = @_;
  48         96  
248 48 100       107  
249 29         120 my $elems = $self->{group_by};
250              
251 19 100       52 return '' if @$elems == 0;
252              
253             return 'GROUP BY '
254             . join(', ', @$elems)
255             . $self->new_line;
256             }
257              
258 36     36 1 130 my ($self, $where) = @_;
259 36 100       53 $self->{where} = $where;
  36         112  
260 36         92 return $self;
261             }
262              
263             my ($self, $col, $val) = @_;
264 267     267 0 454  
265             $self->{where} ||= $self->new_condition();
266 267         470 $self->{where}->add($col, $val);
267             return $self;
268 267 100       698 }
269              
270 26         90 my ($self, $term, $bind) = @_;
271              
272             $self->{where} ||= $self->new_condition();
273             $self->{where}->add_raw($term, $bind);
274             return $self;
275             }
276 84     84 1 216  
277 84         167 my $self = shift;
278 84         186  
279             my $where = $self->{where}->as_sql(undef, sub { $self->_quote($_[0]) });
280             $where ? "WHERE $where" . $self->new_line : '';
281             }
282 29     29 1 106  
283             my $self = shift;
284 29   66     142 if ($self->{having}) {
285 29         113 'HAVING ' . $self->{having}->as_sql . $self->new_line;
286 29         110 } else {
287             ''
288             }
289             }
290 6     6 1 28  
291             my ($self, $col, $val) = @_;
292 6   66     31  
293 6         30 if (my $orig = $self->{select_map_reverse}->{$col}) {
294 6         15 $col = $orig;
295             }
296              
297             $self->{having} ||= $self->new_condition();
298 173     173 0 320 $self->{having}->add($col, $val);
299             return $self;
300 173     3   919 }
  3         61  
301 173 100       870  
302             my ($self, $term, $bind) = @_;
303              
304             $self->{having} ||= $self->new_condition();
305 7     7 0 12 $self->{having}->add_raw($term, $bind);
306 7 50       22 return $self;
307 7         23 }
308              
309 0         0 my $self = shift;
310             $self->{for_update} ? ' FOR UPDATE' : '';
311             }
312              
313             my ($self, $tbl_name, $alias) = @_;
314 3     3 1 15 my $quoted = $alias ? $self->_quote($tbl_name) . ' ' . $self->_quote($alias) : $self->_quote($tbl_name);
315             my $hint = $self->{index_hint}->{$tbl_name};
316 3 50       14 return $quoted unless $hint && ref($hint) eq 'HASH';
317 3         5 if ($hint->{list} && @{ $hint->{list} }) {
318             return $quoted . ' ' . uc($hint->{type} || 'USE') . ' INDEX (' .
319             join (',', map { $self->_quote($_) } @{ $hint->{list} }) .
320 3   33     18 ')';
321 3         25 }
322 3         7 return $quoted;
323             }
324              
325             1;
326 4     4 0 24  
327             =head1 NAME
328 4   33     19  
329 4         15 SQL::Maker::Select - dynamic SQL generator
330 4         7  
331             =head1 SYNOPSIS
332              
333             my $sql = SQL::Maker::Select->new()
334 262     262 0 379 ->add_select('foo')
335 262 50       640 ->add_select('bar')
336             ->add_select('baz')
337             ->add_from('table_name')
338             ->as_sql;
339 287     287   552 # => "SELECT foo, bar, baz FROM table_name"
340 287 100       689  
341 287         597 =head1 DESCRIPTION
342 287 100 66     1112  
343 16 50 33     45 =head1 METHODS
  16         51  
344              
345 16   50     64 =over 4
  17         38  
  16         37  
346              
347             =item C<< my $sql = $stmt->as_sql(); >>
348 0            
349             Render the SQL string.
350              
351             =item C<< my @bind = $stmt->bind(); >>
352              
353             Get the bind variables.
354              
355             =item C<< $stmt->add_select('*') >>
356              
357             =item C<< $stmt->add_select($col => $alias) >>
358              
359             =item C<< $stmt->add_select(\'COUNT(*)' => 'cnt') >>
360              
361             Add a new select term. It's automatically quoted.
362              
363             =item C<< $stmt->add_from($table :Str | $select :SQL::Maker::Select) : SQL::Maker::Select >>
364              
365             Add a new FROM clause. You can specify the table name or an instance of L<SQL::Maker::Select> for a sub-query.
366              
367             I<Return:> $stmt itself.
368              
369             =item C<< $stmt->add_join(user => {type => 'inner', table => 'config', condition => 'user.user_id = config.user_id'}); >>
370              
371             =item C<< $stmt->add_join(user => {type => 'inner', table => 'config', condition => {'user.user_id' => 'config.user_id'}); >>
372              
373             =item C<< $stmt->add_join(user => {type => 'inner', table => 'config', condition => ['user_id']}); >>
374              
375             =item C<< $stmt->add_join(user => {type => 'inner', table => $select :SQL::Maker::Select, condition => 'user.user_id = config.user_id'}); >>
376              
377             Add a new JOIN clause. If you pass an arrayref for 'condition' then it uses 'USING'. If 'type' is omitted
378             it falls back to plain JOIN.
379              
380             You can specify the table name or an instance of L<SQL::Maker::Select> for a sub-query in 'table' argument.
381              
382             my $stmt = SQL::Maker::Select->new();
383             $stmt->add_join(
384             user => {
385             type => 'inner',
386             table => 'config',
387             condition => 'user.user_id = config.user_id',
388             }
389             );
390             $stmt->as_sql();
391             # => 'FROM user INNER JOIN config ON user.user_id = config.user_id'
392              
393             my $stmt = SQL::Maker::Select->new(quote_char => '`', name_sep => '.');
394             $stmt->add_join(
395             user => {
396             type => 'inner',
397             table => 'config',
398             condition => {'user.user_id' => 'config.user_id'},
399             }
400             );
401             $stmt->as_sql();
402             # => 'FROM `user` INNER JOIN `config` ON `user`.`user_id` = `config`.`user_id`'
403              
404             my $stmt = SQL::Maker::Select->new();
405             $stmt->add_select('name');
406             $stmt->add_join(
407             user => {
408             type => 'inner',
409             table => 'config',
410             condition => ['user_id'],
411             }
412             );
413             $stmt->as_sql();
414             # => 'SELECT name FROM user INNER JOIN config USING (user_id)'
415              
416             my $subquery = SQL::Maker::Select->new();
417             $subquery->add_select('*');
418             $subquery->add_from( 'foo' );
419             $subquery->add_where( 'hoge' => 'fuga' );
420             my $stmt = SQL::Maker::Select->new();
421             $stmt->add_join(
422             [ $subquery, 'bar' ] => {
423             type => 'inner',
424             table => 'baz',
425             alias => 'b1',
426             condition => 'bar.baz_id = b1.baz_id'
427             },
428             );
429             $stmt->as_sql;
430             # => "FROM (SELECT * FROM foo WHERE (hoge = ?)) bar INNER JOIN baz b1 ON bar.baz_id = b1.baz_id";
431              
432             my $subquery1 = SQL::Maker::Select->new();
433             $subquery1->add_select('*');
434             $subquery1->add_from( 'foo' );
435             $subquery1->add_where( 'hoge' => 'fuga' );
436             my $subquery2 = SQL::Maker::Select->new();
437             $subquery2->add_select('*');
438             $subquery2->add_from( 'bar' );
439             $subquery2->add_where( 'piyo' => 'gera' );
440             my $stmt = SQL::Maker::Select->new();
441             $stmt->add_join(
442             [ $subquery1, 'f1' ] => {
443             type => 'inner',
444             table => $subquery2,
445             alias => 'b1',
446             condition => 'f1.baz_id = b1.baz_id'
447             },
448             );
449             $stmt->as_sql;
450             # => "FROM (SELECT * FROM foo WHERE (hoge = ?)) f1 INNER JOIN (SELECT * FROM bar WHERE (piyo = ?)) b1 ON f1.baz_id = b1.baz_id";
451              
452             =item C<< $stmt->add_index_hint(foo => {type => 'USE', list => ['index_hint']}); >>
453              
454             =item C<< $stmt->add_index_hint(foo => 'index_hint'); >>
455              
456             =item C<< $stmt->add_index_hint(foo => ['index_hint']); >>
457              
458             my $stmt = SQL::Maker::Select->new();
459             $stmt->add_select('name');
460             $stmt->add_from('user');
461             $stmt->add_index_hint(user => {type => 'USE', list => ['index_hint']});
462             $stmt->as_sql();
463             # => "SELECT name FROM user USE INDEX (index_hint)"
464              
465             =item C<< $stmt->add_where('foo_id' => 'bar'); >>
466              
467             Add a new WHERE clause.
468              
469             my $stmt = SQL::Maker::Select->new()
470             ->add_select('c')
471             ->add_from('foo')
472             ->add_where('name' => 'john')
473             ->add_where('type' => {IN => [qw/1 2 3/]})
474             ->as_sql();
475             # => "SELECT c FROM foo WHERE (name = ?) AND (type IN (?, ?, ?))"
476              
477             =item C<< $stmt->add_where_raw('id = ?', [1]) >>
478              
479             Add a new WHERE clause from raw placeholder string and bind variables.
480              
481             my $stmt = SQL::Maker::Select->new()
482             ->add_select('c')
483             ->add_from('foo')
484             ->add_where_raw('EXISTS(SELECT * FROM bar WHERE name = ?)' => ['john'])
485             ->add_where_raw('type IS NOT NULL')
486             ->as_sql();
487             # => "SELECT c FROM foo WHERE (EXISTS(SELECT * FROM bar WHERE name = ?)) AND (type IS NOT NULL)"
488              
489              
490             =item C<< $stmt->set_where($condition) >>
491              
492             Set the WHERE clause.
493              
494             $condition should be instance of L<SQL::Maker::Condition>.
495              
496             my $cond1 = SQL::Maker::Condition->new()
497             ->add("name" => "john");
498             my $cond2 = SQL::Maker::Condition->new()
499             ->add("type" => {IN => [qw/1 2 3/]});
500             my $stmt = SQL::Maker::Select->new()
501             ->add_select('c')
502             ->add_from('foo')
503             ->set_where($cond1 & $cond2)
504             ->as_sql();
505             # => "SELECT c FROM foo WHERE ((name = ?)) AND ((type IN (?, ?, ?)))"
506              
507             =item C<< $stmt->add_order_by('foo'); >>
508              
509             =item C<< $stmt->add_order_by({'foo' => 'DESC'}); >>
510              
511             Add a new ORDER BY clause.
512              
513             my $stmt = SQL::Maker::Select->new()
514             ->add_select('c')
515             ->add_from('foo')
516             ->add_order_by('name' => 'DESC')
517             ->add_order_by('id')
518             ->as_sql();
519             # => "SELECT c FROM foo ORDER BY name DESC, id"
520              
521             =item C<< $stmt->add_group_by('foo'); >>
522              
523             Add a new GROUP BY clause.
524              
525             my $stmt = SQL::Maker::Select->new()
526             ->add_select('c')
527             ->add_from('foo')
528             ->add_group_by('id')
529             ->as_sql();
530             # => "SELECT c FROM foo GROUP BY id"
531              
532             my $stmt = SQL::Maker::Select->new()
533             ->add_select('c')
534             ->add_from('foo')
535             ->add_group_by('id' => 'DESC')
536             ->as_sql();
537             # => "SELECT c FROM foo GROUP BY id DESC"
538              
539             =item C<< $stmt->limit(30) >>
540              
541             =item C<< $stmt->offset(5) >>
542              
543             Add LIMIT and OFFSET.
544              
545             my $stmt = SQL::Maker::Select->new()
546             ->add_select('c')
547             ->add_from('foo')
548             ->limit(30)
549             ->offset(5)
550             ->as_sql();
551             # => "SELECT c FROM foo LIMIT 30 OFFSET 5"
552              
553             =item C<< $stmt->add_having(cnt => 2) >>
554              
555             Add a HAVING clause.
556              
557             my $stmt = SQL::Maker::Select->new()
558             ->add_from('foo')
559             ->add_select(\'COUNT(*)' => 'cnt')
560             ->add_having(cnt => 2)
561             ->as_sql();
562             # => "SELECT COUNT(*) AS cnt FROM foo HAVING (COUNT(*) = ?)"
563              
564             =back
565              
566             =head1 SEE ALSO
567              
568             L<Data::ObjectDriver::SQL>
569