X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FData%2FQuery%2FRenderer%2FSQL%2FNaive.pm;h=a3e681c8e9a945b21e564e0b244a05706d0e24fe;hb=87867dadad5c03bf5c9cedb8370175b289181932;hp=aeb98d412b1e720bbe2fe6a80aa7597440bc8c3f;hpb=2cf0bb42fdab9fc5322e7d057bbc98de0918952d;p=dbsrgits%2FData-Query.git diff --git a/lib/Data/Query/Renderer/SQL/Naive.pm b/lib/Data/Query/Renderer/SQL/Naive.pm index aeb98d4..a3e681c 100644 --- a/lib/Data/Query/Renderer/SQL/Naive.pm +++ b/lib/Data/Query/Renderer/SQL/Naive.pm @@ -1,8 +1,13 @@ package Data::Query::Renderer::SQL::Naive; use strictures 1; + +sub intersperse { my $i = shift; my @i = map +($_, $i), @_; pop @i; @i } + use SQL::ReservedWords; -use Data::Query::Constants qw(DQ_IDENTIFIER DQ_OPERATOR DQ_VALUE); +use Data::Query::Constants qw( + DQ_IDENTIFIER DQ_OPERATOR DQ_VALUE DQ_JOIN DQ_ALIAS DQ_ORDER DQ_LITERAL +); sub new { bless({ %{$_[1]||{}} }, (ref($_[0])||$_[0]))->BUILDALL; @@ -21,9 +26,13 @@ sub BUILDALL { sub _default_simple_ops { +{ - (map +($_ => 'binop'), qw(= > < >= <=) ), - (map +($_ => 'unop'), (qw(NOT)) ), + (map +($_ => 'binop'), qw(= > < >= <= != LIKE), 'NOT LIKE' ), + (map +($_ => 'unop'), qw(NOT) ), + (map +($_ => 'unop_reverse'), ('IS NULL', 'IS NOT NULL')), (map +($_ => 'flatten'), qw(AND OR) ), + (map +($_ => 'in'), ('IN', 'NOT IN')), + (map +($_ => 'between'), ('BETWEEN', 'NOT BETWEEN')), + (apply => 'apply'), } } @@ -35,28 +44,45 @@ sub render { sub _flatten_structure { my ($self, $struct) = @_; my @bind; - [ (join ' ', map { - my $r = ref; - if (!$r) { $_ } - elsif ($r eq 'ARRAY') { - my ($sql, @b) = @{$self->_flatten_structure($_)}; - push @bind, @b; - $sql; - } - elsif ($r eq 'HASH') { push @bind, $_; () } - else { die "_flatten_structure can't handle ref type $r for $_" } - } @$struct), @bind ]; + [ do { + my @p = map { + my $r = ref; + if (!$r) { $_ } + elsif ($r eq 'ARRAY') { + my ($sql, @b) = @{$self->_flatten_structure($_)}; + push @bind, @b; + $sql; + } + elsif ($r eq 'HASH') { push @bind, $_; () } + else { die "_flatten_structure can't handle ref type $r for $_" } + } @$struct; + join '', map { + ($p[$_], (($p[$_+1]||',') eq ',') ? () : (' ')) + } 0 .. $#p; + }, + @bind + ]; } -# I present this to permit strange people to easily supply a patch to lc() +# I presented this to permit strange people to easily supply a patch to lc() # their keywords, as I have heard many desire to do, lest they infect me # with whatever malady caused this desire by their continued proximity for # want of such a feature. +# +# Then I realised that SQL::Abstract compatibility work required it. +# +# FEH. -sub _format_keyword { $_[1] } +sub _format_keyword { $_[0]->{lc_keywords} ? lc($_[1]) : $_[1] } sub _render { - $_[0]->${\"_render_${\lc($_[1]->{type})}"}($_[1]); + $_[0]->${\"_render_${\(lc($_[1]->{type})||'broken')}"}($_[1]); +} + +sub _render_broken { + my ($self, $dq) = @_; + require Data::Dumper::Concise; + die "Broken DQ entry: ".Data::Dumper::Concise::Dumper($dq); } sub _render_identifier { @@ -82,7 +108,9 @@ sub _render_identifier { } sub _render_value { - [ '?', $_[1] ]; + defined($_[1]->{value}) + ? [ '?', $_[1] ] + : [ 'NULL' ]; } sub _operator_type { 'SQL.Naive' } @@ -93,10 +121,20 @@ sub _render_operator { unless (exists $op->{$self->_operator_type}) { $op->{$self->_operator_type} = $self->_convert_op($dq); } - if (my $op_type = $self->{simple_ops}{my $op_name = $op->{$self->_operator_type}}) { + my $op_name = $op->{$self->_operator_type}; + if (my $op_type = $self->{simple_ops}{$op_name}) { return $self->${\"_handle_op_type_${op_type}"}($op_name, $dq); + } elsif (my $meth = $self->can("_handle_op_special_${op_name}")) { + return $self->$meth($dq); } - die "Couldn't render operator ".$op->{$self->_operator_type}; + if (my $argc = @{$dq->{args}}) { + if ($argc == 1) { + return $self->_handle_op_type_unop($op_name, $dq); + } elsif ($argc == 2) { + return $self->_handle_op_type_binop($op_name, $dq); + } + } + die "Unsure how to handle ${op_name}"; } sub _handle_op_type_binop { @@ -117,8 +155,21 @@ sub _handle_op_type_unop { .scalar(@{$dq->{args}})." entries" unless @{$dq->{args}} == 1; [ - [ $op_name ], + '(', + $op_name, $self->_render($dq->{args}[0]), + ')', + ] +} + +sub _handle_op_type_unop_reverse { + my ($self, $op_name, $dq) = @_; + die "${op_name} registered as unary op but args contain " + .scalar(@{$dq->{args}})." entries" + unless @{$dq->{args}} == 1; + [ + $self->_render($dq->{args}[0]), + $op_name, ] } @@ -149,6 +200,48 @@ sub _handle_op_type_flatten { \@sql; } +sub _handle_op_type_in { + my ($self, $op, $dq) = @_; + my ($lhs, @in) = @{$dq->{args}}; + my @rhs = ('(', map +($self->_render($_), ','), @in); + $rhs[-1] = ')'; + [ $self->_render($lhs), $op, @rhs ]; +} + +sub _handle_op_type_between { + my ($self, $op_name, $dq) = @_; + my @args = @{$dq->{args}}; + if (@args == 3) { + my ($lhs, $rhs1, $rhs2) = (map $self->_render($_), @args); + [ '(', $lhs, $op_name, $rhs1, 'AND', $rhs2, ')' ]; + } elsif (@args == 2 and $args[1]->{type} eq DQ_LITERAL) { + my ($lhs, $rhs) = (map $self->_render($_), @args); + [ '(', $lhs, $op_name, $rhs, ')' ]; + } else { + die "Invalid args for between: ${\scalar @args} given"; + } +} + +sub _handle_op_type_apply { + my ($self, $op_name, $dq) = @_; + my ($func, @args) = @{$dq->{args}}; + die "Function name must be identifier" + unless $func->{type} eq DQ_IDENTIFIER; + my $ident = do { + # The problem we have here is that built-ins can't be quoted, generally. + # I rather wonder if things like MAX(...) need to -not- be handled as + # an apply and instead of something else, maybe a parenop type - but + # as an explicitly Naive renderer this seems like a reasonable answer. + local @{$self}{qw(reserved_ident_parts always_quote)}; + $self->_render_identifier($func)->[0]; + }; + [ + "$ident(", + (map $self->_render($_), @args), + ')' + ] +} + sub _convert_op { my ($self, $dq) = @_; if (my $perl_op = $dq->{'operator'}->{'Perl'}) { @@ -169,17 +262,16 @@ sub _render_select { # it is, in fact, completely valid for there to be nothing for us # to project from since many databases handle 'SELECT 1;' fine - my @select = map { - # we should perhaps validate that what we've been handed - # is an expression and possibly an identifier - at least a - # debugging mode that does such is almost certainly worthwhile; - # but for present I'm focusing on making this work. - my $e = $self->_render($_->{expr}); - $_->{name} ? [ $e, 'AS', $self->_render($_->{name}), ',' ] : [ $e, ',' ] - } @{$dq->{select}}; + my @select = map [ + ($_->{type} eq DQ_ALIAS + ? $self->_render_alias($_, $self->_format_keyword('AS')) + : $self->_render($_) + ), + ',' + ], @{$dq->{select}}; # we put the commas inside the [] for each entry as a hint to the pretty - # printer downstreamso now we need to eliminate the comma from the last + # printer downstream so now we need to eliminate the comma from the last # entry - we know there always is one due to the die guard at the top pop @{$select[-1]}; @@ -197,7 +289,7 @@ sub _render_select { } sub _render_alias { - my ($self, $dq) = @_; + my ($self, $dq, $as) = @_; # FROM foo foo -> FROM foo # FROM foo.bar bar -> FROM foo.bar if ($dq->{alias}{type} eq DQ_IDENTIFIER) { @@ -207,7 +299,7 @@ sub _render_alias { } return [ $self->_render($dq->{alias}), - ' ', + $as || ' ', $self->_render_identifier({ elements => [ $dq->{as} ] }) ]; } @@ -217,8 +309,80 @@ sub _render_literal { unless ($dq->{subtype} eq 'SQL') { die "Can't render non-SQL literal"; } - return [ - $dq->{literal}, + if ($dq->{literal}) { + return [ + $dq->{literal}, @{$dq->{values}||[]} + ]; + } elsif ($dq->{parts}) { + return [ map $self->_render($_), @{$dq->{parts}} ]; + } else { + die "Invalid SQL literal - neither 'literal' nor 'parts' found"; + } +} + +sub _render_join { + my ($self, $dq) = @_; + my ($left, $right) = @{$dq->{join}}; + die "No support for ON yet" if $dq->{on}; + die "No support for LEFT/RIGHT yet" if $dq->{outer}; + [ $self->_render($left), ',', $self->_render($right) ]; +} + +sub _render_where { + my ($self, $dq) = @_; + my ($from, $where) = @{$dq}{qw(from where)}; + [ + ($from ? $self->_render($from) : ()), + $self->_format_keyword('WHERE'), + $self->_render($where) + ] +} + +sub _render_order { + my ($self, $dq) = @_; + my @ret = ( + $self->_format_keyword('ORDER BY'), + $self->_render($dq->{by}), + ($dq->{direction} ? $self->_format_keyword($dq->{direction}) : ()) + ); + my $from; + while ($from = $dq->{from}) { + last unless $from->{type} eq DQ_ORDER; + $dq = $from; + push @ret, ( + ',', + $self->_render($dq->{by}), + ($dq->{direction} ? $self->_format_keyword($dq->{direction}) : ()) + ); + } + unshift @ret, $self->_render($from) if $from; + \@ret; +} + +sub _render_delete { + my ($self, $dq) = @_; + my ($target, $where) = @{$dq}{qw(target where)}; + [ $self->_format_keyword('DELETE FROM'), + $self->_render($target), + ($where + ? ($self->_format_keyword('WHERE'), $self->_render($where)) + : ()) + ]; +} + +sub _render_update { + my ($self, $dq) = @_; + my ($target, $set, $where) = @{$dq}{qw(target set where)}; + my @rendered_set = intersperse( + ',', map [ intersperse('=', map $self->_render($_), @$_) ], @{$set} + ); + [ $self->_format_keyword('UPDATE'), + $self->_render($target), + $self->_format_keyword('SET'), + @rendered_set, + ($where + ? ($self->_format_keyword('WHERE'), $self->_render($where)) + : ()) ]; }