X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FData%2FQuery%2FRenderer%2FSQL%2FNaive.pm;h=56cdb3b4b3648f8617369924df6cdc9c5c68db0c;hb=07b0f7e08e4306ff180223809842668e4837865b;hp=b2517b01f0a8471efbc10307a1f84e75d09069bf;hpb=7f462f860c233998b75949973bf2acb785ef2132;p=dbsrgits%2FData-Query.git diff --git a/lib/Data/Query/Renderer/SQL/Naive.pm b/lib/Data/Query/Renderer/SQL/Naive.pm index b2517b0..56cdb3b 100644 --- a/lib/Data/Query/Renderer/SQL/Naive.pm +++ b/lib/Data/Query/Renderer/SQL/Naive.pm @@ -1,31 +1,44 @@ 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_ORDER + DQ_IDENTIFIER DQ_OPERATOR DQ_VALUE DQ_JOIN DQ_ALIAS DQ_ORDER DQ_LITERAL + DQ_GROUP ); -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 }); + +has collapse_aliases => (is => 'ro', default => sub { 1 }); 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 +70,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 +95,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 +114,7 @@ sub _render_identifier { } sub _render_value { - [ '?', $_[1] ]; + [ '?', $_[1] ] } sub _operator_type { 'SQL.Naive' } @@ -108,10 +125,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 +159,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 +199,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 +273,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($_, $self->_format_keyword('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'), @@ -214,15 +295,17 @@ sub _render_alias { my ($self, $dq, $as) = @_; # FROM foo foo -> FROM foo # FROM foo.bar bar -> FROM foo.bar - if ($dq->{alias}{type} eq DQ_IDENTIFIER) { - if ($dq->{alias}{elements}[-1] eq $dq->{as}) { - return $self->_render($dq->{alias}); + if ($self->collapse_aliases) { + if ($dq->{from}{type} eq DQ_IDENTIFIER) { + if ($dq->{from}{elements}[-1] eq $dq->{to}) { + return $self->_render($dq->{from}); + } } } return [ - $self->_render($dq->{alias}), + $self->_render($dq->{from}), $as || ' ', - $self->_render_identifier({ elements => [ $dq->{as} ] }) + $self->_render_identifier({ elements => [ $dq->{to} ] }) ]; } @@ -231,25 +314,44 @@ sub _render_literal { unless ($dq->{subtype} eq 'SQL') { die "Can't render non-SQL literal"; } - return [ - $dq->{literal}, @{$dq->{values}||[]} - ]; + 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) ]; + my ($left, $right) = @{$dq}{qw(left right)}; + my $join = do { + if ($dq->{outer}) { + $self->_format_keyword(uc($dq->{outer}).' JOIN'); + } elsif ($dq->{on}) { + $self->_format_keyword('JOIN'); + } else { + ',' + } + }; + [ + $self->_render($left), $join, $self->_render($right), + ($dq->{on} + ? ($self->_format_keyword('ON'), $self->_render($dq->{on})) + : ()) + ]; } sub _render_where { my ($self, $dq) = @_; my ($from, $where) = @{$dq}{qw(from where)}; + my $keyword = ($from && $from->{type} eq DQ_GROUP) ? 'HAVING' : 'WHERE'; [ ($from ? $self->_render($from) : ()), - $self->_format_keyword('WHERE'), + $self->_format_keyword($keyword), $self->_render($where) ] } @@ -275,4 +377,74 @@ sub _render_order { \@ret; } +sub _render_group { + my ($self, $dq) = @_; + # this could also squash like order does. but I dunno whether that should + # move somewhere else just yet. + my @ret = ( + ($dq->{from} ? $self->_render($dq->{from}) : ()), + $self->_format_keyword('GROUP BY'), + intersperse(',', map $self->_render($_), @{$dq->{by}}) + ); + \@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;