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   98436 use Moose;
  9         2154338  
  9         60  
3 9     9   64870 use MooseX::Storage;
  9         361307  
  9         49  
4 9     9   7363 use YAML qw(Dump Load DumpFile LoadFile);
  9         64165  
  9         18590  
5             with Storage( 'format' => 'YAML' );
6              
7             our $VERSION = '1.0.2';
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 5541 my $self = shift;
28 16         24 my ( $term, $col ) = @_;
29 16   66     67 $col ||= $term;
30 16         17 push @{ $self->select }, $term;
  16         413  
31 16         397 $self->select_map->{$term} = $col;
32 16         410 $self->select_map_reverse->{$col} = $term;
33             }
34              
35             sub add_join {
36 5     5 0 459 my $self = shift;
37 5         6 my ( $table, $joins ) = @_;
38 5 100       6 push @{ $self->joins },
  5         138  
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 12338 my $self = shift;
76              
77 32         855 my $indent = $self->indent;
78 32         49 my $sql = '';
79              
80 32 100       31 if ( @{ $self->select } ) {
  32         767  
81 15         19 my %select_map = %{ $self->select_map };
  15         402  
82 15         31 my @terms;
83 15         17 for my $term ( @{ $self->select } ) {
  15         353  
84 46 100       77 if ( exists $select_map{$term} ) {
85 30         38 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     662 if ( $alias and $term =~ /(?:^|\.)\Q$alias\E$/ ) {
90 24         63 push @terms, $term;
91             }
92             else {
93 6         32 push @terms, "$term $alias";
94             }
95             }
96             else {
97 16         25 push @terms, $term;
98             }
99             }
100              
101 15         32 $sql .= "SELECT\n";
102 15         74 $sql .= $indent . join( ",\n$indent", @terms ) . "\n";
103             }
104 32         53 $sql .= "FROM ";
105              
106             # Add any explicit JOIN statements before the non-joined tables.
107 32 100 66     797 if ( $self->joins && @{ $self->joins } ) {
  32         726  
108 5         6 my $initial_table_written = 0;
109 5         5 for my $j ( @{ $self->joins } ) {
  5         111  
110 8         10 my ( $table, $joins ) = map { $j->{$_} } qw( table joins );
  16         25  
111 8 100       18 $sql .= $table unless $initial_table_written++;
112 8         7 for my $join ( @{ $j->{joins} } ) {
  8         12  
113             $sql
114             .= "\n"
115             . $indent
116             . uc( $join->{type} )
117             . ' JOIN '
118             . $join->{table} . " ON\n"
119             . $indent x 2
120 9         36 . $join->{condition};
121             }
122             }
123 5 100       5 $sql .= ', ' if @{ $self->from };
  5         114  
124             }
125 32         49 $sql .= join( ', ', @{ $self->from } ) . "\n";
  32         733  
126 32         76 $sql .= $self->as_sql_where;
127              
128 32         90 $sql .= $self->as_aggregate('group');
129 32         93 $sql .= $self->as_sql_having;
130 32         77 $sql .= $self->as_aggregate('order');
131              
132 32         114 $sql .= $self->as_limit;
133              
134 32 100       35 if ( keys %{ $self->replace } ) {
  32         782  
135 4         6 for my $find ( keys %{ $self->replace } ) {
  4         91  
136 7         8 my $replace = ${ $self->replace }{$find};
  7         192  
137 7         89 $sql =~ s/\Q$find\E/$replace/gi;
138             }
139             }
140              
141 32         1158 return $sql;
142             }
143              
144             sub as_limit {
145 32     32 0 35 my $self = shift;
146 32 100       773 my $n = $self->limit
147             or return '';
148 14 100       389 return sprintf "LIMIT %d%s\n", $n, ( $self->offset ? " OFFSET " . int( $self->offset ) : "" );
149             }
150              
151             sub as_aggregate {
152 64     64 0 69 my $self = shift;
153 64         68 my ($set) = @_;
154 64         1488 my $indent = $self->indent;
155              
156 64 100       1544 if ( my $attribute = $self->$set() ) {
157 16 100       47 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         99  
164             . "\n";
165             }
166              
167 48         84 return '';
168             }
169              
170             sub as_sql_where {
171 45     45 0 103 my $self = shift;
172 45         1141 my $indent = $self->indent;
173             $self->where && @{ $self->where }
174 45 100 66     1121 ? 'WHERE ' . join( "\n$indent" . "AND ", @{ $self->where } ) . "\n"
  26         638  
175             : '';
176             }
177              
178             sub as_sql_having {
179 32     32 0 36 my $self = shift;
180 32         772 my $indent = $self->indent;
181             $self->having && @{ $self->having }
182 32 100 66     845 ? 'HAVING ' . join( "\n$indent" . "AND ", @{ $self->having } ) . "\n"
  4         93  
183             : '';
184             }
185              
186             sub add_where {
187 30     30 0 14429 my $self = shift;
188             ## xxx Need to support old range and transform behaviors.
189 30         60 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         45 push @{ $self->{where} }, "($term)";
  30         96  
194 30         62 push @{ $self->{bind} }, @$bind;
  30         77  
195 30         1084 $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 15 my $self = shift;
208 3         6 my ( $col, $val ) = @_;
209              
210 3 50       79 if ( my $orig = $self->select_map_reverse->{$col} ) {
211 3         6 $col = $orig;
212             }
213              
214 3         9 my ( $term, $bind ) = $self->_mk_term( $col, $val );
215 3         8 push @{ $self->{having} }, "($term)";
  3         11  
216 3         3 push @{ $self->{bind} }, @$bind;
  3         10  
217             }
218              
219             #@returns AlignDB::SQL
220             sub copy {
221 1     1 0 5 my $self = shift;
222 1         7 my $copy = __PACKAGE__->thaw( $self->freeze );
223 1         20298 return $copy;
224             }
225              
226             sub _mk_term {
227 56     56   74 my $self = shift;
228 56         69 my ( $col, $val ) = @_;
229 56         71 my $term = '';
230 56         63 my @bind;
231 56 100       203 if ( ref($val) eq 'ARRAY' ) {
    100          
    100          
232 11 100 100     85 if ( ref $val->[0] or $val->[0] eq '-and' ) {
233 9         15 my $logic = 'OR';
234 9         30 my @values = @$val;
235 9 100       32 if ( $val->[0] eq '-and' ) {
236 7         11 $logic = 'AND';
237 7         11 shift @values;
238             }
239              
240 9         18 my @terms;
241 9         22 for my $v (@values) {
242 23         62 my ( $term, $bind ) = $self->_mk_term( $col, $v );
243 23         55 push @terms, "($term)";
244 23         53 push @bind, @$bind;
245             }
246 9         43 $term = join " $logic ", @terms;
247             }
248             else {
249 2         60 $term = "$col IN (" . join( ',', ('?') x scalar @$val ) . ')';
250 2         8 @bind = @$val;
251             }
252             }
253             elsif ( ref($val) eq 'HASH' ) {
254 14   66     90 my $c = $val->{column} || $col;
255 14         40 $term = "$c $val->{op} ?";
256 14         30 push @bind, $val->{value};
257             }
258             elsif ( ref($val) eq 'SCALAR' ) {
259 4         14 $term = "$col $$val";
260             }
261             else {
262 27         48 $term = "$col = ?";
263 27         51 push @bind, $val;
264             }
265              
266 56         160 return ( $term, \@bind );
267             }
268              
269             1;
270              
271             __END__
272              
273             =pod
274              
275             =encoding UTF-8
276              
277             =head1 NAME
278              
279             AlignDB::SQL - An SQL statement generator.
280              
281             =head1 SYNOPSIS
282              
283             my $sql = AlignDB::SQL->new();
284             $sql->select([ 'id', 'name', 'bucket_id', 'note_id' ]);
285             $sql->from([ 'foo' ]);
286             $sql->add_where('name', 'fred');
287             $sql->add_where('bucket_id', { op => '!=', value => 47 });
288             $sql->add_where('note_id', \'IS NULL');
289             $sql->limit(1);
290              
291             my $sth = $dbh->prepare($sql->as_sql);
292             $sth->execute(@{ $sql->{bind} });
293             my @values = $sth->selectrow_array();
294              
295             my $obj = SomeObject->new();
296             $obj->set_columns(...);
297              
298             =head1 DESCRIPTION
299              
300             I<AlignDB::SQL> represents an SQL statement.
301              
302             Most codes come from Data::ObjectDriver::SQL
303              
304             =head1 ATTRIBUTES
305              
306             =head2 replace
307              
308             with this, as_sql() method will replace strings in the final SQL statement
309              
310             =head1 ACKNOWLEDGEMENTS
311              
312             Sixapart
313              
314             =head1 AUTHOR
315              
316             Qiang Wang <wang-q@outlook.com>
317              
318             =head1 COPYRIGHT AND LICENSE
319              
320             This software is copyright (c) 2008 by Qiang Wang.
321              
322             This is free software; you can redistribute it and/or modify it under
323             the same terms as the Perl 5 programming language system itself.
324              
325             =cut