File Coverage

blib/lib/Data/Model/SQL.pm
Criterion Covered Total %
statement 222 240 92.5
branch 76 96 79.1
condition 29 38 76.3
subroutine 23 23 100.0
pod 0 13 0.0
total 350 410 85.3


line stmt bran cond sub pod time code
1             package Data::Model::SQL;
2 95     2721   1145 use strict;
  95         232  
  95         3204  
3 95     95   460 use warnings;
  95         169  
  95         2722  
4 95     95   460 use base qw(Data::Model::Accessor);
  95         167  
  95         59983  
5              
6 95     95   646 use Carp ();
  95         168  
  95         4767  
7             $Carp::Internal{(__PACKAGE__)}++;
8              
9             __PACKAGE__->mk_accessors(qw/ select where having bind bind_column limit offset select_map select_map_reverse column_mutator where_values /);
10              
11              
12             for my $name (qw/ from joins /) {
13 95     95   534 no strict 'refs';
  95         179  
  95         12068  
14             *{$name} = sub {
15 4684 100   4684   27471 return $_[0]->{$name} unless @_ > 1;
16 698         1283 my $self = shift;
17 698 100 100     6401 $self->{$name} = ((@_ == 1 && ref($_[0]) eq 'ARRAY') ? $_[0] : [@_]);
18             };
19             }
20              
21             for my $name (qw/ group order /) {
22 95     95   519 no strict 'refs';
  95         204  
  95         489636  
23             *{$name} = sub {
24 4547 100   4547   32538 return $_[0]->{$name} unless @_ > 1;
25 1113         2360 my $self = shift;
26 1113 100 100     9114 $self->{$name} = ((@_ == 1 && ref($_[0]) eq 'ARRAY') ? $_[0] : [@_]);
27             };
28             }
29              
30             sub new {
31 590     590 0 4459 my($class, %args) = @_;
32 590         4523 my $self = bless { %args }, $class;
33 590         2093 for my $name (qw/ select from joins bind bind_column group order where /) {
34 4720 100 66     40595 unless ($self->$name && ref $self->$name eq 'ARRAY') {
35 3588 50       10586 $self->$name ? $self->$name([ $self->$name ]) : $self->$name([]);;
36             }
37             }
38 590         2309 for my $name (qw/ select_map select_map_reverse where_values /) {
39 1770 50 33     7578 $self->$name( {} ) unless $self->$name && ref $self->$name eq 'HASH';
40             }
41              
42             # ここで select, join, where クエリ 等を%args から構築する
43              
44             # where
45 590 100       2745 if (exists $args{where}) {
46 89         147 my @wheres;
47 89 50       348 if (ref($args{where}) eq 'ARRAY') {
    0          
48 89         210 while (my($column, $value) = splice @{ $args{where} }, 0, 2) {
  182         910  
49 93         350 push @wheres, +[ $column, $value ];
50             }
51             } elsif (ref($args{where}) eq 'HASH') {
52 0         0 while (my($column, $value) = each %{ $args{where} }) {
  0         0  
53 0         0 push @wheres, +[ $column, $value ];
54             }
55             } else {
56 0         0 Carp::croak('where requires the type of ARRAY or HASH reference');
57             }
58              
59 89         248 for my $where (@wheres) {
60 93         151 $self->add_where(@{ $where });
  93         425  
61             }
62             }
63              
64             # where_sql
65 590 50       2306 if (exists $args{where_sql}) {
66 0         0 my @wheres;
67 0 0       0 if (ref($args{where_sql}) eq 'ARRAY') {
    0          
68 0         0 while (my($sql, $values) = splice @{ $args{where_sql} }, 0, 2) {
  0         0  
69 0         0 push @wheres, +[ $sql, $values ];
70             }
71             } elsif (ref($args{where_sql}) eq 'HASH') {
72 0         0 while (my($sql, $values) = each %{ $args{where} }) {
  0         0  
73 0         0 push @wheres, +[ $sql, $values ];
74             }
75             } else {
76 0         0 Carp::croak('where_sql requires the type of ARRAY or HASH reference');
77             }
78              
79 0         0 for my $where (@wheres) {
80 0         0 my($sql, $values) = @{ $where };
  0         0  
81 0         0 $self->add_where_sql( $sql => @{ $values });
  0         0  
82             }
83             }
84              
85              
86             =pod
87              
88             Data::Model::SQL->new(
89             where => +[
90             foo => [ -and => 'foo', 'bar', 'baz'],
91             bar => 'baz',
92             baz => +{ '!=' => 2 },
93             ],
94             order => [
95             +{ foo => 'ASC' },
96             ],
97             joins => [
98             foo => [
99             { inner => { 'baz b1' => 'foo.baz_id = b1.baz_id AND b1.quux_id = 1' }}
100             ],
101             ],
102             group => [qw/ foo bar /],
103             );
104              
105             =cut
106              
107 590         3333 $self;
108             }
109              
110             sub add_select {
111 10     10 0 54 my($self, $term, $col) = @_;
112 10         8 push @{ $self->{select} }, $term;
  10         20  
113 10 100       24 return unless $col;
114 7         17 $self->select_map->{$term} = $col;
115 7         17 $self->select_map_reverse->{$col} = $term;
116             }
117              
118             sub add_join {
119 5     5 0 10 my($self, $table, $joins) = @_;
120 5 100       6 push @{ $self->joins }, {
  5         10  
121             table => $table,
122             joins => ref($joins) eq 'ARRAY' ? $joins : [ $joins ],
123             };
124             }
125              
126             sub _add_where {
127 651     651   1400 my($self, $col, $val) = @_;
128 651 100 100     6276 if (lc($col) eq '-and' || lc($col) eq '-or') {
129 26 100       86 my $op = lc($col) eq '-and' ? 'AND' : 'OR';
130 26         49 my(@terms, @binds, @bind_columns, @tcols);
131 26         40 while (my($ccol, $cval) = splice @{ $val }, 0, 2) {
  75         358  
132 49         180 my($term, $bind, $bind_column, $tcol) = $self->_add_where( $ccol => $cval );
133 49         1150 push @terms, "($term)";
134 49         57 push @binds, @{ $bind };
  49         92  
135 49         60 push @bind_columns, @{ $bind_column };
  49         84  
136 49         66 push @tcols, @{ $tcol };
  49         197  
137             }
138 26         103 my $term = join " $op ", @terms;
139 26         114 return $term, \@binds, \@bind_columns, \@tcols;
140             } else {
141             ## xxx Need to support old range and transform behaviors.
142 625 50 66     5679 Carp::croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/ || ref($col) eq 'SCALAR';
143 625         2484 my($term, $bind, $tcol) = $self->_mk_term($col, $val);
144 625         2551 my @bind_column = (($tcol) x scalar(@$bind));
145 625         3957 return $term, $bind, \@bind_column, [ $tcol => $val ];
146             }
147             }
148              
149             sub add_where {
150 602     602 0 1223 my $self = shift;
151 602         2463 my($term, $binds, $bind_columns, $tcols) = $self->_add_where(@_);
152              
153 602         1975 push @{ $self->{where} }, "($term)";
  602         2534  
154 602         1456 push @{ $self->{bind} }, @{ $binds };
  602         1790  
  602         1609  
155 602         980 push @{ $self->{bind_column} }, @{ $bind_columns };
  602         1670  
  602         1705  
156 602         11769 my @tcols = @{ $tcols };
  602         1676  
157 602         3656 while (my($tcol, $tval) = splice @tcols, 0, 2) {
158 625 100       3220 $self->where_values->{$tcol} = $tval if defined $tcol;
159             }
160             }
161              
162             sub add_where_sql {
163 2     2 0 15 my($self, $term, @bind) = @_;
164              
165 2         3 my(@columns, @values);
166 2         10 while (my($column, $value) = splice @bind, 0, 2) {
167 5         14 $self->where_values->{$column} = $value;
168 5         9 push @columns, $column;
169 5         18 push @values, $value;
170             }
171              
172 2         3 push @{ $self->{where} }, sprintf("($term)", @columns);
  2         12  
173 2         3 push @{ $self->{bind_column} }, @columns;
  2         6  
174 2         3 push @{ $self->{bind} }, @values;
  2         8  
175             }
176              
177             sub add_having {
178 1     1 0 5 my $stmt = shift;
179 1         2 my($col, $val) = @_;
180              
181 1 50       4 if (my $orig = $stmt->select_map_reverse->{$col}) {
182 1         2 $col = $orig;
183             }
184              
185 1         5 my($term, $bind, $tcol) = $stmt->_mk_term($col, $val);
186 1         2 push @{ $stmt->{having} }, "($term)";
  1         4  
187 1         2 push @{ $stmt->{bind_column} }, (($tcol) x scalar(@$bind));
  1         3  
188 1         2 push @{ $stmt->{bind} }, @$bind;
  1         4  
189             }
190              
191             sub as_select {
192 537     537 0 1194 my $self = shift;
193 537         1170 my $sql = '';
194 537 100       855 if (@{ $self->select }) {
  537         1946  
195 461         1317 $sql .= 'SELECT ';
196 1066         3703 $sql .= join(', ', map {
197 461         2417 my $alias = $self->select_map->{$_};
198 1066 100       19499 $alias ? /(?:^|\.)\Q$alias\E$/ ? $_ : "$_ $alias" : $_;
    100          
199 461         863 } @{ $self->select });
200 461         1087 $sql .= "\n";
201             }
202 537         1906 $sql;
203             }
204              
205             sub as_join {
206 537     537 0 1287 my $self = shift;
207 537         1128 my $sql = '';
208 537 100 66     1356 if ($self->joins && @{ $self->joins }) {
  537         1330  
209 5         8 my $initial_table_written = 0;
210 5         6 for my $data (@{ $self->joins }) {
  5         6  
211 8         10 my($table, $joins) = map { $data->{$_} } qw( table joins );
  16         36  
212 8 100       23 $sql .= $table unless $initial_table_written++;
213 8         8 for my $join (@{ $joins }) {
  8         17  
214 9         12 my($type, $target) = (%{ $join });
  9         20  
215 9         12 my $condition = '';
216 9 50       21 if (ref $target eq 'HASH') {
217 9         10 my($key, $val) = (%{ $target });
  9         19  
218 9         14 $target = $key;
219 9         16 $condition = $val;
220             }
221 9         19 $sql .= ' ' . uc($type) . ' JOIN ' . $target;
222 9 50       53 $sql .= ' ON ' . $condition if $condition;
223             }
224             }
225 5 100       7 $sql .= ', ' if @{ $self->from };
  5         10  
226             }
227 537         2075 $sql;
228             }
229              
230             sub as_sql_where {
231 599     599 0 1428 my $self = shift;
232 599 100 66     1988 if ($self->where && @{ $self->where }) {
  599         1953  
233 551         1258 return 'WHERE ' . join(' AND ', @{ $self->where }) . "\n";
  551         1924  
234             }
235 48         125 return '';
236             }
237              
238             sub as_sql_having {
239 537     537 0 980 my $self = shift;
240 537 100 66     2299 if ($self->having && @{ $self->having }) {
  1         3  
241 1         1 return 'HAVING ' . join(' AND ', @{ $self->having }) . "\n";
  1         3  
242             }
243 536         1329 return '';
244             }
245              
246             sub as_limit {
247 537     537 0 1060 my $self = shift;
248 537 100       3119 my $n = $self->limit or return '';
249 8 100       253 Carp::croak "Non-numerics in limit clause ($n)" if $n =~ /\D/;
250 7 100       40 return sprintf "LIMIT %d%s\n", $n,
251             ($self->offset ? " OFFSET " . int($self->offset) : "");
252             }
253              
254             sub as_aggregate {
255 1074     1074 0 2370 my($self, $set) = @_;
256 1074 50       4188 return '' unless my $attribute = $self->$set;
257              
258 1074         1843 my @sqls;
259 1074         5477 for my $element (@{ $attribute }) {
  1074         2921  
260 142         270 my $ref = ref $element;
261 142 100       530 if (!$ref) {
    50          
262 5         12 push @sqls, $element;
263             } elsif ($ref eq 'HASH') {
264 137         1632 while (my($column, $desc) = each %{ $element }) {
  275         1123  
265 138         649 push @sqls, $column . ' ' . uc($desc);
266             }
267             }
268             }
269 1074 100       4647 return '' unless @sqls;
270 89         568 return uc($set) . ' BY ' . join(', ', @sqls) . "\n";
271             }
272              
273             sub as_sql {
274 537     537 0 1280 my $self = shift;
275              
276 537         1258 my $sql = '';
277 537         2275 $sql .= $self->as_select;
278              
279 537         1256 $sql .= 'FROM ';
280             ## Add any explicit JOIN statements before the non-joined tables.
281 537         2187 $sql .= $self->as_join;
282 537         1191 $sql .= join(', ', @{ $self->from }) . "\n";
  537         1690  
283              
284 537         2070 $sql .= $self->as_sql_where;
285              
286 537         3228 $sql .= $self->as_aggregate('group');
287 537         2399 $sql .= $self->as_sql_having;
288 537         1472 $sql .= $self->as_aggregate('order');
289              
290 537         2022 $sql .= $self->as_limit;
291 536         1992 $sql;
292             }
293              
294             sub _mk_term {
295 647     647   1679 my($self, $col, $val) = @_;
296 647         1205 my $term = '';
297 647         987 my (@bind, $m);
298 647 100       4596 if (ref($col) eq 'SCALAR') {
    100          
    100          
    100          
299 1         2 $term = ${ $col };
  1         3  
300 1         2 $col = undef;
301             } elsif (ref($val) eq 'ARRAY') {
302 29 100 50     285 if (ref $val->[0] or (($val->[0] || '') eq '-and')) {
      100        
303 8         15 my $logic = 'OR';
304 8         23 my @values = @$val;
305 8 100       24 if ($val->[0] eq '-and') {
306 7         11 $logic = 'AND';
307 7         13 shift @values;
308             }
309              
310 8         10 my @terms;
311 8         15 for my $v (@values) {
312 21         60 my($term, $bind) = $self->_mk_term($col, $v);
313 21         53 push @terms, "($term)";
314 21         56 push @bind, @$bind;
315             }
316 8         34 $term = join " $logic ", @terms;
317             } else {
318 21 50       89 $col = $m->($col) if $m = $self->column_mutator;
319 21         134 $term = "$col IN (".join(',', ('?') x scalar @$val).')';
320 21         105 @bind = @$val;
321             }
322             } elsif (ref($val) eq 'HASH') {
323 50 50       198 $col = $m->($col) if $m = $self->column_mutator;
324 50         97 my($op, $v) = (%{ $val });
  50         180  
325 50 100 100     417 if ((uc($op) eq 'IN' || uc($op) eq 'NOT IN') && ref($v) eq 'ARRAY') {
      66        
326 12         64 $term = "$col $op (".join(',', ('?') x scalar @$v).')';
327 12         38 @bind = @$v;
328             } else {
329 38         118 $term = "$col $op ?";
330 38         93 push @bind, $v;
331             }
332             } elsif (ref($val) eq 'SCALAR') {
333 1 50       5 $col = $m->($col) if $m = $self->column_mutator;
334 1         4 $term = "$col $$val";
335             } else {
336 566 50       2156 $col = $m->($col) if $m = $self->column_mutator;
337 566         1721 $term = "$col = ?";
338 566         2506 push @bind, $val;
339             }
340 647         3215 ($term, \@bind, $col);
341             }
342              
343             1;
344              
345             __END__