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