File Coverage

blib/lib/SQL/Concrete.pm
Criterion Covered Total %
statement 113 114 99.1
branch 53 60 88.3
condition 12 15 80.0
subroutine 23 23 100.0
pod 2 5 40.0
total 203 217 93.5


line stmt bran cond sub pod time code
1 1     1   57938 use 5.006;
  1         4  
  1         48  
2 1     1   6 use strict;
  1         2  
  1         44  
3 1     1   21 use warnings;
  1         1  
  1         138  
4              
5             package SQL::Concrete;
6             $SQL::Concrete::VERSION = '1.003';
7             # ABSTRACT: render SQL from fragments and placeholders from data structures
8              
9             use Exporter::Tidy
10 1         14 core => [ qw( sql_render ) ],
11             util => [ qw( sql ) ],
12             clauses => [ qw( sql_values sql_set sql_select ) ],
13             CLAUSES => [ qw( VALUES SET SELECT ) ],
14             all => [ qw( :core :util :CLAUSES ) ],
15             _map => { VALUES => 'sql_values', SET => 'sql_set', SELECT => 'sql_select' },
16 1     1   1503 noncore => [ qw( :util :clauses :CLAUSES ) ]; # used internally by SQL::Concrete::Dollars
  1         14  
17              
18 56     56 1 24898 sub sql_render { SQL::Concrete::Renderer->new->render( @_ ) }
19 25     25 1 121 sub sql { my @stuff = @_; bless sub { $_[0]->render_sql( @stuff ) }, __PACKAGE__ }
  25     25   35556  
  25         323  
20 2     2 0 8 sub sql_set { my @stuff = @_; bless sub { $_[0]->render_set( @stuff ) }, __PACKAGE__ }
  2     2   1687  
  2         15  
21 7     7 0 32 sub sql_values { my @stuff = @_; bless sub { $_[0]->render_values( @stuff ) }, __PACKAGE__ }
  7     7   6516  
  7         62  
22 24     24 0 102 sub sql_select { my @stuff = @_; bless sub { $_[0]->render_select( @stuff ) }, __PACKAGE__ }
  24     24   32046  
  24         279  
23              
24             package SQL::Concrete::Renderer;
25             $SQL::Concrete::Renderer::VERSION = '1.003';
26 1     1   1070 use Object::Tiny::Lvalue qw( alias_id prev_item bind );
  1         557  
  1         7  
27              
28             # our code references are blessed into this package
29             # so that we can distinguish them from other code references
30             sub _CODE_() { 'SQL::Concrete' }
31              
32 59     59   126 sub new { my $class = shift; bless { @_ }, $class }
  59         382  
33              
34             sub render {
35 59     59   102 my $self = shift;
36 59         211 local $self->{'bind'} = [];
37 59         229 local $self->{'alias_id'} = 0;
38 59         197 my $sql = $self->render_sql( @_ );
39 54         80 return ( $sql, @{ $self->bind } );
  54         2446  
40             }
41              
42             sub render_sql {
43 84     84   128 my $self = shift;
44              
45 84         135 my $sql = '';
46 84         3044 my $bind = $self->bind;
47 84         513 local $self->{'prev_item'};
48              
49 84         187 for my $item ( @_ ) {
50 96         189 my $type = ref $item;
51              
52 96 100       1346 my $append
    100          
    50          
    100          
    100          
    100          
    100          
53             = ( not $type ) ? $self->prev_item = $item
54             : ( 'SCALAR' eq $type ) ? $self->render_bind( $$item )
55             : ( 'ARRAY' eq $type ) ? ( @$item ? join ', ', $self->bind_or_render_values( @$item ) : $self->error( 'empty array' ) )
56             : ( _CODE_ eq $type ) ? $item->( $self )
57             : ( 'HASH' eq $type ) ? ( keys %$item ? undef : '1=1' ) # further handled below
58             : $self->error( "unrecognized $type value in interpolation" );
59              
60 92 100       1916 if ( not defined $append ) { # 'twas a non-empty hash
61 14         20 $append = join ' AND ', map {
62 10         56 my $lft = $_;
63 14         22 my $rgt = $item->{ $lft };
64 14         25 my $type = ref $rgt;
65             my $term
66             = ( not defined $rgt ) ? $lft . ' IS NULL'
67             : ( not $type ) ? join( '=', $lft, $self->bind_or_render_values( $rgt ) )
68             : ( _CODE_ eq $type ) ? $lft . '=' . $rgt->( $self )
69 14 100       80 : ( 'ARRAY' eq $type ) ? do {
    100          
    100          
    100          
70 6   66     40 my $list = @$rgt && join ', ', $self->bind_or_render_values( @$rgt );
71 6 100       28 @$rgt ? "$lft IN ($list)" : '1 IN (0)';
72             }
73             : $self->error( "unrecognized $type value for key '$lft' in hash" );
74 13         45 $term;
75             } sort keys %$item;
76 9 100       40 $append = "($append)" if keys %$item > 1;
77             }
78              
79 91 100 100     277 $sql .= '1 IN (1)', next if '1 IN (0)' eq $append and $sql =~ s/\bNOT\s*\z//i;
80 90 100 66     424 $sql .= ' ' if $sql =~ /\S/ and $append !~ /\A\s/ and $sql !~ /=\z/;
      100        
81 90         389 $sql .= $append;
82             }
83              
84 79         317 return $sql;
85             }
86              
87             sub bind_or_render_values {
88 47     47   70 my $self = shift;
89 74         126 map {
90 47         105 my $type = ref;
91 74 50 66     237 $self->error( "unrecognized $type value in aggregate" ) if $type and _CODE_ ne $type;
92 74 100       226 $type ? $_->( $self ) : $self->render_bind( $_ );
93             } @_;
94             }
95              
96 67     67   80 sub render_bind { push @{ $_[0]{'bind'} }, $_[1]; '?' }
  67         201  
  67         288  
