X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FData%2FQuery%2FRenderer%2FSQL%2FNaive.pm;h=6cfce82d38c55c28b26b9d94a1615944f7a31b0f;hb=713f29aad5577b9b2def21ae437bff994c3c34b0;hp=c35da5c19d39e2c844e8834036b4bc532661eafb;hpb=bdb576cb80d2f6fb1e07bd2a91acb31ec251fdbf;p=dbsrgits%2FData-Query.git diff --git a/lib/Data/Query/Renderer/SQL/Naive.pm b/lib/Data/Query/Renderer/SQL/Naive.pm index c35da5c..6cfce82 100644 --- a/lib/Data/Query/Renderer/SQL/Naive.pm +++ b/lib/Data/Query/Renderer/SQL/Naive.pm @@ -1,31 +1,41 @@ 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 DQ_JOIN DQ_ALIAS + DQ_IDENTIFIER DQ_OPERATOR DQ_VALUE DQ_JOIN DQ_ALIAS DQ_ORDER DQ_LITERAL ); -sub new { - bless({ %{$_[1]||{}} }, (ref($_[0])||$_[0]))->BUILDALL; -} +use Moo; -sub BUILDALL { - my $self = shift; - $self->{reserved_ident_parts} - ||= ( - our $_DEFAULT_RESERVED ||= { map +($_ => 1), SQL::ReservedWords->words } - ); - $self->{quote_chars}||=['']; - $self->{simple_ops}||=$self->_default_simple_ops; - return $self; -} +has reserved_ident_parts => ( + is => 'ro', default => sub { + our $_DEFAULT_RESERVED ||= { map +($_ => 1), SQL::ReservedWords->words } + } +); + +has quote_chars => (is => 'ro', default => sub { [''] }); + +has identifier_sep => (is => 'ro', default => sub { '.' }); + +has simple_ops => (is => 'ro', builder => '_default_simple_ops'); + +has lc_keywords => (is => 'ro', default => sub { 0 }); + +has always_quote => (is => 'ro', default => sub { 0 }); 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'), } } @@ -57,12 +67,16 @@ sub _flatten_structure { ]; } -# 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})||'broken')}"}($_[1]); @@ -78,17 +92,17 @@ sub _render_identifier { die "Unidentified identifier (SQL can no has \$_)" unless my @i = @{$_[1]->{elements}}; # handle single or paired quote chars - my ($q1, $q2) = @{$_[0]->{quote_chars}}[0,-1]; - my $always_quote = $_[0]->{always_quote}; - my $res_check = $_[0]->{reserved_ident_parts}; + my ($q1, $q2) = @{$_[0]->quote_chars}[0,-1]; + my $always_quote = $_[0]->always_quote; + my $res_check = $_[0]->reserved_ident_parts; return [ join - $_[0]->{identifier_sep}||'.', + $_[0]->identifier_sep, map +( $_ eq '*' # Yes, this means you can't have a column just called '*'. ? $_ # Yes, this is a feature. Go shoot the DBA if he disagrees. : ( # reserved are stored uc, quote if non-word - $always_quote || $res_check->{+uc} || /\W/ + ($always_quote and $q1) || $res_check->{+uc} || /\W/ ? $q1.$_.$q2 : $_ ) @@ -97,7 +111,7 @@ sub _render_identifier { } sub _render_value { - [ '?', $_[1] ]; + [ '?', $_[1] ] } sub _operator_type { 'SQL.Naive' } @@ -108,10 +122,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 { @@ -132,8 +156,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, ] } @@ -159,9 +196,58 @@ sub _handle_op_type_flatten { push @arg_final, $arg; } } - my @sql = ('(', map +($self->_render($_), $op_name), @arg_final); - $sql[-1] = ')'; # replace final AND or whatever with ) - \@sql; + [ '(', + intersperse( + $self->_format_keyword($op_name), + map $self->_render($_), @arg_final + ), + ')' + ]; +} + +sub _handle_op_type_in { + my ($self, $op, $dq) = @_; + my ($lhs, @in) = @{$dq->{args}}; + [ $self->_render($lhs), + $op, + '(', + intersperse(',', map $self->_render($_), @in), + ')' + ]; +} + +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(", + intersperse(',', map $self->_render($_), @args), + ')' + ] } sub _convert_op { @@ -184,19 +270,11 @@ 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 [ - ($_->{type} eq DQ_ALIAS - ? $self->_render_alias($_, 'AS') - : $self->_render($_) - ), - ',' - ], @{$dq->{select}}; - - # we put the commas inside the [] for each entry as a hint to the pretty - # 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]}; + my @select = intersperse(',', + map +($_->{type} eq DQ_ALIAS + ? $self->_render_alias($_, $self->_format_keyword('AS')) + : $self->_render($_)), @{$dq->{select}} + ); return [ $self->_format_keyword('SELECT'), @@ -231,9 +309,15 @@ 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 { @@ -244,4 +328,93 @@ sub _render_join { [ $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)}; + unless ($set) { + die "Must have set key - names+value keys not yet tested"; + my ($names, $value) = @{$dq}{qw(names value)}; + die "Must have names and value or set" unless $names and $value; + die "names and value must be same size" unless @$names == @$value; + $set = [ map [ $names->[$_], $value->[$_] ], 0..$#$names ]; + } + 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)) + : ()) + ]; +} + +sub _render_insert { + my ($self, $dq) = @_; + my ($target, $names, $values, $returning) + = @{$dq}{qw(target names values returning)}; + unless ($values) { + die "Must have values key - sets key not yet implemented"; + } + [ $self->_format_keyword('INSERT INTO'), + $self->_render($target), + ($names + ? ('(', intersperse(',', map $self->_render($_), @$names), ')') + : ()), + $self->_format_keyword('VALUES'), + intersperse(',', + map [ '(', intersperse(',', map $self->_render($_), @$_), ')' ], + @$values + ), + ($returning + ? ($self->_format_keyword('RETURNING'), + intersperse(',', map $self->_render($_), @$returning)) + : ()), + ]; +} + 1;