X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FSQLAHacks.pm;h=3cf750c151dae47b5a27cfb26a1005286950d1cb;hb=f8583f8f51fd7ac8eeb348ad390219152a8a3bf8;hp=d2f96bf2156328c16db3e03f5eae4219cd371aee;hpb=db56cf3d3805161f7b1694c589397b0c4ebcc116;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/SQLAHacks.pm b/lib/DBIx/Class/SQLAHacks.pm index d2f96bf..3cf750c 100644 --- a/lib/DBIx/Class/SQLAHacks.pm +++ b/lib/DBIx/Class/SQLAHacks.pm @@ -1,9 +1,46 @@ package # Hide from PAUSE - DBIx::Class::SQLAHacks; # Would merge upstream, but nate doesn't reply :( + DBIx::Class::SQLAHacks; + +# This module is a subclass of SQL::Abstract::Limit and includes a number +# of DBIC-specific workarounds, not yet suitable for inclusion into the +# SQLA core use base qw/SQL::Abstract::Limit/; -use Carp::Clan qw/^DBIx::Class/; +use strict; +use warnings; +use List::Util 'first'; +use Sub::Name 'subname'; +use namespace::clean; +use Carp::Clan qw/^DBIx::Class|^SQL::Abstract|^Try::Tiny/; + +BEGIN { + # reinstall the carp()/croak() functions imported into SQL::Abstract + # as Carp and Carp::Clan do not like each other much + no warnings qw/redefine/; + no strict qw/refs/; + for my $f (qw/carp croak/) { + + my $orig = \&{"SQL::Abstract::$f"}; + *{"SQL::Abstract::$f"} = subname "SQL::Abstract::$f" => + sub { + if (Carp::longmess() =~ /DBIx::Class::SQLAHacks::[\w]+ .+? called \s at/x) { + __PACKAGE__->can($f)->(@_); + } + else { + goto $orig; + } + }; + } +} +# the "oh noes offset/top without limit" constant +# limited to 32 bits for sanity (and since it is fed +# to sprintf %u) +sub __max_int { 0xFFFFFFFF }; + + +# Tries to determine limit dialect. +# sub new { my $self = shift->SUPER::new(@_); @@ -16,248 +53,674 @@ sub new { $self; } +# !!! THIS IS ALSO HORRIFIC !!! /me ashamed +# +# Generates inner/outer select lists for various limit dialects +# which result in one or more subqueries (e.g. RNO, Top, RowNum) +# Any non-root-table columns need to have their table qualifier +# turned into a column alias (otherwise names in subqueries clash +# and/or lose their source table) +# +# Returns inner/outer strings of SQL QUOTED selectors with aliases +# (to be used in whatever select statement), and an alias index hashref +# of QUOTED SEL => QUOTED ALIAS pairs (to maybe be used for string-subst +# higher up). +# If an order_by is supplied, the inner select needs to bring out columns +# used in implicit (non-selected) orders, and the order condition itself +# needs to be realiased to the proper names in the outer query. Thus we +# also return a hashref (order doesn't matter) of QUOTED EXTRA-SEL => +# QUOTED ALIAS pairs, which is a list of extra selectors that do *not* +# exist in the original select list + +sub _subqueried_limit_attrs { + my ($self, $rs_attrs) = @_; + + croak 'Limit dialect implementation usable only in the context of DBIC (missing $rs_attrs)' + unless ref ($rs_attrs) eq 'HASH'; + + my ($re_sep, $re_alias) = map { quotemeta $_ } ( + $self->name_sep || '.', + $rs_attrs->{alias}, + ); + + # correlate select and as, build selection index + my (@sel, $in_sel_index); + for my $i (0 .. $#{$rs_attrs->{select}}) { + + my $s = $rs_attrs->{select}[$i]; + my $sql_sel = $self->_recurse_fields ($s); + my $sql_alias = (ref $s) eq 'HASH' ? $s->{-as} : undef; + + + push @sel, { + sql => $sql_sel, + unquoted_sql => do { local $self->{quote_char}; $self->_recurse_fields ($s) }, + as => + $sql_alias + || + $rs_attrs->{as}[$i] + || + croak "Select argument $i ($s) without corresponding 'as'" + , + }; + $in_sel_index->{$sql_sel}++; + $in_sel_index->{$self->_quote ($sql_alias)}++ if $sql_alias; -# Some databases (sqlite) do not handle multiple parenthesis -# around in/between arguments. A tentative x IN ( ( 1, 2 ,3) ) -# is interpreted as x IN 1 or something similar. -# -# Since we currently do not have access to the SQLA AST, resort -# to barbaric mutilation of any SQL supplied in literal form + # record unqualified versions too, so we do not have + # to reselect the same column twice (in qualified and + # unqualified form) + if (! ref $s && $sql_sel =~ / $re_sep (.+) $/x) { + $in_sel_index->{$1}++; + } + } -sub _strip_outer_paren { - my ($self, $arg) = @_; - return $self->_SWITCH_refkind ($arg, { - ARRAYREFREF => sub { - $$arg->[0] = __strip_outer_paren ($$arg->[0]); - return $arg; - }, - SCALARREF => sub { - return \__strip_outer_paren( $$arg ); - }, - FALLBACK => sub { - return $arg - }, - }); -} - -sub __strip_outer_paren { - my $sql = shift; - - if ($sql and not ref $sql) { - while ($sql =~ /^ \s* \( (.*) \) \s* $/x ) { - $sql = $1; + # re-alias and remove any name separators from aliases, + # unless we are dealing with the current source alias + # (which will transcend the subqueries as it is necessary + # for possible further chaining) + my (@in_sel, @out_sel, %renamed); + for my $node (@sel) { + if (first { $_ =~ / (?{as}, $node->{unquoted_sql}) ) { + $node->{as} = $self->_unqualify_colname($node->{as}); + my $quoted_as = $self->_quote($node->{as}); + push @in_sel, sprintf '%s AS %s', $node->{sql}, $quoted_as; + push @out_sel, $quoted_as; + $renamed{$node->{sql}} = $quoted_as; } + else { + push @in_sel, $node->{sql}; + push @out_sel, $self->_quote ($node->{as}); + } + } + + # see if the order gives us anything + my %extra_order_sel; + for my $chunk ($self->_order_by_chunks ($rs_attrs->{order_by})) { + # order with bind + $chunk = $chunk->[0] if (ref $chunk) eq 'ARRAY'; + $chunk =~ s/\s+ (?: ASC|DESC ) \s* $//ix; + + next if $in_sel_index->{$chunk}; + + $extra_order_sel{$chunk} ||= $self->_quote ( + 'ORDER__BY__' . scalar keys %extra_order_sel + ); } + return ( + (map { join (', ', @$_ ) } ( + \@in_sel, + \@out_sel) + ), + \%renamed, + keys %extra_order_sel ? \%extra_order_sel : (), + ); +} + +sub _unqualify_colname { + my ($self, $fqcn) = @_; + my $re_sep = quotemeta($self->name_sep || '.'); + $fqcn =~ s/ $re_sep /__/xg; + return $fqcn; +} + +# ANSI standard Limit/Offset implementation. DB2 and MSSQL >= 2005 use this +sub _RowNumberOver { + my ($self, $sql, $rs_attrs, $rows, $offset ) = @_; + + # mangle the input sql as we will be replacing the selector + $sql =~ s/^ \s* SELECT \s+ .+? \s+ (?= \b FROM \b )//ix + or croak "Unrecognizable SELECT: $sql"; + + # get selectors, and scan the order_by (if any) + my ($in_sel, $out_sel, $alias_map, $extra_order_sel) + = $self->_subqueried_limit_attrs ( $rs_attrs ); + + # make up an order if none exists + my $requested_order = (delete $rs_attrs->{order_by}) || $self->_rno_default_order; + my $rno_ord = $self->_order_by ($requested_order); + + # this is the order supplement magic + my $mid_sel = $out_sel; + if ($extra_order_sel) { + for my $extra_col (sort + { $extra_order_sel->{$a} cmp $extra_order_sel->{$b} } + keys %$extra_order_sel + ) { + $in_sel .= sprintf (', %s AS %s', + $extra_col, + $extra_order_sel->{$extra_col}, + ); + + $mid_sel .= ', ' . $extra_order_sel->{$extra_col}; + } + } + + # and this is order re-alias magic + for ($extra_order_sel, $alias_map) { + for my $col (keys %$_) { + my $re_col = quotemeta ($col); + $rno_ord =~ s/$re_col/$_->{$col}/; + } + } + + # whatever is left of the order_by (only where is processed at this point) + my $group_having = $self->_parse_rs_attrs($rs_attrs); + + my $qalias = $self->_quote ($rs_attrs->{alias}); + my $idx_name = $self->_quote ('rno__row__index'); + + $sql = sprintf (<_strip_outer_paren ($rhs); - return $self->SUPER::_where_field_IN ($lhs, $op, $rhs); +# some databases are happy with OVER (), some need OVER (ORDER BY (SELECT (1)) ) +sub _rno_default_order { + return undef; } -sub _where_field_BETWEEN { - my ($self, $lhs, $op, $rhs) = @_; - $rhs = $self->_strip_outer_paren ($rhs); - return $self->SUPER::_where_field_BETWEEN ($lhs, $op, $rhs); +# Informix specific limit, almost like LIMIT/OFFSET +sub _SkipFirst { + my ($self, $sql, $rs_attrs, $rows, $offset) = @_; + + $sql =~ s/^ \s* SELECT \s+ //ix + or croak "Unrecognizable SELECT: $sql"; + + return sprintf ('SELECT %s%s%s%s', + $offset + ? sprintf ('SKIP %u ', $offset) + : '' + , + sprintf ('FIRST %u ', $rows), + $sql, + $self->_parse_rs_attrs ($rs_attrs), + ); } +# Firebird specific limit, reverse of _SkipFirst for Informix +sub _FirstSkip { + my ($self, $sql, $rs_attrs, $rows, $offset) = @_; + + $sql =~ s/^ \s* SELECT \s+ //ix + or croak "Unrecognizable SELECT: $sql"; + + return sprintf ('SELECT %s%s%s%s', + sprintf ('FIRST %u ', $rows), + $offset + ? sprintf ('SKIP %u ', $offset) + : '' + , + $sql, + $self->_parse_rs_attrs ($rs_attrs), + ); +} +# WhOracle limits +sub _RowNum { + my ( $self, $sql, $rs_attrs, $rows, $offset ) = @_; -# DB2 is the only remaining DB using this. Even though we are not sure if -# RowNumberOver is still needed here (should be part of SQLA) leave the -# code in place -sub _RowNumberOver { - my ($self, $sql, $order, $rows, $offset ) = @_; + # mangle the input sql as we will be replacing the selector + $sql =~ s/^ \s* SELECT \s+ .+? \s+ (?= \b FROM \b )//ix + or croak "Unrecognizable SELECT: $sql"; + + my ($insel, $outsel) = $self->_subqueried_limit_attrs ($rs_attrs); + + my $qalias = $self->_quote ($rs_attrs->{alias}); + my $idx_name = $self->_quote ('rownum__index'); + my $order_group_having = $self->_parse_rs_attrs($rs_attrs); + + $sql = sprintf (<_subqueried_limit_attrs ($rs_attrs); + + my $requested_order = delete $rs_attrs->{order_by}; + + my $order_by_requested = $self->_order_by ($requested_order); + + # make up an order unless supplied + my $inner_order = ($order_by_requested + ? $requested_order + : [ map + { join ('', $rs_attrs->{alias}, $self->{name_sep}||'.', $_ ) } + ( $rs_attrs->{_rsroot_source_handle}->resolve->_pri_cols ) + ] + ); + + my ($order_by_inner, $order_by_reversed); + + # localise as we already have all the bind values we need + { + local $self->{order_bind}; + $order_by_inner = $self->_order_by ($inner_order); + + my @out_chunks; + for my $ch ($self->_order_by_chunks ($inner_order)) { + $ch = $ch->[0] if ref $ch eq 'ARRAY'; + + $ch =~ s/\s+ ( ASC|DESC ) \s* $//ix; + my $dir = uc ($1||'ASC'); + + push @out_chunks, \join (' ', $ch, $dir eq 'ASC' ? 'DESC' : 'ASC' ); + } + + $order_by_reversed = $self->_order_by (\@out_chunks); + } + + # this is the order supplement magic + my $mid_sel = $out_sel; + if ($extra_order_sel) { + for my $extra_col (sort + { $extra_order_sel->{$a} cmp $extra_order_sel->{$b} } + keys %$extra_order_sel + ) { + $in_sel .= sprintf (', %s AS %s', + $extra_col, + $extra_order_sel->{$extra_col}, + ); + + $mid_sel .= ', ' . $extra_order_sel->{$extra_col}; + } + + # since whatever order bindvals there are, they will be realiased + # and need to show up in front of the entire initial inner subquery + # Unshift *from_bind* to make this happen (horrible, horrible, but + # we don't have another mechanism yet) + unshift @{$self->{from_bind}}, @{$self->{order_bind}}; + } + + # and this is order re-alias magic + for my $map ($extra_order_sel, $alias_map) { + for my $col (keys %$map) { + my $re_col = quotemeta ($col); + $_ =~ s/$re_col/$map->{$col}/ + for ($order_by_reversed, $order_by_requested); + } + } + + # generate the rest of the sql + my $grpby_having = $self->_parse_rs_attrs ($rs_attrs); + + my $quoted_rs_alias = $self->_quote ($rs_attrs->{alias}); + + $sql = sprintf ('SELECT TOP %u %s %s %s %s', + $rows + ($offset||0), + $in_sel, + $sql, + $grpby_having, + $order_by_inner, + ); + + $sql = sprintf ('SELECT TOP %u %s FROM ( %s ) %s %s', + $rows, + $mid_sel, + $sql, + $quoted_rs_alias, + $order_by_reversed, + ) if $offset; + + $sql = sprintf ('SELECT TOP %u %s FROM ( %s ) %s %s', + $rows, + $out_sel, + $sql, + $quoted_rs_alias, + $order_by_requested, + ) if ( ($offset && $order_by_requested) || ($mid_sel ne $out_sel) ); + + $sql =~ s/\s*\n\s*/ /g; # easier to read in the debugger + return $sql; +} + +# This for Sybase ASE, to use SET ROWCOUNT when there is no offset, and +# GenericSubQ otherwise. +sub _RowCountOrGenericSubQ { + my $self = shift; + my ($sql, $rs_attrs, $rows, $offset) = @_; + + return $self->_GenericSubQ(@_) if $offset; + + return sprintf <<"EOF", $rows, $sql; +SET ROWCOUNT %d +%s +SET ROWCOUNT 0 +EOF +} + +# This is the most evil limit "dialect" (more of a hack) for *really* +# stupid databases. It works by ordering the set by some unique column, +# and calculating amount of rows that have a less-er value (thus +# emulating a RowNum-like index). Of course this implies the set can +# only be ordered by a single unique columns. +sub _GenericSubQ { + my ($self, $sql, $rs_attrs, $rows, $offset) = @_; + + my $root_rsrc = $rs_attrs->{_rsroot_source_handle}->resolve; + my $root_tbl_name = $root_rsrc->name; + + # mangle the input sql as we will be replacing the selector + $sql =~ s/^ \s* SELECT \s+ .+? \s+ (?= \b FROM \b )//ix + or croak "Unrecognizable SELECT: $sql"; + + my ($order_by, @rest) = do { + local $self->{quote_char}; + $self->_order_by_chunks ($rs_attrs->{order_by}) + }; + + unless ( + $order_by + && + ! @rest + && + ( ! ref $order_by + || + ( ref $order_by eq 'ARRAY' and @$order_by == 1 ) + ) + ) { + croak ( + 'Generic Subquery Limit does not work on resultsets without an order, or resultsets ' + . 'with complex order criteria (multicolumn and/or functions). Provide a single, ' + . 'unique-column order criteria.' + ); + } + + ($order_by) = @$order_by if ref $order_by; + + $order_by =~ s/\s+ ( ASC|DESC ) \s* $//ix; + my $direction = lc ($1 || 'asc'); + + my ($unq_sort_col) = $order_by =~ /(?:^|\.)([^\.]+)$/; + + my $inf = $root_rsrc->storage->_resolve_column_info ( + $rs_attrs->{from}, [$order_by, $unq_sort_col] + ); - $offset += 1; - my $last = $rows + $offset; - my ( $order_by ) = $self->_order_by( $order ); + my $ord_colinfo = $inf->{$order_by} || croak "Unable to determine source of order-criteria '$order_by'"; - $sql = <<"SQL"; -SELECT * FROM -( - SELECT Q1.*, ROW_NUMBER() OVER( ) AS ROW_NUM FROM ( - $sql - $order_by - ) Q1 -) Q2 -WHERE ROW_NUM BETWEEN $offset AND $last + if ($ord_colinfo->{-result_source}->name ne $root_tbl_name) { + croak "Generic Subquery Limit order criteria can be only based on the root-source '" + . $root_rsrc->source_name . "' (aliased as '$rs_attrs->{alias}')"; + } + + # make sure order column is qualified + $order_by = "$rs_attrs->{alias}.$order_by" + unless $order_by =~ /^$rs_attrs->{alias}\./; -SQL + my $is_u; + my $ucs = { $root_rsrc->unique_constraints }; + for (values %$ucs ) { + if (@$_ == 1 && "$rs_attrs->{alias}.$_->[0]" eq $order_by) { + $is_u++; + last; + } + } + croak "Generic Subquery Limit order criteria column '$order_by' must be unique (no unique constraint found)" + unless $is_u; + + my ($in_sel, $out_sel, $alias_map, $extra_order_sel) + = $self->_subqueried_limit_attrs ($rs_attrs); + + my $cmp_op = $direction eq 'desc' ? '>' : '<'; + my $count_tbl_alias = 'rownum__emulation'; + + my $order_sql = $self->_order_by (delete $rs_attrs->{order_by}); + my $group_having_sql = $self->_parse_rs_attrs($rs_attrs); + + # add the order supplement (if any) as this is what will be used for the outer WHERE + $in_sel .= ", $_" for keys %{$extra_order_sel||{}}; + + $sql = sprintf (<_quote ($_) } ( + $rs_attrs->{alias}, + $root_tbl_name, + $count_tbl_alias, + "$count_tbl_alias.$unq_sort_col", + $order_by, + )), + $offset + ? sprintf ('BETWEEN %u AND %u', $offset, $offset + $rows - 1) + : sprintf ('< %u', $rows ) + , + ); + $sql =~ s/\s*\n\s*/ /g; # easier to read in the debugger return $sql; } # While we're at it, this should make LIMIT queries more efficient, # without digging into things too deeply -use Scalar::Util 'blessed'; sub _find_syntax { my ($self, $syntax) = @_; - - # DB2 is the only remaining DB using this. Even though we are not sure if - # RowNumberOver is still needed here (should be part of SQLA) leave the - # code in place - my $dbhname = blessed($syntax) ? $syntax->{Driver}{Name} : $syntax; - if(ref($self) && $dbhname && $dbhname eq 'DB2') { - return 'RowNumberOver'; - } - - $self->{_cached_syntax} ||= $self->SUPER::_find_syntax($syntax); + return $self->{_cached_syntax} ||= $self->SUPER::_find_syntax($syntax); } +# Quotes table names, handles "limit" dialects (e.g. where rownum between x and +# y) sub select { - my ($self, $table, $fields, $where, $order, @rest) = @_; - local $self->{having_bind} = []; - local $self->{from_bind} = []; + my ($self, $table, $fields, $where, $rs_attrs, @rest) = @_; - if (ref $table eq 'SCALAR') { - $table = $$table; - } - elsif (not ref $table) { + if (not ref($table) or ref($table) eq 'SCALAR') { $table = $self->_quote($table); } - local $self->{rownum_hack_count} = 1 - if (defined $rest[0] && $self->{limit_dialect} eq 'RowNum'); + @rest = (-1) unless defined $rest[0]; - die "LIMIT 0 Does Not Compute" if $rest[0] == 0; + croak "LIMIT 0 Does Not Compute" if $rest[0] == 0; # and anyway, SQL::Abstract::Limit will cause a barf if we don't first - my ($sql, @where_bind) = $self->SUPER::select( - $table, $self->_recurse_fields($fields), $where, $order, @rest + + my ($sql, @bind) = $self->SUPER::select( + $table, $self->_recurse_fields($fields), $where, $rs_attrs, @rest ); - $sql .= - $self->{for} ? - ( - $self->{for} eq 'update' ? ' FOR UPDATE' : - $self->{for} eq 'shared' ? ' FOR SHARE' : - '' - ) : - '' - ; - return wantarray ? ($sql, @{$self->{from_bind}}, @where_bind, @{$self->{having_bind}}) : $sql; + push @{$self->{where_bind}}, @bind; + +# this *must* be called, otherwise extra binds will remain in the sql-maker + my @all_bind = $self->_assemble_binds; + + return wantarray ? ($sql, @all_bind) : $sql; +} + +sub _assemble_binds { + my $self = shift; + return map { @{ (delete $self->{"${_}_bind"}) || [] } } (qw/from where having order/); } +# Quotes table names, and handles default inserts sub insert { my $self = shift; my $table = shift; - $table = $self->_quote($table) unless ref($table); + $table = $self->_quote($table); + + # SQLA will emit INSERT INTO $table ( ) VALUES ( ) + # which is sadly understood only by MySQL. Change default behavior here, + # until SQLA2 comes with proper dialect support + if (! $_[0] or (ref $_[0] eq 'HASH' and !keys %{$_[0]} ) ) { + my $sql = "INSERT INTO ${table} DEFAULT VALUES"; + + if (my $ret = ($_[1]||{})->{returning} ) { + $sql .= $self->_insert_returning ($ret); + } + + return $sql; + } + $self->SUPER::insert($table, @_); } +# Just quotes table names. sub update { my $self = shift; my $table = shift; - $table = $self->_quote($table) unless ref($table); + $table = $self->_quote($table); $self->SUPER::update($table, @_); } +# Just quotes table names. sub delete { my $self = shift; my $table = shift; - $table = $self->_quote($table) unless ref($table); + $table = $self->_quote($table); $self->SUPER::delete($table, @_); } sub _emulate_limit { my $self = shift; + # my ( $syntax, $sql, $order, $rows, $offset ) = @_; + if ($_[3] == -1) { - return $_[1].$self->_order_by($_[2]); + return $_[1] . $self->_parse_rs_attrs($_[2]); } else { return $self->SUPER::_emulate_limit(@_); } } sub _recurse_fields { - my ($self, $fields, $params) = @_; + my ($self, $fields) = @_; my $ref = ref $fields; return $self->_quote($fields) unless $ref; return $$fields if $ref eq 'SCALAR'; if ($ref eq 'ARRAY') { - return join(', ', map { - $self->_recurse_fields($_) - .(exists $self->{rownum_hack_count} && !($params && $params->{no_rownum_hack}) - ? ' AS col'.$self->{rownum_hack_count}++ - : '') - } @$fields); - } elsif ($ref eq 'HASH') { - foreach my $func (keys %$fields) { - if ($func eq 'distinct') { - my $_fields = $fields->{$func}; - if (ref $_fields eq 'ARRAY' && @{$_fields} > 1) { - die "Unsupported syntax, please use " . - "{ group_by => [ qw/" . (join ' ', @$_fields) . "/ ] }" . - " or " . - "{ select => [ qw/" . (join ' ', @$_fields) . "/ ], distinct => 1 }"; - } - else { - $_fields = @{$_fields}[0] if ref $_fields eq 'ARRAY'; - carp "This syntax will be deprecated in 09, please use " . - "{ group_by => '${_fields}' }" . - " or " . - "{ select => '${_fields}', distinct => 1 }"; - } - } - - return $self->_sqlcase($func) - .'( '.$self->_recurse_fields($fields->{$func}).' )'; + return join(', ', map { $self->_recurse_fields($_) } @$fields); + } + elsif ($ref eq 'HASH') { + my %hash = %$fields; # shallow copy + + my $as = delete $hash{-as}; # if supplied + + my ($func, $args, @toomany) = %hash; + + # there should be only one pair + if (@toomany) { + croak "Malformed select argument - too many keys in hash: " . join (',', keys %$fields ); + } + + if (lc ($func) eq 'distinct' && ref $args eq 'ARRAY' && @$args > 1) { + croak ( + 'The select => { distinct => ... } syntax is not supported for multiple columns.' + .' Instead please use { group_by => [ qw/' . (join ' ', @$args) . '/ ] }' + .' or { select => [ qw/' . (join ' ', @$args) . '/ ], distinct => 1 }' + ); } + + my $select = sprintf ('%s( %s )%s', + $self->_sqlcase($func), + $self->_recurse_fields($args), + $as + ? sprintf (' %s %s', $self->_sqlcase('as'), $self->_quote ($as) ) + : '' + ); + + return $select; } # Is the second check absolutely necessary? elsif ( $ref eq 'REF' and ref($$fields) eq 'ARRAY' ) { return $self->_fold_sqlbind( $fields ); } else { - Carp::croak($ref . qq{ unexpected in _recurse_fields()}) + croak($ref . qq{ unexpected in _recurse_fields()}) } } +my $for_syntax = { + update => 'FOR UPDATE', + shared => 'FOR SHARE', +}; + +# this used to be a part of _order_by but is broken out for clarity. +# What we have been doing forever is hijacking the $order arg of +# SQLA::select to pass in arbitrary pieces of data (first the group_by, +# then pretty much the entire resultset attr-hash, as more and more +# things in the SQLA space need to have mopre info about the $rs they +# create SQL for. The alternative would be to keep expanding the +# signature of _select with more and more positional parameters, which +# is just gross. All hail SQLA2! +sub _parse_rs_attrs { + my ($self, $arg) = @_; + + my $sql = ''; + + if (my $g = $self->_recurse_fields($arg->{group_by}) ) { + $sql .= $self->_sqlcase(' group by ') . $g; + } + + if (defined $arg->{having}) { + my ($frag, @bind) = $self->_recurse_where($arg->{having}); + push(@{$self->{having_bind}}, @bind); + $sql .= $self->_sqlcase(' having ') . $frag; + } + + if (defined $arg->{order_by}) { + $sql .= $self->_order_by ($arg->{order_by}); + } + + if (my $for = $arg->{for}) { + $sql .= " $for_syntax->{$for}" if $for_syntax->{$for}; + } + + return $sql; +} + sub _order_by { - my $self = shift; - my $ret = ''; - my @extra; - if (ref $_[0] eq 'HASH') { - if (defined $_[0]->{group_by}) { - $ret = $self->_sqlcase(' group by ') - .$self->_recurse_fields($_[0]->{group_by}, { no_rownum_hack => 1 }); - } - if (defined $_[0]->{having}) { - my $frag; - ($frag, @extra) = $self->_recurse_where($_[0]->{having}); - push(@{$self->{having_bind}}, @extra); - $ret .= $self->_sqlcase(' having ').$frag; - } - if (defined $_[0]->{order_by}) { - $ret .= $self->_order_by($_[0]->{order_by}); - } - if (grep { $_ =~ /^-(desc|asc)/i } keys %{$_[0]}) { - return $self->SUPER::_order_by($_[0]); - } - } elsif (ref $_[0] eq 'SCALAR') { - $ret = $self->_sqlcase(' order by ').${ $_[0] }; - } elsif (ref $_[0] eq 'ARRAY' && @{$_[0]}) { - my @order = @{+shift}; - $ret = $self->_sqlcase(' order by ') - .join(', ', map { - my $r = $self->_order_by($_, @_); - $r =~ s/^ ?ORDER BY //i; - $r; - } @order); - } else { - $ret = $self->SUPER::_order_by(@_); + my ($self, $arg) = @_; + + # check that we are not called in legacy mode (order_by as 4th argument) + if (ref $arg eq 'HASH' and not grep { $_ =~ /^-(?:desc|asc)/i } keys %$arg ) { + return $self->_parse_rs_attrs ($arg); + } + else { + my ($sql, @bind) = $self->SUPER::_order_by ($arg); + push @{$self->{order_bind}}, @bind; + return $sql; } - return $ret; } sub _order_directions { my ($self, $order) = @_; - $order = $order->{order_by} if ref $order eq 'HASH'; - return $self->SUPER::_order_directions($order); + + # strip bind values - none of the current _order_directions users support them + return $self->SUPER::_order_directions( [ map + { ref $_ ? $_->[0] : $_ } + $self->_order_by_chunks ($order) + ]); } sub _table { @@ -276,6 +739,14 @@ sub _table { } } +sub _generate_join_clause { + my ($self, $join_type) = @_; + + return sprintf ('%s JOIN ', + $join_type ? ' ' . uc($join_type) : '' + ); +} + sub _recurse_from { my ($self, $from, @join) = @_; my @sqlf; @@ -283,15 +754,18 @@ sub _recurse_from { foreach my $j (@join) { my ($to, $on) = @$j; + # check whether a join type exists - my $join_clause = ''; my $to_jt = ref($to) eq 'ARRAY' ? $to->[0] : $to; - if (ref($to_jt) eq 'HASH' and exists($to_jt->{-join_type})) { - $join_clause = ' '.uc($to_jt->{-join_type}).' JOIN '; - } else { - $join_clause = ' JOIN '; + my $join_type; + if (ref($to_jt) eq 'HASH' and defined($to_jt->{-join_type})) { + $join_type = $to_jt->{-join_type}; + $join_type =~ s/^\s+ | \s+$//xg; } - push(@sqlf, $join_clause); + + $join_type = $self->{_default_jointype} if not defined $join_type; + + push @sqlf, $self->_generate_join_clause( $join_type ); if (ref $to eq 'ARRAY') { push(@sqlf, '(', $self->_recurse_from(@$to), ')'); @@ -305,8 +779,11 @@ sub _recurse_from { sub _fold_sqlbind { my ($self, $sqlbind) = @_; - my $sql = shift @$$sqlbind; - push @{$self->{from_bind}}, @$$sqlbind; + + my @sqlbind = @$$sqlbind; # copy + my $sql = shift @sqlbind; + push @{$self->{from_bind}}, @sqlbind; + return $sql; } @@ -333,8 +810,7 @@ sub _join_condition { for (keys %$cond) { my $v = $cond->{$_}; if (ref $v) { - # XXX no throw_exception() in this package and croak() fails with strange results - Carp::croak(ref($v) . qq{ reference arguments are not supported in JOINS - try using \"..." instead'}) + croak (ref($v) . qq{ reference arguments are not supported in JOINS - try using \"..." instead'}) if ref($v) ne 'SCALAR'; $j{$_} = $v; } @@ -346,38 +822,28 @@ sub _join_condition { } elsif (ref $cond eq 'ARRAY') { return join(' OR ', map { $self->_join_condition($_) } @$cond); } else { - die "Can't handle this yet!"; - } -} - -sub _quote { - my ($self, $label) = @_; - return '' unless defined $label; - return "*" if $label eq '*'; - return $label unless $self->{quote_char}; - if(ref $self->{quote_char} eq "ARRAY"){ - return $self->{quote_char}->[0] . $label . $self->{quote_char}->[1] - if !defined $self->{name_sep}; - my $sep = $self->{name_sep}; - return join($self->{name_sep}, - map { $self->{quote_char}->[0] . $_ . $self->{quote_char}->[1] } - split(/\Q$sep\E/,$label)); + croak "Can't handle this yet!"; } - return $self->SUPER::_quote($label); } sub limit_dialect { my $self = shift; - $self->{limit_dialect} = shift if @_; + if (@_) { + $self->{limit_dialect} = shift; + undef $self->{_cached_syntax}; + } return $self->{limit_dialect}; } +# Set to an array-ref to specify separate left and right quotes for table names. +# A single scalar is equivalen to [ $char, $char ] sub quote_char { my $self = shift; $self->{quote_char} = shift if @_; return $self->{quote_char}; } +# Character separating quoted table names. sub name_sep { my $self = shift; $self->{name_sep} = shift if @_; @@ -385,48 +851,3 @@ sub name_sep { } 1; - -__END__ - -=pod - -=head1 NAME - -DBIx::Class::SQLAHacks - Things desired to be merged into SQL::Abstract - -=head1 METHODS - -=head2 new - -Tries to determine limit dialect. - -=head2 select - -Quotes table names, handles "limit" dialects (e.g. where rownum between x and -y), supports SELECT ... FOR UPDATE and SELECT ... FOR SHARE. - -=head2 insert update delete - -Just quotes table names. - -=head2 limit_dialect - -Specifies the dialect of used for implementing an SQL "limit" clause for -restricting the number of query results returned. Valid values are: RowNum. - -See L for details. - -=head2 name_sep - -Character separating quoted table names. - -See L for details. - -=head2 quote_char - -Set to an array-ref to specify separate left and right quotes for table names. - -See L for details. - -=cut -