X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FData%2FQuery%2FRenderer%2FSQL%2FNaive.pm;h=07a9d505f5780c368b44dd257e550e18297bbd4b;hb=e0fb8686a710e72b2348c6637f61ff28354ae5cf;hp=6c2375ad7fc47d850cd3a03fe2a40e56d513c9b7;hpb=dc657ce1c330949f697e55ca708f0da7c924ff69;p=dbsrgits%2FData-Query.git diff --git a/lib/Data/Query/Renderer/SQL/Naive.pm b/lib/Data/Query/Renderer/SQL/Naive.pm index 6c2375a..07a9d50 100644 --- a/lib/Data/Query/Renderer/SQL/Naive.pm +++ b/lib/Data/Query/Renderer/SQL/Naive.pm @@ -1,25 +1,31 @@ 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_LITERAL +use Data::Query::ExprHelpers; + +use Moo; + +has reserved_ident_parts => ( + is => 'ro', default => sub { + our $_DEFAULT_RESERVED ||= { map +($_ => 1), SQL::ReservedWords->words } + } ); -sub new { - bless({ %{$_[1]||{}} }, (ref($_[0])||$_[0]))->BUILDALL; -} +has quote_chars => (is => 'ro', default => sub { [''] }); -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 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 { +{ @@ -70,9 +76,12 @@ sub _flatten_structure { # # FEH. -sub _format_keyword { $_[0]->{lc_keywords} ? lc($_[1]) : $_[1] } +sub _format_keyword { $_[0]->lc_keywords ? lc($_[1]) : $_[1] } sub _render { + unless (ref($_[1]) eq 'HASH') { + die "Expected hashref, got ".(defined($_[1])?$_[1]:'undef'); + } $_[0]->${\"_render_${\(lc($_[1]->{type})||'broken')}"}($_[1]); } @@ -86,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 : $_ ) @@ -105,9 +114,7 @@ sub _render_identifier { } sub _render_value { - defined($_[1]->{value}) - ? [ '?', $_[1] ] - : [ 'NULL' ]; + [ '?', $_[1] ] } sub _operator_type { 'SQL.Naive' } @@ -119,7 +126,7 @@ sub _render_operator { $op->{$self->_operator_type} = $self->_convert_op($dq); } my $op_name = $op->{$self->_operator_type}; - if (my $op_type = $self->{simple_ops}{$op_name}) { + 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); @@ -134,15 +141,24 @@ sub _render_operator { die "Unsure how to handle ${op_name}"; } +sub _maybe_parenthesise { + my ($self, $dq) = @_; + for ($dq) { + return is_Select() || is_Group() || is_Slice() || is_Having() + ? [ '(', $self->_render($dq), ')' ] + : $self->_render($dq); + } +} + sub _handle_op_type_binop { my ($self, $op_name, $dq) = @_; die "${op_name} registered as binary op but args contain " .scalar(@{$dq->{args}})." entries" unless @{$dq->{args}} == 2; [ - $self->_render($dq->{args}[0]), + $self->_maybe_parenthesise($dq->{args}[0]), $op_name, - $self->_render($dq->{args}[1]), + $self->_maybe_parenthesise($dq->{args}[1]), ] } @@ -176,7 +192,7 @@ sub _handle_op_type_flatten { my @arg_final; while (my $arg = shift @argq) { - unless ($arg->{type} eq DQ_OPERATOR) { + unless (is_Operator($arg)) { push @arg_final, $arg; next; } @@ -192,26 +208,33 @@ 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->_maybe_parenthesise($_), @arg_final + ), + ')' + ]; } 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 ]; + [ $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); + my ($lhs, $rhs1, $rhs2) = (map $self->_maybe_parenthesise($_), @args); [ '(', $lhs, $op_name, $rhs1, 'AND', $rhs2, ')' ]; - } elsif (@args == 2 and $args[1]->{type} eq DQ_LITERAL) { + } elsif (@args == 2 and is_Literal $args[1]) { my ($lhs, $rhs) = (map $self->_render($_), @args); [ '(', $lhs, $op_name, $rhs, ')' ]; } else { @@ -223,7 +246,7 @@ 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; + unless is_Identifier $func; 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 @@ -234,7 +257,7 @@ sub _handle_op_type_apply { }; [ "$ident(", - (map $self->_render($_), @args), + intersperse(',', map $self->_maybe_parenthesise($_), @args), ')' ] } @@ -259,19 +282,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 +(is_Alias() + ? $self->_render_alias($_, $self->_format_keyword('AS')) + : $self->_render($_)), @{$dq->{select}} + ); return [ $self->_format_keyword('SELECT'), @@ -281,7 +296,7 @@ sub _render_select { ($dq->{from} ? ($self->_format_keyword('FROM'), @{$self->_render($dq->{from})}) : () - ) + ), ]; } @@ -289,15 +304,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 (is_Identifier(my $from = $dq->{from})) { + if ($from->{elements}[-1] eq $dq->{to}) { + return $self->_render($from); + } } } return [ - $self->_render($dq->{alias}), - $as || ' ', - $self->_render_identifier({ elements => [ $dq->{as} ] }) + $self->_maybe_parenthesise($dq->{from}), + $as || '', + $self->_render_identifier({ elements => [ $dq->{to} ] }) ]; } @@ -306,7 +323,7 @@ sub _render_literal { unless ($dq->{subtype} eq 'SQL') { die "Can't render non-SQL literal"; } - if ($dq->{literal}) { + if (defined($dq->{literal})) { return [ $dq->{literal}, @{$dq->{values}||[]} ]; @@ -319,18 +336,33 @@ sub _render_literal { 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 { + ',' + } + }; + my $rhs = $self->_render($right); + [ + $self->_render($left), $join, + (is_Join($right) ? ('(', $rhs, ')') : $rhs), + ($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 = (is_Group($from) ? 'HAVING' : 'WHERE'); [ ($from ? $self->_render($from) : ()), - $self->_format_keyword('WHERE'), + $self->_format_keyword($keyword), $self->_render($where) ] } @@ -340,25 +372,98 @@ sub _render_order { my @ret = ( $self->_format_keyword('ORDER BY'), $self->_render($dq->{by}), - ($dq->{direction} ? $self->_format_keyword($dq->{direction}) : ()) + ($dq->{reverse} + ? $self->_format_keyword('DESC') + : ()) ); my $from; while ($from = $dq->{from}) { - last unless $from->{type} eq DQ_ORDER; + last unless is_Order $from; $dq = $from; push @ret, ( ',', $self->_render($dq->{by}), - ($dq->{direction} ? $self->_format_keyword($dq->{direction}) : ()) + ($dq->{reverse} + ? $self->_format_keyword('DESC') + : ()) ); } unshift @ret, $self->_render($from) if $from; \@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}) : ()), + (@{$dq->{by}} + ? ( + $self->_format_keyword('GROUP BY'), + intersperse(',', map $self->_render($_), @{$dq->{by}}) + ) + : ()) + ); + \@ret; +} + sub _render_delete { my ($self, $dq) = @_; - [ $self->_format_keyword('DELETE FROM'), @{$self->_render($dq->{from})} ]; + 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;