initial Mooification
[dbsrgits/Data-Query.git] / lib / Data / Query / Renderer / SQL / Naive.pm
1 package Data::Query::Renderer::SQL::Naive;
2
3 use strictures 1;
4
5 sub intersperse { my $i = shift; my @i = map +($_, $i), @_; pop @i; @i }
6
7 use SQL::ReservedWords;
8 use Data::Query::Constants qw(
9   DQ_IDENTIFIER DQ_OPERATOR DQ_VALUE DQ_JOIN DQ_ALIAS DQ_ORDER DQ_LITERAL
10 );
11
12 use Moo;
13
14 has reserved_ident_parts => (
15   is => 'ro', default => sub {
16     our $_DEFAULT_RESERVED ||= { map +($_ => 1), SQL::ReservedWords->words }
17   }
18 );
19
20 has quote_chars => (is => 'ro', default => sub { [''] });
21
22 has simple_ops => (is => 'ro', builder => '_default_simple_ops');
23
24 sub _default_simple_ops {
25   +{
26     (map +($_ => 'binop'), qw(= > < >= <= != LIKE), 'NOT LIKE' ),
27     (map +($_ => 'unop'), qw(NOT) ),
28     (map +($_ => 'unop_reverse'), ('IS NULL', 'IS NOT NULL')),
29     (map +($_ => 'flatten'), qw(AND OR) ),
30     (map +($_ => 'in'), ('IN', 'NOT IN')),
31     (map +($_ => 'between'), ('BETWEEN', 'NOT BETWEEN')),
32     (apply => 'apply'),
33   }
34 }
35
36 sub render {
37   my $self = shift;
38   $self->_flatten_structure($self->_render(@_))
39 }
40
41 sub _flatten_structure {
42   my ($self, $struct) = @_;
43   my @bind;
44   [ do {
45       my @p = map {
46         my $r = ref;
47         if (!$r) { $_ }
48         elsif ($r eq 'ARRAY') {
49           my ($sql, @b) = @{$self->_flatten_structure($_)};
50           push @bind, @b;
51           $sql;
52         }
53         elsif ($r eq 'HASH') { push @bind, $_; () }
54         else { die "_flatten_structure can't handle ref type $r for $_" }
55       } @$struct;
56       join '', map {
57         ($p[$_], (($p[$_+1]||',') eq ',') ? () : (' '))
58       } 0 .. $#p;
59     },
60     @bind
61   ];
62 }
63
64 # I presented this to permit strange people to easily supply a patch to lc()
65 # their keywords, as I have heard many desire to do, lest they infect me
66 # with whatever malady caused this desire by their continued proximity for
67 # want of such a feature.
68 #
69 # Then I realised that SQL::Abstract compatibility work required it.
70 #
71 # FEH.
72
73 sub _format_keyword { $_[0]->{lc_keywords} ? lc($_[1]) : $_[1] }
74
75 sub _render {
76   $_[0]->${\"_render_${\(lc($_[1]->{type})||'broken')}"}($_[1]);
77 }
78
79 sub _render_broken {
80   my ($self, $dq) = @_;
81   require Data::Dumper::Concise;
82   die "Broken DQ entry: ".Data::Dumper::Concise::Dumper($dq);
83 }
84
85 sub _render_identifier {
86   die "Unidentified identifier (SQL can no has \$_)"
87     unless my @i = @{$_[1]->{elements}};
88   # handle single or paired quote chars
89   my ($q1, $q2) = @{$_[0]->quote_chars}[0,-1];
90   my $always_quote = $_[0]->{always_quote};
91   my $res_check = $_[0]->reserved_ident_parts;
92   return [
93     join
94       $_[0]->{identifier_sep}||'.',
95       map +(
96         $_ eq '*' # Yes, this means you can't have a column just called '*'.
97           ? $_    # Yes, this is a feature. Go shoot the DBA if he disagrees.
98           : ( # reserved are stored uc, quote if non-word
99               $always_quote || $res_check->{+uc} || /\W/
100                 ? $q1.$_.$q2
101                 : $_
102             )
103       ), @i
104   ];
105 }
106
107 sub _render_value {
108   [ '?', $_[1] ]
109 }
110
111 sub _operator_type { 'SQL.Naive' }
112
113 sub _render_operator {
114   my ($self, $dq) = @_;
115   my $op = $dq->{operator};
116   unless (exists $op->{$self->_operator_type}) {
117     $op->{$self->_operator_type} = $self->_convert_op($dq);
118   }
119   my $op_name = $op->{$self->_operator_type};
120   if (my $op_type = $self->{simple_ops}{$op_name}) {
121     return $self->${\"_handle_op_type_${op_type}"}($op_name, $dq);
122   } elsif (my $meth = $self->can("_handle_op_special_${op_name}")) {
123     return $self->$meth($dq);
124   }
125   if (my $argc = @{$dq->{args}}) {
126     if ($argc == 1) {
127       return $self->_handle_op_type_unop($op_name, $dq);
128     } elsif ($argc == 2) {
129       return $self->_handle_op_type_binop($op_name, $dq);
130     }
131   }
132   die "Unsure how to handle ${op_name}";
133 }
134
135 sub _handle_op_type_binop {
136   my ($self, $op_name, $dq) = @_;
137   die "${op_name} registered as binary op but args contain "
138       .scalar(@{$dq->{args}})." entries"
139     unless @{$dq->{args}} == 2;
140   [
141     $self->_render($dq->{args}[0]),
142     $op_name,
143     $self->_render($dq->{args}[1]),
144   ]
145 }
146
147 sub _handle_op_type_unop {
148   my ($self, $op_name, $dq) = @_;
149   die "${op_name} registered as unary op but args contain "
150       .scalar(@{$dq->{args}})." entries"
151     unless @{$dq->{args}} == 1;
152   [
153     '(',
154     $op_name,
155     $self->_render($dq->{args}[0]),
156     ')',
157   ]
158 }
159
160 sub _handle_op_type_unop_reverse {
161   my ($self, $op_name, $dq) = @_;
162   die "${op_name} registered as unary op but args contain "
163       .scalar(@{$dq->{args}})." entries"
164     unless @{$dq->{args}} == 1;
165   [
166     $self->_render($dq->{args}[0]),
167     $op_name,
168   ]
169 }
170
171 sub _handle_op_type_flatten {
172   my ($self, $op_name, $dq) = @_;
173   my @argq = @{$dq->{args}};
174   my @arg_final;
175   while (my $arg = shift @argq) {
176
177     unless ($arg->{type} eq DQ_OPERATOR) {
178       push @arg_final, $arg;
179       next;
180     }
181
182     my $op = $arg->{operator};
183     unless (exists $op->{$self->_operator_type}) {
184       $op->{$self->_operator_type} = $self->_convert_op($arg);
185     }
186   
187     if ($op->{$self->_operator_type} eq $op_name) {
188       unshift @argq, @{$arg->{args}};
189     } else {
190       push @arg_final, $arg;
191     }
192   }
193   [ '(',
194       intersperse(
195         $self->_format_keyword($op_name),
196         map $self->_render($_), @arg_final
197       ),
198     ')'
199   ];
200 }
201
202 sub _handle_op_type_in {
203   my ($self, $op, $dq) = @_;
204   my ($lhs, @in) = @{$dq->{args}};
205   [ $self->_render($lhs),
206     $op,
207     '(',
208       intersperse(',', map $self->_render($_), @in),
209     ')'
210   ];
211 }
212
213 sub _handle_op_type_between {
214   my ($self, $op_name, $dq) = @_;
215   my @args = @{$dq->{args}};
216   if (@args == 3) {
217     my ($lhs, $rhs1, $rhs2) = (map $self->_render($_), @args);
218     [ '(', $lhs, $op_name, $rhs1, 'AND', $rhs2, ')' ];
219   } elsif (@args == 2 and $args[1]->{type} eq DQ_LITERAL) {
220     my ($lhs, $rhs) = (map $self->_render($_), @args);
221     [ '(', $lhs, $op_name, $rhs, ')' ];
222   } else {
223     die "Invalid args for between: ${\scalar @args} given";
224   }
225 }
226
227 sub _handle_op_type_apply {
228   my ($self, $op_name, $dq) = @_;
229   my ($func, @args) = @{$dq->{args}};
230   die "Function name must be identifier"
231     unless $func->{type} eq DQ_IDENTIFIER;
232   my $ident = do {
233     # The problem we have here is that built-ins can't be quoted, generally.
234     # I rather wonder if things like MAX(...) need to -not- be handled as
235     # an apply and instead of something else, maybe a parenop type - but
236     # as an explicitly Naive renderer this seems like a reasonable answer.
237     local @{$self}{qw(reserved_ident_parts always_quote)};
238     $self->_render_identifier($func)->[0];
239   };
240   [
241     "$ident(",
242       intersperse(',', map $self->_render($_), @args),
243     ')'
244   ]
245 }
246
247 sub _convert_op {
248   my ($self, $dq) = @_;
249   if (my $perl_op = $dq->{'operator'}->{'Perl'}) {
250     for ($perl_op) {
251       $_ eq '==' and return '=';
252       $_ eq 'eq' and return '=';
253       $_ eq '!' and return 'NOT';
254     }
255     return uc $perl_op; # hope!
256   }
257   die "Can't convert non-perl op yet";
258 }
259
260 sub _render_select {
261   my ($self, $dq) = @_;
262   die "Empty select list" unless @{$dq->{select}};
263
264   # it is, in fact, completely valid for there to be nothing for us
265   # to project from since many databases handle 'SELECT 1;' fine
266
267   my @select = intersperse(',',
268     map +($_->{type} eq DQ_ALIAS
269            ? $self->_render_alias($_, $self->_format_keyword('AS'))
270            : $self->_render($_)), @{$dq->{select}}
271   );
272
273   return [
274     $self->_format_keyword('SELECT'),
275     \@select,
276     # if present this may be a bare FROM, a FROM+WHERE, or a FROM+WHERE+GROUP
277     # since we're the SELECT and therefore always come first, we don't care.
278     ($dq->{from}
279        ? ($self->_format_keyword('FROM'), @{$self->_render($dq->{from})})
280        : ()
281     )
282   ];
283 }
284
285 sub _render_alias {
286   my ($self, $dq, $as) = @_;
287   # FROM foo foo -> FROM foo
288   # FROM foo.bar bar -> FROM foo.bar
289   if ($dq->{alias}{type} eq DQ_IDENTIFIER) {
290     if ($dq->{alias}{elements}[-1] eq $dq->{as}) {
291       return $self->_render($dq->{alias});
292     }
293   }
294   return [
295     $self->_render($dq->{alias}),
296     $as || ' ',
297     $self->_render_identifier({ elements => [ $dq->{as} ] })
298   ];
299 }
300
301 sub _render_literal {
302   my ($self, $dq) = @_;
303   unless ($dq->{subtype} eq 'SQL') {
304     die "Can't render non-SQL literal";
305   }
306   if ($dq->{literal}) {
307     return [
308       $dq->{literal}, @{$dq->{values}||[]}
309     ];
310   } elsif ($dq->{parts}) {
311     return [ map $self->_render($_), @{$dq->{parts}} ];
312   } else {
313     die "Invalid SQL literal - neither 'literal' nor 'parts' found";
314   }
315 }
316
317 sub _render_join {
318   my ($self, $dq) = @_;
319   my ($left, $right) = @{$dq->{join}};
320   die "No support for ON yet" if $dq->{on};
321   die "No support for LEFT/RIGHT yet" if $dq->{outer};
322   [ $self->_render($left), ',', $self->_render($right) ];
323 }
324
325 sub _render_where {
326   my ($self, $dq) = @_;
327   my ($from, $where) = @{$dq}{qw(from where)};
328   [
329     ($from ? $self->_render($from) : ()),
330     $self->_format_keyword('WHERE'),
331     $self->_render($where)
332   ]
333 }
334
335 sub _render_order {
336   my ($self, $dq) = @_;
337   my @ret = (
338     $self->_format_keyword('ORDER BY'),
339     $self->_render($dq->{by}),
340     ($dq->{direction} ? $self->_format_keyword($dq->{direction}) : ())
341   );
342   my $from;
343   while ($from = $dq->{from}) {
344     last unless $from->{type} eq DQ_ORDER;
345     $dq = $from;
346     push @ret, (
347       ',',
348       $self->_render($dq->{by}),
349       ($dq->{direction} ? $self->_format_keyword($dq->{direction}) : ())
350     );
351   }
352   unshift @ret, $self->_render($from) if $from;
353   \@ret;
354 }
355
356 sub _render_delete {
357   my ($self, $dq) = @_;
358   my ($target, $where) = @{$dq}{qw(target where)};
359   [ $self->_format_keyword('DELETE FROM'), 
360     $self->_render($target),
361     ($where
362       ? ($self->_format_keyword('WHERE'), $self->_render($where))
363       : ())
364   ];
365 }
366
367 sub _render_update {
368   my ($self, $dq) = @_;
369   my ($target, $set, $where) = @{$dq}{qw(target set where)};
370   unless ($set) {
371     die "Must have set key - names+value keys not yet tested";
372     my ($names, $value) = @{$dq}{qw(names value)};
373     die "Must have names and value or set" unless $names and $value;
374     die "names and value must be same size" unless @$names == @$value;
375     $set = [ map [ $names->[$_], $value->[$_] ], 0..$#$names ];
376   }
377   my @rendered_set = intersperse(
378     ',', map [ intersperse('=', map $self->_render($_), @$_) ], @{$set}
379   );
380   [ $self->_format_keyword('UPDATE'),
381     $self->_render($target),
382     $self->_format_keyword('SET'),
383     @rendered_set,
384     ($where
385       ? ($self->_format_keyword('WHERE'), $self->_render($where))
386       : ())
387   ];
388 }
389
390 sub _render_insert {
391   my ($self, $dq) = @_;
392   my ($target, $names, $values, $returning)
393     = @{$dq}{qw(target names values returning)};
394   unless ($values) {
395     die "Must have values key - sets key not yet implemented";
396   }
397   [ $self->_format_keyword('INSERT INTO'),
398     $self->_render($target),
399     ($names
400       ? ('(', intersperse(',', map $self->_render($_), @$names), ')')
401       : ()),
402     $self->_format_keyword('VALUES'),
403     intersperse(',',
404       map [ '(', intersperse(',', map $self->_render($_), @$_), ')' ],
405         @$values
406     ),
407     ($returning
408       ? ($self->_format_keyword('RETURNING'),
409          intersperse(',', map $self->_render($_), @$returning))
410       : ()),
411   ];
412 }
413
414 1;