File Coverage

blib/lib/SQL/Maker/Select.pm
Criterion Covered Total %
statement 192 198 96.9
branch 80 94 85.1
condition 27 43 62.7
subroutine 31 31 100.0
pod 14 22 63.6
total 344 388 88.6


line stmt bran cond sub pod time code
1             package SQL::Maker::Select;
2 17     17   2243 use strict;
  17         33  
  17         598  
3 17     17   103 use warnings;
  17         34  
  17         477  
4 17     17   22561 use utf8;
  17         151  
  17         95  
5 17     17   11940 use SQL::Maker::Condition;
  17         70  
  17         600  
6 17     17   339 use SQL::Maker::Util;
  17         59  
  17         897  
7             use Class::Accessor::Lite (
8 17         1302 new => 0,
9             wo => [qw/distinct for_update/],
10             rw => [qw/prefix/],
11             ro => [qw/quote_char name_sep new_line strict/],
12 17     17   3233 );
  17         4566  
13 17     17   3811 use Scalar::Util ();
  17         37  
  17         67448  
14              
15             sub offset {
16 7 50   7 1 24 if (@_==1) {
17 0         0 return $_[0]->{offset};
18             } else {
19 7         20 $_[0]->{offset} = $_[1];
20 7         21 return $_[0];
21             }
22             }
23              
24             sub limit {
25 22 50   22 1 72 if (@_==1) {
26 0         0 $_[0]->{limit};
27             } else {
28 22         74 $_[0]->{limit} = $_[1];
29 22         53 return $_[0];
30             }
31             }
32              
33             sub new {
34 190     190 0 179561 my $class = shift;
35 190 50       1333 my %args = @_ == 1 ? %{$_[0]} : @_;
  0         0  
36 190         2894 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 190         16595 return $self;
53             }
54              
55             sub new_condition {
56 34     34 0 2593 my $self = shift;
57              
58 34         240 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 177     177 1 5691 my $self = shift;
67 177         267 my @bind;
68 177 100       481 push @bind, @{$self->{subqueries}} if $self->{subqueries};
  14         37  
69 177 100       3559 push @bind, $self->{where}->bind if $self->{where};
70 177 50       550 push @bind, $self->{having}->bind if $self->{having};
71 177 100       2010 return wantarray ? @bind : \@bind;
72             }
73              
74             sub add_select {
75 186     186 1 545 my ($self, $term, $col) = @_;
76              
77 186   66     859 $col ||= $term;
78 186         228 push @{ $self->{select} }, $term;
  186         674  
79 186         574 $self->{select_map}->{$term} = $col;
80 186         439 $self->{select_map_reverse}->{$col} = $term;
81 186         652 return $self;
82             }
83              
84             sub add_from {
85 162     162 1 721 my ($self, $table, $alias) = @_;
86 162 100 66     997 if ( Scalar::Util::blessed( $table ) and $table->can('as_sql') ) {
87 6         12 push @{ $self->{subqueries} }, $table->bind;
  6         31  
88 6         12 push @{$self->{from}}, [ \do{ '(' . $table->as_sql . ')' }, $alias ];
  6         15  
  6         23  
89             }
90             else {
91 156         459 push @{$self->{from}}, [$table, $alias];
  156         1254  
92             }
93 162         549 return $self;
94             }
95              
96             sub add_join {
97 40     40 1 301 my ($self, $table_ref, $joins) = @_;
98 40 100       117 my ($table, $alias) = ref($table_ref) eq 'ARRAY' ? @$table_ref : ($table_ref);
99              
100 40 100 66     202 if ( Scalar::Util::blessed( $table ) and $table->can('as_sql') ) {
101 5         11 push @{ $self->{subqueries} }, $table->bind;
  5         32  
102 5         9 $table = \do{ '(' . $table->as_sql . ')' };
  5         17  
103             }
104              
105 40         56 push @{ $self->{joins} }, {
  40         164  
106             table => [ $table, $alias ],
107             joins => $joins,
108             };
109 40         184 return $self;
110             }
111              
112             sub add_index_hint {
113 12     12 1 50 my ($self, $table, $hint) = @_;
114              
115 12         16 my ($type, $list);
116              
117 12 100       34 if (ref $hint eq 'HASH') {
118             # { type => '...', list => ['foo'] }
119 8   50     26 $type = $hint->{type} || 'USE';
120 8 50       27 $list = ref($hint->{list}) eq 'ARRAY' ? $hint->{list} : [ $hint->{list} ];
121             } else {
122             # ['foo, 'bar'] or just 'foo'
123 4         6 $type = 'USE';
124 4 100       20 $list = ref($hint) eq 'ARRAY' ? $hint : [ $hint ];
125             }
126              
127 12         57 $self->{index_hint}->{$table} = {
128             type => $type,
129             list => $list,
130             };
131              
132 12         43 return $self;
133             }
134              
135             sub _quote {
136 706     706   981 my ($self, $label) = @_;
137              
138 706 100       1945 return $$label if ref $label eq 'SCALAR';
139 666         4820 SQL::Maker::Util::quote_identifier($label, $self->{quote_char}, $self->{name_sep})
140             }
141              
142             sub as_sql {
143 247     247 1 820 my $self = shift;
144 247         425 my $sql = '';
145 247         786 my $new_line = $self->new_line;
146            
147 247 100       1841 if (@{ $self->{select} }) {
  247         772  
148 198         643 $sql .= $self->{prefix};
149 198 100       1047 $sql .= 'DISTINCT ' if $self->{distinct};
150 252         654 $sql .= join(', ', map {
151 198         498 my $alias = $self->{select_map}->{$_};
152 252 50 66     7900 if (!$alias) {
    100          
153 0         0 $self->_quote($_)
154             } elsif ($alias && $_ =~ /(?:^|\.)\Q$alias\E$/) {
155 241         912 $self->_quote($_)
156             } else {
157 11         33 $self->_quote($_) . ' AS ' . $self->_quote($alias)
158             }
159 198         286 } @{ $self->{select} }) . $new_line;
160             }
161              
162 247         500 $sql .= 'FROM ';
163              
164             ## Add any explicit JOIN statements before the non-joined tables.
165 247 100 66     805 if ($self->{joins} && @{ $self->{joins} }) {
  247         2100  
166 33         53 my $initial_table_written = 0;
167 33         42 for my $j (@{ $self->{joins} }) {
  33         208  
168 42         65 my ($table, $join) = map { $j->{$_} } qw( table joins );
  84         184  
169 42         113 $table = $self->_add_index_hint(@$table); ## index hint handling
170 42 100       1306 $sql .= $table unless $initial_table_written++;
171 42 50       166 $sql .= ' ' . uc($join->{type}) if $join->{type};
172 42         101 $sql .= ' JOIN ' . $self->_quote($join->{table});
173 42 100       181 $sql .= ' ' . $self->_quote($join->{alias}) if $join->{alias};
174              
175 42 100       118 if ( defined $join->{condition} ) {
176 40 100 100     216 if (ref $join->{condition} && ref $join->{condition} eq 'ARRAY') {
    100 66        
177 3         6 $sql .= ' USING ('. join(', ', map { $self->_quote($_) } @{ $join->{condition} }) . ')';
  5         11  
  3         6  
178             }
179             elsif (ref $join->{condition} && ref $join->{condition} eq 'HASH') {
180 3         5 my @conds;
181 3         5 for my $key (keys %{ $join->{condition} }) {
  3         16  
182 4         32 push @conds, $self->_quote($key) . ' = ' . $self->_quote($join->{condition}{$key});
183             }
184 3         19 $sql .= ' ON ' . join(' AND ', @conds);
185             }
186             else {
187 34         125 $sql .= ' ON ' . $join->{condition};
188             }
189             }
190             }
191 33 100       53 $sql .= ', ' if @{ $self->{from} };
  33         779  
192             }
193              
194 247 100 66     861 if ($self->{from} && @{ $self->{from} }) {
  247         848  
195 225         1536 $sql .= join ', ',
196 215         427 map { $self->_add_index_hint($_->[0], $_->[1]) }
197 215         397 @{ $self->{from} };
198             }
199              
200 247         668 $sql .= $new_line;
201 247 100       1061 $sql .= $self->as_sql_where() if $self->{where};
202              
203 247 50       2450 $sql .= $self->as_sql_group_by if $self->{group_by};
204 247 100       793 $sql .= $self->as_sql_having if $self->{having};
205 247 50       1244 $sql .= $self->as_sql_order_by if $self->{order_by};
206              
207 247 100       982 $sql .= $self->as_sql_limit if defined $self->{limit};
208              
209 242         914 $sql .= $self->as_sql_for_update;
210 242         2426 $sql =~ s/${new_line}+$//;
211              
212 242         1319 return $sql;
213             }
214              
215             sub as_sql_limit {
216 24     24 0 39 my $self = shift;
217              
218 24         38 my $n = $self->{limit};
219 24 50       118 return '' unless defined $n;
220              
221 24 100       156 die "Non-numerics in limit clause ($n)" if $n =~ /\D/;
222 19 100       58 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 195 my ($self, $col, $type) = @_;
228 48         61 push @{$self->{order_by}}, [$col, $type];
  48         151  
229 48         141 return $self;
230             }
231              
232             sub as_sql_order_by {
233 247     247 0 333 my ($self) = @_;
234              
235 247         288 my @attrs = @{$self->{order_by}};
  247         668  
236 247 100       830 return '' unless @attrs;
237              
238 48         92 return 'ORDER BY '
239             . join(', ', map {
240 39         92 my ($col, $type) = @$_;
241 48 100       122 if (ref $col) {
242 29         128 $$col
243             } else {
244 19 100       66 $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 97 my ($self, $group, $order) = @_;
252 36 100       44 push @{$self->{group_by}}, $order ? $self->_quote($group) . " $order" : $self->_quote($group);
  36         132  
253 36         106 return $self;
254             }
255              
256             sub as_sql_group_by {
257 247     247 0 348 my ($self,) = @_;
258              
259 247         425 my $elems = $self->{group_by};
260              
261 247 100       1024 return '' if @$elems == 0;
262              
263 26         92 return 'GROUP BY '
264             . join(', ', @$elems)
265             . $self->new_line;
266             }
267              
268             sub set_where {
269 78     78 1 144 my ($self, $where) = @_;
270 78         219 $self->{where} = $where;
271 78         191 return $self;
272             }
273              
274             sub add_where {
275 25     25 1 85 my ($self, $col, $val) = @_;
276              
277 25   66     156 $self->{where} ||= $self->new_condition();
278 25         120 $self->{where}->add($col, $val);
279 25         78 return $self;
280             }
281              
282             sub add_where_raw {
283 6     6 1 26 my ($self, $term, $bind) = @_;
284              
285 6   66     30 $self->{where} ||= $self->new_condition();
286 6         29 $self->{where}->add_raw($term, $bind);
287 6         15 return $self;
288             }
289              
290             sub as_sql_where {
291 163     163 0 234 my $self = shift;
292              
293 163         920 my $where = $self->{where}->as_sql();
294 163 100       1560 $where ? "WHERE $where" . $self->new_line : '';
295             }
296              
297             sub as_sql_having {
298 3     3 0 5 my $self = shift;
299 3 50       13 if ($self->{having}) {
300 3         32 'HAVING ' . $self->{having}->as_sql . $self->new_line;
301             } else {
302 0         0 ''
303             }
304             }
305              
306             sub add_having {
307 3     3 1 12 my ($self, $col, $val) = @_;
308              
309 3 50       21 if (my $orig = $self->{select_map_reverse}->{$col}) {
310 3         6 $col = $orig;
311             }
312              
313 3   33     20 $self->{having} ||= $self->new_condition();
314 3         14 $self->{having}->add($col, $val);
315 3         9 return $self;
316             }
317              
318             sub as_sql_for_update {
319 242     242 0 342 my $self = shift;
320 242 50       704 $self->{for_update} ? ' FOR UPDATE' : '';
321             }
322              
323             sub _add_index_hint {
324 267     267   425 my ($self, $tbl_name, $alias) = @_;
325 267 100       1059 my $quoted = $alias ? $self->_quote($tbl_name) . ' ' . $self->_quote($alias) : $self->_quote($tbl_name);
326 267         701 my $hint = $self->{index_hint}->{$tbl_name};
327 267 100 66     1416 return $quoted unless $hint && ref($hint) eq 'HASH';
328 14 50 33     44 if ($hint->{list} && @{ $hint->{list} }) {
  14         49  
329 15         30 return $quoted . ' ' . uc($hint->{type} || 'USE') . ' INDEX (' .
330 14   50     51 join (',', map { $self->_quote($_) } @{ $hint->{list} }) .
  14         27  
331             ')';
332             }
333 0           return $quoted;
334             }
335              
336              
337             1;
338             __END__