File Coverage

blib/lib/SQL/Maker/Select.pm
Criterion Covered Total %
statement 192 199 96.4
branch 80 94 85.1
condition 27 43 62.7
subroutine 31 32 96.8
pod 14 22 63.6
total 344 390 88.2


line stmt bran cond sub pod time code
1             package SQL::Maker::Select;
2 18     18   1376 use strict;
  18         19  
  18         568  
3 18     18   77 use warnings;
  18         29  
  18         378  
4 18     18   8154 use utf8;
  18         118  
  18         72  
5 18     18   7181 use SQL::Maker::Condition;
  18         51  
  18         544  
6 18     18   117 use SQL::Maker::Util;
  18         26  
  18         597  
7             use Class::Accessor::Lite (
8 18         133 new => 0,
9             wo => [qw/distinct for_update/],
10             rw => [qw/prefix/],
11             ro => [qw/quote_char name_sep new_line strict/],
12 18     18   1474 );
  18         2495  
13 18     18   2132 use Scalar::Util ();
  18         31  
  18         39267  
14              
15             sub offset {
16 7 50   7 1 17 if (@_==1) {
17 0         0 return $_[0]->{offset};
18             } else {
19 7         20 $_[0]->{offset} = $_[1];
20 7         15 return $_[0];
21             }
22             }
23              
24             sub limit {
25 22 50   22 1 52 if (@_==1) {
26 0         0 $_[0]->{limit};
27             } else {
28 22         60 $_[0]->{limit} = $_[1];
29 22         38 return $_[0];
30             }
31             }
32              
33             sub new {
34 187     187 0 94414 my $class = shift;
35 187 50       687 my %args = @_ == 1 ? %{$_[0]} : @_;
  0         0  
36 187         1683 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 187         648 return $self;
53             }
54              
55             sub new_condition {
56 33     33 0 60 my $self = shift;
57              
58 33         134 SQL::Maker::Condition->new(
59             quote_char => $self->{quote_char},
60             name_sep => $self->{name_sep},
61             strict => $self->{strict},
62             );
63             }
64              
65             sub bind {
66 176     176 1 3050 my $self = shift;
67 176         150 my @bind;
68 176 100       329 push @bind, @{$self->{subqueries}} if $self->{subqueries};
  14         20  
69 176 100       603 push @bind, $self->{where}->bind if $self->{where};
70 176 50       304 push @bind, $self->{having}->bind if $self->{having};
71 176 100       883 return wantarray ? @bind : \@bind;
72             }
73              
74             sub add_select {
75 184     184 1 325 my ($self, $term, $col) = @_;
76              
77 184   66     554 $col ||= $term;
78 184         165 push @{ $self->{select} }, $term;
  184         347  
79 184         275 $self->{select_map}->{$term} = $col;
80 184         231 $self->{select_map_reverse}->{$col} = $term;
81 184         439 return $self;
82             }
83              
84             sub add_from {
85 160     160 1 278 my ($self, $table, $alias) = @_;
86 160 100 66     592 if ( Scalar::Util::blessed( $table ) and $table->can('as_sql') ) {
87 6         7 push @{ $self->{subqueries} }, $table->bind;
  6         24  
88 6         7 push @{$self->{from}}, [ \do{ '(' . $table->as_sql . ')' }, $alias ];
  6         16  
  6         11  
89             }
90             else {
91 154         133 push @{$self->{from}}, [$table, $alias];
  154         342  
92             }
93 160         288 return $self;
94             }
95              
96             sub add_join {
97 40     40 1 240 my ($self, $table_ref, $joins) = @_;
98 40 100       88 my ($table, $alias) = ref($table_ref) eq 'ARRAY' ? @$table_ref : ($table_ref);
99              
100 40 100 66     147 if ( Scalar::Util::blessed( $table ) and $table->can('as_sql') ) {
101 5         8 push @{ $self->{subqueries} }, $table->bind;
  5         46  
102 5         7 $table = \do{ '(' . $table->as_sql . ')' };
  5         12  
103             }
104              
105 40         39 push @{ $self->{joins} }, {
  40         113  
106             table => [ $table, $alias ],
107             joins => $joins,
108             };
109 40         120 return $self;
110             }
111              
112             sub add_index_hint {
113 12     12 1 36 my ($self, $table, $hint) = @_;
114              
115 12         13 my ($type, $list);
116              
117 12 100       29 if (ref $hint eq 'HASH') {
118             # { type => '...', list => ['foo'] }
119 8   50     19 $type = $hint->{type} || 'USE';
120 8 50       21 $list = ref($hint->{list}) eq 'ARRAY' ? $hint->{list} : [ $hint->{list} ];
121             } else {
122             # ['foo, 'bar'] or just 'foo'
123 4         3 $type = 'USE';
124 4 100       13 $list = ref($hint) eq 'ARRAY' ? $hint : [ $hint ];
125             }
126              
127 12         29 $self->{index_hint}->{$table} = {
128             type => $type,
129             list => $list,
130             };
131              
132 12         29 return $self;
133             }
134              
135             sub _quote {
136 704     704   659 my ($self, $label) = @_;
137              
138 704 100       1106 return $$label if ref $label eq 'SCALAR';
139 664         1348 SQL::Maker::Util::quote_identifier($label, $self->{quote_char}, $self->{name_sep})
140             }
141              
142             sub as_sql {
143 246     246 1 390 my $self = shift;
144 246         247 my $sql = '';
145 246         496 my $new_line = $self->new_line;
146            
147 246 100       1031 if (@{ $self->{select} }) {
  246         496  
148 197         290 $sql .= $self->{prefix};
149 197 100       336 $sql .= 'DISTINCT ' if $self->{distinct};
150 251         345 $sql .= join(', ', map {
151 197         286 my $alias = $self->{select_map}->{$_};
152 251 50 66     4059 if (!$alias) {
    100          
153 0         0 $self->_quote($_)
154             } elsif ($alias && $_ =~ /(?:^|\.)\Q$alias\E$/) {
155 240         417 $self->_quote($_)
156             } else {
157 11         29 $self->_quote($_) . ' AS ' . $self->_quote($alias)
158             }
159 197         192 } @{ $self->{select} }) . $new_line;
160             }
161              
162 246         293 $sql .= 'FROM ';
163              
164             ## Add any explicit JOIN statements before the non-joined tables.
165 246 100 66     521 if ($self->{joins} && @{ $self->{joins} }) {
  246         684  
166 33         35 my $initial_table_written = 0;
167 33         27 for my $j (@{ $self->{joins} }) {
  33         58  
168 42         45 my ($table, $join) = map { $j->{$_} } qw( table joins );
  84         116  
169 42         71 $table = $self->_add_index_hint(@$table); ## index hint handling
170 42 100       120 $sql .= $table unless $initial_table_written++;
171 42 50       126 $sql .= ' ' . uc($join->{type}) if $join->{type};
172 42         73 $sql .= ' JOIN ' . $self->_quote($join->{table});
173 42 100       106 $sql .= ' ' . $self->_quote($join->{alias}) if $join->{alias};
174              
175 42 100       96 if ( defined $join->{condition} ) {
176 40 100 100     160 if (ref $join->{condition} && ref $join->{condition} eq 'ARRAY') {
    100 66        
177 3         5 $sql .= ' USING ('. join(', ', map { $self->_quote($_) } @{ $join->{condition} }) . ')';
  5         8  
  3         6  
178             }
179             elsif (ref $join->{condition} && ref $join->{condition} eq 'HASH') {
180 3         6 my @conds;
181 3         4 for my $key (keys %{ $join->{condition} }) {
  3         14  
182 4         22 push @conds, $self->_quote($key) . ' = ' . $self->_quote($join->{condition}{$key});
183             }
184 3         12 $sql .= ' ON ' . join(' AND ', @conds);
185             }
186             else {
187 34         85 $sql .= ' ON ' . $join->{condition};
188             }
189             }
190             }
191 33 100       30 $sql .= ', ' if @{ $self->{from} };
  33         71  
192             }
193              
194 246 100 66     452 if ($self->{from} && @{ $self->{from} }) {
  246         601  
195 224         483 $sql .= join ', ',
196 214         270 map { $self->_add_index_hint($_->[0], $_->[1]) }
197 214         193 @{ $self->{from} };
198             }
199              
200 246         247 $sql .= $new_line;
201 246 100       659 $sql .= $self->as_sql_where() if $self->{where};
202              
203 246 50       1050 $sql .= $self->as_sql_group_by if $self->{group_by};
204 246 100       518 $sql .= $self->as_sql_having if $self->{having};
205 246 50       638 $sql .= $self->as_sql_order_by if $self->{order_by};
206              
207 246 100       649 $sql .= $self->as_sql_limit if defined $self->{limit};
208              
209 241         545 $sql .= $self->as_sql_for_update;
210 241         1846 $sql =~ s/${new_line}+$//;
211              
212 241         817 return $sql;
213             }
214              
215             sub as_sql_limit {
216 24     24 0 28 my $self = shift;
217              
218 24         32 my $n = $self->{limit};
219 24 50       39 return '' unless defined $n;
220              
221 24 100       139 die "Non-numerics in limit clause ($n)" if $n =~ /\D/;
222 19 100       46 return sprintf "LIMIT %d%s" . $self->new_line, $n,
223             ($self->{offset} ? " OFFSET " . int($self->{offset}) : "");
224             }
225              
226             sub add_order_by {
227 48     48 1 85 my ($self, $col, $type) = @_;
228 48         46 push @{$self->{order_by}}, [$col, $type];
  48         93  
229 48         79 return $self;
230             }
231              
232             sub as_sql_order_by {
233 246     246 0 250 my ($self) = @_;
234              
235 246         189 my @attrs = @{$self->{order_by}};
  246         350  
236 246 100       514 return '' unless @attrs;
237              
238 48         85 return 'ORDER BY '
239             . join(', ', map {
240 39         56 my ($col, $type) = @$_;
241 48 100       82 if (ref $col) {
242 29         96 $$col
243             } else {
244 19 100       52 $type ? $self->_quote($col) . " $type" : $self->_quote($col)
245             }
246             } @attrs)
247             . $self->new_line;
248             }
249              
250             sub add_group_by {
251 36     36 1 70 my ($self, $group, $order) = @_;
252 36 100       32 push @{$self->{group_by}}, $order ? $self->_quote($group) . " $order" : $self->_quote($group);
  36         91  
253 36         75 return $self;
254             }
255              
256             sub as_sql_group_by {
257 246     246 0 235 my ($self,) = @_;
258              
259 246         236 my $elems = $self->{group_by};
260              
261 246 100       562 return '' if @$elems == 0;
262              
263 26         67 return 'GROUP BY '
264             . join(', ', @$elems)
265             . $self->new_line;
266             }
267              
268             sub set_where {
269 77     77 1 128 my ($self, $where) = @_;
270 77         164 $self->{where} = $where;
271 77         108 return $self;
272             }
273              
274             sub add_where {
275 25     25 1 55 my ($self, $col, $val) = @_;
276              
277 25   66     92 $self->{where} ||= $self->new_condition();
278 25         73 $self->{where}->add($col, $val);
279 25         50 return $self;
280             }
281              
282             sub add_where_raw {
283 6     6 1 18 my ($self, $term, $bind) = @_;
284              
285 6   66     20 $self->{where} ||= $self->new_condition();
286 6         17 $self->{where}->add_raw($term, $bind);
287 6         7 return $self;
288             }
289              
290             sub as_sql_where {
291 162     162 0 150 my $self = shift;
292              
293 162     0   726 my $where = $self->{where}->as_sql(undef, sub { $self->_quote($_[0]) });
  0         0  
294 162 100       623 $where ? "WHERE $where" . $self->new_line : '';
295             }
296              
297             sub as_sql_having {
298 3     3 0 4 my $self = shift;
299 3 50       10 if ($self->{having}) {
300 3         9 'HAVING ' . $self->{having}->as_sql . $self->new_line;
301             } else {
302 0         0 ''
303             }
304             }
305              
306             sub add_having {
307 3     3 1 10 my ($self, $col, $val) = @_;
308              
309 3 50       14 if (my $orig = $self->{select_map_reverse}->{$col}) {
310 3         5 $col = $orig;
311             }
312              
313 3   33     14 $self->{having} ||= $self->new_condition();
314 3         12 $self->{having}->add($col, $val);
315 3         5 return $self;
316             }
317              
318             sub as_sql_for_update {
319 241     241 0 210 my $self = shift;
320 241 50       443 $self->{for_update} ? ' FOR UPDATE' : '';
321             }
322              
323             sub _add_index_hint {
324 266     266   285 my ($self, $tbl_name, $alias) = @_;
325 266 100       459 my $quoted = $alias ? $self->_quote($tbl_name) . ' ' . $self->_quote($alias) : $self->_quote($tbl_name);
326 266         406 my $hint = $self->{index_hint}->{$tbl_name};
327 266 100 66     830 return $quoted unless $hint && ref($hint) eq 'HASH';
328 14 50 33     30 if ($hint->{list} && @{ $hint->{list} }) {
  14         37  
329 15         34 return $quoted . ' ' . uc($hint->{type} || 'USE') . ' INDEX (' .
330 14   50     39 join (',', map { $self->_quote($_) } @{ $hint->{list} }) .
  14         21  
331             ')';
332             }
333 0           return $quoted;
334             }
335              
336              
337             1;
338             __END__