97              
98             sub render_set {
99 2     2   4 my $self = shift;
100 2 50       8 $self->error( 'empty SET' ) if not @_;
101 2         7 my %h = @_;
102 2         13 my @k = sort keys %h;
103 2         7 my @v = $self->bind_or_render_values( @h{ @k } );
104 2         9 my $list = join ', ', map { "$k[$_]=$v[$_]" } 0 .. $#k;
  4         14  
105 2         11 "SET $list";
106             }
107              
108             sub render_values {
109 7     7   12 my $self = shift;
110 7         10 my ( $item ) = @_;
111 7         15 my $type = ref $item;
112 7         10 my $columns = '';
113             my @value
114             = 'ARRAY' eq $type ? $self->bind_or_render_values( @$item )
115 7 50       34 : 'HASH' eq $type ? do {
    100          
116 3         22 my @key = sort keys %$item;
117 3         10 $columns = join ', ', @key;
118 3         8 $columns = "($columns) ";
119 3         14 $self->bind_or_render_values( @$item{ @key } );
120             }
121             : $self->error( "unrecognized $type value in VALUES" );
122 7         20 my $list = join ', ', @value;
123 7         88 "${columns}VALUES($list)";
124             }
125              
126             sub render_select {
127 24     24   44 my $self = shift;
128 24 100       169 my @alias = ref $_[0] ? () : shift @_;
129              
130 24 100       85 $self->error( 'empty SELECT' ) if not @_;
131              
132 23         43 my $row0 = shift @_;
133 23         51 my $type0 = ref $row0;
134              
135 23         28 my @select;
136              
137 23 100       75 if ( 'ARRAY' eq $type0 ) {
    50          
138 14 100       37 $self->error( 'empty first row in SELECT' ) if not @$row0; # improve?
139 13         29 @select = map { join ', ', $self->bind_or_render_values( @$_ ) } $row0, @_;
  15         51  
140             }
141             elsif ( 'HASH' eq $type0 ) {
142 9 100       41 $self->error( 'empty first row in SELECT' ) if not keys %$row0; # improve?
143 8         51 my @k = sort keys %$row0;
144 8         37 my @v = $self->bind_or_render_values( @$row0{ @k } );
145 12         59 @select = (
146 2         8 ( join ', ', map { "$v[$_] AS $k[$_]" } 0 .. $#k ),
147 8         34 map { join ', ', $self->bind_or_render_values( @$_{ @k } ) } @_,
148             );
149             }
150 0         0 else { $self->error( "unrecognized first row '$row0' in SELECT" ) }
151              
152 21         91 my $sql = join ' UNION ALL ', map "SELECT $_", @select;
153 21         55 $sql = "($sql)";
154              
155 21 100       61 if ( @alias ) {
156 14         99 $sql .= ' AS ';
157 14 50       494 $sql .= defined $alias[0] ? $alias[0] : 'tbl'.$self->alias_id++;
158             }
159              
160 21         152 $sql;
161             }
162              
163             sub error {
164 5     5   10 my $self = shift;
165 5         240 my $prev = $self->prev_item;
166 5 50       35 push @_, " (somewhere past '$prev')" if defined $prev;
167 5         68 require Carp;
168 5         20 local $Carp::Internal{ (_CODE_) } = 1;
169 5         11 local $Carp::Internal{ (__PACKAGE__) } = 1;
170 5         1250 Carp::croak( 'SQL::Concrete: ', @_ );
171             }
172              
173             1;
174              
175             __END__