File Coverage

blib/lib/AlignDB/SQL.pm
Criterion Covered Total %
statement 132 156 84.6
branch 41 48 85.4
condition 15 21 71.4
subroutine 14 16 87.5
pod 0 12 0.0
total 202 253 79.8


line stmt bran cond sub pod time code
1             package AlignDB::SQL;
2 9     9   90261 use Moose;
  9         1906307  
  9         51  
3 9     9   54671 use MooseX::Storage;
  9         310282  
  9         43  
4 9     9   6218 use YAML qw(Dump Load DumpFile LoadFile);
  9         54707  
  9         15628  
5             with Storage( 'format' => 'YAML' );
6              
7             our $VERSION = '1.0.1';
8              
9             has 'select' => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } );
10             has 'select_map' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
11             has 'select_map_reverse' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
12             has 'from' => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } );
13             has 'joins' => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } );
14             has 'where' => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } );
15             has 'bind' => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } );
16             has 'limit' => ( is => 'rw', isa => 'Int' );
17             has 'offset' => ( is => 'rw', );
18             has 'group' => ( is => 'rw', );
19             has 'order' => ( is => 'rw', );
20             has 'having' => ( is => 'rw', isa => 'ArrayRef', default => sub { [] } );
21             has 'where_values' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
22             has '_sql' => ( is => 'rw', isa => 'Str', default => '' );
23             has 'indent' => ( is => 'rw', isa => 'Str', default => ' ' x 2 );
24             has 'replace' => ( is => 'rw', isa => 'HashRef', default => sub { {} } );
25              
26             sub add_select {
27 16     16 0 5372 my $self = shift;
28 16         29 my ( $term, $col ) = @_;
29 16   66     62 $col ||= $term;
30 16         19 push @{ $self->select }, $term;
  16         453  
31 16         468 $self->select_map->{$term} = $col;
32 16         462 $self->select_map_reverse->{$col} = $term;
33             }
34              
35             sub add_join {
36 5     5 0 339 my $self = shift;
37 5         7 my ( $table, $joins ) = @_;
38 5 100       5 push @{ $self->joins },
  5         120  
39             {
40             table => $table,
41             joins => ref($joins) eq 'ARRAY' ? $joins : [$joins],
42             };
43             }
44              
45             sub as_header {
46 0     0 0 0 my $self = shift;
47              
48 0         0 my @terms;
49 0 0       0 if ( @{ $self->select } ) {
  0         0  
50 0         0 my %select_map = %{ $self->select_map };
  0         0  
51 0         0 for my $term ( @{ $self->select } ) {
  0         0  
52 0 0       0 if ( exists $select_map{$term} ) {
53 0         0 my $alias = $select_map{$term};
54 0         0 push @terms, $alias;
55             }
56             else {
57 0         0 push @terms, $term;
58             }
59             }
60             }
61              
62 0 0       0 if ( keys %{ $self->replace } ) {
  0         0  
63 0         0 for my $find ( keys %{ $self->replace } ) {
  0         0  
64 0         0 my $replace = ${ $self->replace }{$find};
  0         0  
65 0         0 for (@terms) {
66 0         0 s/\Q$find\E/$replace/gi;
67             }
68             }
69             }
70              
71 0         0 return @terms;
72             }
73              
74             sub as_sql {
75 32     32 0 19802 my $self = shift;
76              
77 32         860 my $indent = $self->indent;
78 32         43 my $sql = '';
79              
80 32 100       36 if ( @{ $self->select } ) {
  32         799  
81 15         20 my %select_map = %{ $self->select_map };
  15         458  
82 15         29 my @terms;
83 15         17 for my $term ( @{ $self->select } ) {
  15         417  
84 46 100       95 if ( exists $select_map{$term} ) {
85 30         57 my $alias = $select_map{$term};
86              
87             # add_select( 'f.foo' => 'foo' ) ===> f.foo
88             # add_select( 'COUNT(*)' => 'count' ) ===> COUNT(*) count
89 30 100 66     806 if ( $alias and $term =~ /(?:^|\.)\Q$alias\E$/ ) {
90 24         85 push @terms, $term;
91             }
92             else {
93 6         21 push @terms, "$term $alias";
94             }
95             }
96             else {
97 16         31 push @terms, $term;
98             }
99             }
100              
101 15         34 $sql .= "SELECT\n";
102 15         79 $sql .= $indent . join( ",\n$indent", @terms ) . "\n";
103             }
104 32         52 $sql .= "FROM ";
105              
106             # Add any explicit JOIN statements before the non-joined tables.
107 32 100 66     887 if ( $self->joins && @{ $self->joins } ) {
  32         826  
108 5         6 my $initial_table_written = 0;
109 5         5 for my $j ( @{ $self->joins } ) {
  5         109  
110 8         12 my ( $table, $joins ) = map { $j->{$_} } qw( table joins );
  16         28  
111 8 100       16 $sql .= $table unless $initial_table_written++;
112 8         8 for my $join ( @{ $j->{joins} } ) {
  8         14  
113             $sql
114             .= "\n"
115             . $indent
116             . uc( $join->{type} )
117             . ' JOIN '
118             . $join->{table} . " ON\n"
119             . $indent x 2
120 9         35 . $join->{condition};
121             }
122             }
123 5 100       6 $sql .= ', ' if @{ $self->from };
  5         117  
124             }
125 32         49 $sql .= join( ', ', @{ $self->from } ) . "\n";
  32         862  
126 32         76 $sql .= $self->as_sql_where;
127              
128 32         94 $sql .= $self->as_aggregate('group');
129 32         79 $sql .= $self->as_sql_having;
130 32         103 $sql .= $self->as_aggregate('order');
131              
132 32         110 $sql .= $self->as_limit;
133              
134 32 100       39 if ( keys %{ $self->replace } ) {
  32         775  
135 4         6 for my $find ( keys %{ $self->replace } ) {
  4         89  
136 7         7 my $replace = ${ $self->replace }{$find};
  7         159  
137 7         87 $sql =~ s/\Q$find\E/$replace/gi;
138             }
139             }
140              
141 32         219 return $sql;
142             }
143              
144             sub as_limit {
145 32     32 0 42 my $self = shift;
146 32 100       799 my $n = $self->limit
147             or return '';
148 14 100       395 return sprintf "LIMIT %d%s\n", $n, ( $self->offset ? " OFFSET " . int( $self->offset ) : "" );
149             }
150              
151             sub as_aggregate {
152 64     64 0 74 my $self = shift;
153 64         80 my ($set) = @_;
154 64         1611 my $indent = $self->indent;
155              
156 64 100       1713 if ( my $attribute = $self->$set() ) {
157 16 100       46 my $elements
158             = ( ref($attribute) eq 'ARRAY' ) ? $attribute : [$attribute];
159             return
160             uc($set)
161             . " BY\n$indent"
162             . join( ",\n$indent",
163 16 100       45 map { $_->{column} . ( $_->{desc} ? ( ' ' . $_->{desc} ) : '' ) } @$elements )
  19         93  
164             . "\n";
165             }
166              
167 48         106 return '';
168             }
169              
170             sub as_sql_where {
171 45     45 0 90 my $self = shift;
172 45         1124 my $indent = $self->indent;
173             $self->where && @{ $self->where }
174 45 100 66     1068 ? 'WHERE ' . join( "\n$indent" . "AND ", @{ $self->where } ) . "\n"
  26         631  
175             : '';
176             }
177              
178             sub as_sql_having {
179 32     32 0 39 my $self = shift;
180 32         822 my $indent = $self->indent;
181             $self->having && @{ $self->having }
182 32 100 66     824 ? 'HAVING ' . join( "\n$indent" . "AND ", @{ $self->having } ) . "\n"
  4         92  
183             : '';
184             }
185              
186             sub add_where {
187 30     30 0 12729 my $self = shift;
188             ## xxx Need to support old range and transform behaviors.
189 30         51 my ( $col, $val ) = @_;
190              
191             #croak("Invalid/unsafe column name $col") unless $col =~ /^[\w\.]+$/;
192 30         81 my ( $term, $bind ) = $self->_mk_term( $col, $val );
193 30         40 push @{ $self->{where} }, "($term)";
  30         81  
194 30         32 push @{ $self->{bind} }, @$bind;
  30         60  
195 30         1013 $self->where_values->{$col} = $val;
196             }
197              
198             sub has_where {
199 0     0 0 0 my $self = shift;
200 0         0 my ( $col, $val ) = @_;
201              
202             # TODO: should check if the value is same with $val?
203 0         0 exists $self->where_values->{$col};
204             }
205              
206             sub add_having {
207 3     3 0 17 my $self = shift;
208 3         7 my ( $col, $val ) = @_;
209              
210 3 50       78 if ( my $orig = $self->select_map_reverse->{$col} ) {
211 3         5 $col = $orig;
212             }
213              
214 3         7 my ( $term, $bind ) = $self->_mk_term( $col, $val );
215 3         6 push @{ $self->{having} }, "($term)";
  3         9  
216 3         5 push @{ $self->{bind} }, @$bind;
  3         10  
217             }
218              
219             sub copy {
220 1     1 0 4 my $self = shift;
221 1         6 my $copy = __PACKAGE__->thaw( $self->freeze );
222 1         20661 return $copy;
223             }
224              
225             sub _mk_term {
226 56     56   58 my $self = shift;
227 56         58 my ( $col, $val ) = @_;
228 56         73 my $term = '';
229 56         54 my @bind;
230 56 100       198 if ( ref($val) eq 'ARRAY' ) {
    100          
    100          
231 11 100 100     70 if ( ref $val->[0] or $val->[0] eq '-and' ) {
232 9         13 my $logic = 'OR';
233 9         22 my @values = @$val;
234 9 100       25 if ( $val->[0] eq '-and' ) {
235 7         9 $logic = 'AND';
236 7         9 shift @values;
237             }
238              
239 9         10 my @terms;
240 9         18 for my $v (@values) {
241 23         46 my ( $term, $bind ) = $self->_mk_term( $col, $v );
242 23         38 push @terms, "($term)";
243 23         40 push @bind, @$bind;
244             }
245 9         29 $term = join " $logic ", @terms;
246             }
247             else {
248 2         55 $term = "$col IN (" . join( ',', ('?') x scalar @$val ) . ')';
249 2         9 @bind = @$val;
250             }
251             }
252             elsif ( ref($val) eq 'HASH' ) {
253 14   66     68 my $c = $val->{column} || $col;
254 14         35 $term = "$c $val->{op} ?";
255 14         29 push @bind, $val->{value};
256             }
257             elsif ( ref($val) eq 'SCALAR' ) {
258 4         17 $term = "$col $$val";
259             }
260             else {
261 27         33 $term = "$col = ?";
262 27         41 push @bind, $val;
263             }
264              
265 56         124 return ( $term, \@bind );
266             }
267              
268             1;
269              
270             __END__
271              
272             =pod
273              
274             =encoding UTF-8
275              
276             =head1 NAME
277              
278             AlignDB::SQL - An SQL statement generator.
279              
280             =head1 SYNOPSIS
281              
282             my $sql = AlignDB::SQL->new();
283             $sql->select([ 'id', 'name', 'bucket_id', 'note_id' ]);
284             $sql->from([ 'foo' ]);
285             $sql->add_where('name', 'fred');
286             $sql->add_where('bucket_id', { op => '!=', value => 47 });
287             $sql->add_where('note_id', \'IS NULL');
288             $sql->limit(1);
289              
290             my $sth = $dbh->prepare($sql->as_sql);
291             $sth->execute(@{ $sql->{bind} });
292             my @values = $sth->selectrow_array();
293              
294             my $obj = SomeObject->new();
295             $obj->set_columns(...);
296              
297             =head1 DESCRIPTION
298              
299             I<AlignDB::SQL> represents an SQL statement.
300              
301             Most codes come from Data::ObjectDriver::SQL
302              
303             =head1 ATTRIBUTES
304              
305             =head2 replace
306              
307             with this, as_sql() method will replace strings in the final SQL statement
308              
309             =head1 ACKNOWLEDGEMENTS
310              
311             Sixapart
312              
313             =head1 AUTHOR
314              
315             Qiang Wang <wang-q@outlook.com>
316              
317             =head1 COPYRIGHT AND LICENSE
318              
319             This software is copyright (c) 2008 by Qiang Wang.
320              
321             This is free software; you can redistribute it and/or modify it under
322             the same terms as the Perl 5 programming language system itself.
323              
324             =cut