From: Peter Rabbitson Date: Sat, 17 May 2014 09:39:49 +0000 (+0200) Subject: Massively refactor and sanify condition collapsing X-Git-Tag: v0.082800~213 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=8d005ad9929e4bf227919cb6374e2a9e9689324f;p=dbsrgits%2FDBIx-Class.git Massively refactor and sanify condition collapsing Wow... what a ride. This commit adds a faithful reimplementation of the SQLA descend algorithm, but instead of SQL produces a reduced HASH that nevertheless corresponds to the original query 1:1 (or so I *really* hope) This is another one of these "I can try it with DQ but I don't even have tests" stories, so once again opted to implement things the "dumb" way. The benefits are quite substantial: - Better deduplication of WHERE condition - Consolidated functions - the vaguely similar logic in the current version of _collapse_cond replaces almost the entirety of: ::DBIHacks::_extract_fixed_condition_columns ::ResultSet::_collapse_cond ::ResultSet::_stack_cond - Extra fixes for create/populate inheritance corner cases - More predictable SQL condition generation order (which incidentally may prove problematic down the road with broken tests, but oh well, we'll burn when we get there) - Ton of extra tests and corner cases We even managed to fulfill a longstanding TODO, even though it is a *lucky* side-effect and not a real fix. Making a note to address this later... sigh. This work started from d8b7d9f58..337f3ee80, which while taking the right direction had too many loose ends. ilmari++ --- diff --git a/Changes b/Changes index 784b3d1..e363637 100644 --- a/Changes +++ b/Changes @@ -6,6 +6,11 @@ Revision history for DBIx::Class routines, triggering a connection before the set-cycle is finished - Fix multi-value literal populate not working with simplified bind specifications + - Massively improve the implied resultset condition parsing - now all + applicable conditions within a resultset should be properly picked + up by create() and populate() + - Ensure definitive condition extractor handles bizarre corner cases + without bombing out (RT#93244) 0.08270 2014-01-30 21:54 (PST) * Fixes diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index b0b0e01..292dbc3 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -585,60 +585,32 @@ sub _normalize_selection { sub _stack_cond { my ($self, $left, $right) = @_; - # collapse single element top-level conditions - # (single pass only, unlikely to need recursion) - for ($left, $right) { - if (ref $_ eq 'ARRAY') { - if (@$_ == 0) { - $_ = undef; - } - elsif (@$_ == 1) { - $_ = $_->[0]; - } - } - elsif (ref $_ eq 'HASH') { - my ($first, $more) = keys %$_; + ( + (ref $_ eq 'ARRAY' and !@$_) + or + (ref $_ eq 'HASH' and ! keys %$_) + ) and $_ = undef for ($left, $right); - # empty hash - if (! defined $first) { - $_ = undef; - } - # one element hash - elsif (! defined $more) { - if ($first eq '-and' and ref $_->{'-and'} eq 'HASH') { - $_ = $_->{'-and'}; - } - elsif ($first eq '-or' and ref $_->{'-or'} eq 'ARRAY') { - $_ = $_->{'-or'}; - } - } - } + # either on of the two undef or both undef + if ( ( (defined $left) xor (defined $right) ) or ! defined $left ) { + return defined $left ? $left : $right; } - # merge hashes with weeding out of duplicates (simple cases only) - if (ref $left eq 'HASH' and ref $right eq 'HASH') { + my $cond = $self->result_source->schema->storage->_collapse_cond({ -and => [$left, $right] }); - # shallow copy to destroy - $right = { %$right }; - for (grep { exists $right->{$_} } keys %$left) { - # the use of eq_deeply here is justified - the rhs of an - # expression can contain a lot of twisted weird stuff - delete $right->{$_} if Data::Compare::Compare( $left->{$_}, $right->{$_} ); - } + for my $c (grep { ref $cond->{$_} eq 'ARRAY' and ($cond->{$_}[0]||'') eq '-and' } keys %$cond) { - $right = undef unless keys %$right; - } + my @vals = sort @{$cond->{$c}}[ 1..$#{$cond->{$c}} ]; + my @fin = shift @vals; + for my $v (@vals) { + push @fin, $v unless Data::Compare::Compare( $fin[-1], $v ); + } - if (defined $left xor defined $right) { - return defined $left ? $left : $right; - } - elsif (! defined $left) { - return undef; - } - else { - return { -and => [ $left, $right ] }; + $cond->{$c} = (@fin == 1) ? $fin[0] : [-and => @fin ]; } + + $cond; } =head2 search_literal @@ -2466,28 +2438,36 @@ sub _merge_with_rscond { ); } else { - # precedence must be given to passed values over values inherited from - # the cond, so the order here is important. - my $collapsed_cond = $self->_collapse_cond($self->{cond}); - my %implied = %{$self->_remove_alias($collapsed_cond, $alias)}; + if ($self->{cond}) { + my $implied = $self->_remove_alias( + $self->result_source->schema->storage->_collapse_cond($self->{cond}), + $alias, + ); - while ( my($col, $value) = each %implied ) { - my $vref = ref $value; - if ( - $vref eq 'HASH' - and - keys(%$value) == 1 - and - (keys %$value)[0] eq '=' - ) { - $new_data{$col} = $value->{'='}; - } - elsif( !$vref or $vref eq 'SCALAR' or blessed($value) ) { - $new_data{$col} = $value; + for my $c (keys %$implied) { + my $v = $implied->{$c}; + if ( + ! ref $v + or + overload::Method($v, '""') + ) { + $new_data{$c} = $v; + } + elsif ( + ref $v eq 'HASH' and keys %$v == 1 and exists $v->{'='} and ( + ref $v->{'='} eq 'SCALAR' + or + ( ref $v->{'='} eq 'REF' and ref ${$v->{'='}} eq 'ARRAY' ) + ) + ) { + $new_data{$c} = $v->{'='}; + } } } } + # precedence must be given to passed values over values inherited from + # the cond, so the order here is important. %new_data = ( %new_data, %{ $self->_remove_alias($data, $alias) }, @@ -2549,38 +2529,6 @@ sub _has_resolved_attr { return 0; } -# _collapse_cond -# -# Recursively collapse the condition. - -sub _collapse_cond { - my ($self, $cond, $collapsed) = @_; - - $collapsed ||= {}; - - if (ref $cond eq 'ARRAY') { - foreach my $subcond (@$cond) { - next unless ref $subcond; # -or - $collapsed = $self->_collapse_cond($subcond, $collapsed); - } - } - elsif (ref $cond eq 'HASH') { - if (keys %$cond and (keys %$cond)[0] eq '-and') { - foreach my $subcond (@{$cond->{-and}}) { - $collapsed = $self->_collapse_cond($subcond, $collapsed); - } - } - else { - foreach my $col (keys %$cond) { - my $value = $cond->{$col}; - $collapsed->{$col} = $value; - } - } - } - - return $collapsed; -} - # _remove_alias # # Remove the specified alias from the specified query hash. A copy is made so diff --git a/lib/DBIx/Class/Storage/DBIHacks.pm b/lib/DBIx/Class/Storage/DBIHacks.pm index 3c7d1c4..4147e81 100644 --- a/lib/DBIx/Class/Storage/DBIHacks.pm +++ b/lib/DBIx/Class/Storage/DBIHacks.pm @@ -709,6 +709,9 @@ sub _resolve_ident_sources { # for all sources sub _resolve_column_info { my ($self, $ident, $colnames) = @_; + + return {} if $colnames and ! @$colnames; + my $alias2src = $self->_resolve_ident_sources($ident); my (%seen_cols, @auto_colnames); @@ -880,8 +883,8 @@ sub _order_by_is_stable { my ($self, $ident, $order_by, $where) = @_; my @cols = ( - (map { $_->[0] } $self->_extract_order_criteria($order_by)), - $where ? @{$self->_extract_fixed_condition_columns($where)} :(), + ( map { $_->[0] } $self->_extract_order_criteria($order_by) ), + ( $where ? @{ $self->_extract_fixed_condition_columns($where) || [] } : () ), ) or return undef; my $colinfo = $self->_resolve_column_info($ident, \@cols); @@ -952,7 +955,7 @@ sub _main_source_order_by_portion_is_stable { my $unqualified_idset = $main_rsrc->_identifying_column_set({ ( $where ? %{ $self->_resolve_column_info( - $main_rsrc, $self->_extract_fixed_condition_columns($where) + $main_rsrc, $self->_extract_fixed_condition_columns($where)||[] ) } : () ), %$order_portion_ci @@ -978,8 +981,212 @@ sub _main_source_order_by_portion_is_stable { die 'How did we get here...'; } +# Attempts to flatten a passed in SQLA condition as much as possible towards +# a plain hashref, *without* altering its semantics. Required by +# create/populate being able to extract definitive conditions from preexisting +# resultset {where} stacks +# +# FIXME - while relatively robust, this is still imperfect, one of the first +# things to tackle with DQ +sub _collapse_cond { + my ($self, $where, $where_is_anded_array) = @_; + + if (! $where) { + return; + } + elsif ($where_is_anded_array or ref $where eq 'HASH') { + + my @pairs; + + my @pieces = $where_is_anded_array ? @$where : $where; + while (@pieces) { + my $chunk = shift @pieces; + + if (ref $chunk eq 'HASH') { + push @pairs, map { [ $_ => $chunk->{$_} ] } sort keys %$chunk; + } + elsif (ref $chunk eq 'ARRAY') { + push @pairs, [ -or => $chunk ] + if @$chunk; + } + elsif ( ! ref $chunk) { + push @pairs, [ $chunk, shift @pieces ]; + } + else { + push @pairs, [ '', $chunk ]; + } + } + + return unless @pairs; + + my @conds = $self->_collapse_cond_unroll_pairs(\@pairs) + or return; + + # Consolidate various @conds back into something more compact + my $fin; + + for my $c (@conds) { + if (ref $c ne 'HASH') { + push @{$fin->{-and}}, $c; + } + else { + for my $col (sort keys %$c) { + if (exists $fin->{$col}) { + my ($l, $r) = ($fin->{$col}, $c->{$col}); + + (ref $_ ne 'ARRAY' or !@$_) and $_ = [ -and => $_ ] for ($l, $r); + + if (@$l and @$r and $l->[0] eq $r->[0] and $l->[0] eq '-and') { + $fin->{$col} = [ -and => map { @$_[1..$#$_] } ($l, $r) ]; + } + else { + $fin->{$col} = [ -and => $fin->{$col}, $c->{$col} ]; + } + } + else { + $fin->{$col} = $c->{$col}; + } + } + } + } + + if ( ref $fin->{-and} eq 'ARRAY' and @{$fin->{-and}} == 1 ) { + my $piece = (delete $fin->{-and})->[0]; + if (ref $piece eq 'ARRAY') { + $fin->{-or} = $fin->{-or} ? [ $piece, $fin->{-or} ] : $piece; + } + elsif (! exists $fin->{''}) { + $fin->{''} = $piece; + } + } + + return $fin; + } + elsif (ref $where eq 'ARRAY') { + my @w = @$where; + + while ( @w and ( + (ref $w[0] eq 'ARRAY' and ! @{$w[0]} ) + or + (ref $w[0] eq 'HASH' and ! keys %{$w[0]}) + )) { shift @w }; + + return unless @w; + + if ( @w == 1 ) { + return ( ref $w[0] ) + ? $self->_collapse_cond($w[0]) + : { $w[0] => undef } + ; + } + elsif ( ref $w[0] ) { + return \@w; + } + elsif ( @w == 2 ) { + if ( ( $w[0]||'' ) =~ /^\-and$/i ) { + return (ref $w[1] eq 'HASH' or ref $w[1] eq 'ARRAY') + ? $self->_collapse_cond($w[1], (ref $w[1] eq 'ARRAY') ) + : $self->throw_exception("Unsupported top-level op/arg pair: [ $w[0] => $w[1] ]") + ; + } + else { + return $self->_collapse_cond({ @w }); + } + } + } + else { + # not a hash not an array + return { '' => $where }; + } + + # catchall, some of the things above fall through + return $where; +} + +sub _collapse_cond_unroll_pairs { + my ($self, $pairs) = @_; + + my @conds; + + while (@$pairs) { + my ($lhs, $rhs) = @{ shift @$pairs }; + + if ($lhs eq '') { + push @conds, $self->_collapse_cond($rhs); + } + elsif ( $lhs =~ /^\-and$/i ) { + push @conds, $self->_collapse_cond($rhs, (ref $rhs eq 'ARRAY')); + } + elsif ( $lhs =~ /^\-or$/i ) { + push @conds, $self->_collapse_cond( + (ref $rhs eq 'HASH') ? [ map { $_ => $rhs->{$_} } sort keys %$rhs ] : $rhs + ); + } + else { + if (ref $rhs eq 'HASH' and ! keys %$rhs) { + # FIXME - SQLA seems to be doing... nothing...? + } + elsif (ref $rhs eq 'HASH' and keys %$rhs == 1 and exists $rhs->{'='}) { + for my $p ($self->_collapse_cond_unroll_pairs([ [ $lhs => $rhs->{'='} ] ])) { + + # extra sanity check + if (keys %$p > 1) { + require Data::Dumper::Concise; + local $Data::Dumper::Deepcopy = 1; + $self->throw_exception( + "Internal error: unexpected collapse unroll:" + . Data::Dumper::Concise::Dumper { in => { $lhs => $rhs }, out => $p } + ); + } + + my ($l, $r) = %$p; + + push @conds, ( ! ref $r or overload::Method($r, '""' ) ) + ? { $l => $r } + : { $l => { '=' => $r } } + ; + } + } + elsif (ref $rhs eq 'ARRAY') { + # some of these conditionals encounter multi-values - roll them out using + # an unshift, which will cause extra looping in the while{} above + if (! @$rhs ) { + push @conds, { $lhs => [] }; + } + elsif ( ($rhs->[0]||'') =~ /^\-(?:and|or)$/i ) { + $self->throw_exception("Value modifier not followed by any values: $lhs => [ $rhs->[0] ] ") + if @$rhs == 1; + + if( $rhs->[0] =~ /^\-and$/i ) { + unshift @$pairs, map { [ $lhs => $_ ] } @{$rhs}[1..$#$rhs]; + } + # if not an AND then it's an OR + elsif(@$rhs == 2) { + unshift @$pairs, [ $lhs => $rhs->[1] ]; + } + else { + push @conds, { $lhs => $rhs }; + } + } + elsif (@$rhs == 1) { + unshift @$pairs, [ $lhs => $rhs->[0] ]; + } + else { + push @conds, { $lhs => $rhs }; + } + } + else { + push @conds, { $lhs => $rhs }; + } + } + } + + return @conds; +} + + # returns an arrayref of column names which *definitely* have some -# sort of non-nullable equality requested in the given condition +# sort of non-nullable *single* equality requested in the given condition # specification. This is used to figure out if a resultset is # constrained to a column which is part of a unique constraint, # which in turn allows us to better predict how ordering will behave @@ -988,31 +1195,35 @@ sub _main_source_order_by_portion_is_stable { # this is a rudimentary, incomplete, and error-prone extractor # however this is OK - it is conservative, and if we can not find # something that is in fact there - the stack will recover gracefully -# Also - DQ and the mst it rode in on will save us all RSN!!! sub _extract_fixed_condition_columns { - my ($self, $where) = @_; - - return unless ref $where eq 'HASH'; - - my @cols; - for my $lhs (keys %$where) { - if ($lhs =~ /^\-and$/i) { - push @cols, ref $where->{$lhs} eq 'ARRAY' - ? ( map { @{ $self->_extract_fixed_condition_columns($_) } } @{$where->{$lhs}} ) - : @{ $self->_extract_fixed_condition_columns($where->{$lhs}) } - ; - } - elsif ($lhs !~ /^\-/) { - my $val = $where->{$lhs}; - - push @cols, $lhs if (defined $val and ( - ! ref $val + my $self = shift; + my $where_hash = $self->_collapse_cond(shift); + + my $res; + for my $c (keys %$where_hash) { + if (defined (my $v = $where_hash->{$c}) ) { + if ( + ! ref $v or - (ref $val eq 'HASH' and keys %$val == 1 and defined $val->{'='}) - )); + (ref $v eq 'HASH' and keys %$v == 1 and defined $v->{'='} and ( + ! ref $v->{'='} + or + ref $v->{'='} eq 'SCALAR' + or + ( ref $v->{'='} eq 'REF' and ref ${$v->{'='}} eq 'ARRAY' ) + or + overload::Method($v->{'='}, '""') + )) + ) { + $res->{$c} = 1; + } + elsif (ref $v eq 'ARRAY' and ($v->[0]||'') eq '-and') { + $res->{$_} = 1 for map { @{ $self->_extract_fixed_condition_columns({ $c => $_ }) } } @{$v}[1..$#$v]; + } } } - return \@cols; + + return [ sort keys %$res ]; } 1; diff --git a/t/101populate_rs.t b/t/101populate_rs.t index 56e87f0..8a0eea4 100644 --- a/t/101populate_rs.t +++ b/t/101populate_rs.t @@ -24,7 +24,7 @@ my $schema = DBICTest->init_schema(); my $art_rs = $schema->resultset('Artist'); my $cd_rs = $schema->resultset('CD'); -my $restricted_art_rs = $art_rs->search({rank => 42}); +my $restricted_art_rs = $art_rs->search({ -and => [ rank => 42, charfield => { '=', \['(SELECT MAX(artistid) FROM artist) + ?', 6] } ] }); ok( $schema, 'Got a Schema object'); ok( $art_rs, 'Got Good Artist Resultset'); @@ -343,7 +343,9 @@ ARRAY_CONTEXT: { ]); ## Did it use the condition in the resultset? + $more_crap->discard_changes; cmp_ok( $more_crap->rank, '==', 42, "Got Correct rank for result object"); + cmp_ok( $more_crap->charfield, '==', $more_crap->id + 5, "Got Correct charfield for result object"); } } @@ -626,7 +628,9 @@ VOID_CONTEXT: { })->first; ## Did it use the condition in the resultset? + $more_crap->discard_changes; cmp_ok( $more_crap->rank, '==', 42, "Got Correct rank for result object"); + cmp_ok( $more_crap->charfield, '==', $more_crap->id + 5, "Got Correct charfield for result object"); } } @@ -655,7 +659,11 @@ ARRAYREF_OF_ARRAYREF_STYLE: { is $cooler->name, 'Cooler', 'Correct Name'; is $lamer->name, 'Lamer', 'Correct Name'; - cmp_ok $cooler->rank, '==', 42, 'Correct Rank'; + for ($cooler, $lamer) { + $_->discard_changes; + cmp_ok( $_->rank, '==', 42, "Got Correct rank for result object"); + cmp_ok( $_->charfield, '==', $_->id + 5, "Got Correct charfield for result object"); + } ARRAY_CONTEXT_WITH_COND_FROM_RS: { @@ -666,7 +674,9 @@ ARRAYREF_OF_ARRAYREF_STYLE: { ]); ## Did it use the condition in the resultset? + $mega_lamer->discard_changes; cmp_ok( $mega_lamer->rank, '==', 42, "Got Correct rank for result object"); + cmp_ok( $mega_lamer->charfield, '==', $mega_lamer->id + 5, "Got Correct charfield for result object"); } VOID_CONTEXT_WITH_COND_FROM_RS: { @@ -683,6 +693,7 @@ ARRAYREF_OF_ARRAYREF_STYLE: { ## Did it use the condition in the resultset? cmp_ok( $mega_lamer->rank, '==', 42, "Got Correct rank for result object"); + cmp_ok( $mega_lamer->charfield, '==', $mega_lamer->id + 5, "Got Correct charfield for result object"); } } diff --git a/t/prefetch/correlated.t b/t/prefetch/correlated.t index 6452a94..df349fc 100644 --- a/t/prefetch/correlated.t +++ b/t/prefetch/correlated.t @@ -36,10 +36,10 @@ is_same_sql_bind( SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track, (SELECT COUNT( * ) FROM cd siblings - WHERE siblings.artist = me.artist + WHERE me.artist != ? + AND siblings.artist = me.artist AND siblings.cdid != me.cdid AND siblings.cdid != ? - AND me.artist != ? ), tracks.trackid, tracks.cd, tracks.position, tracks.title, tracks.last_updated_on, tracks.last_updated_at FROM cd me @@ -50,12 +50,12 @@ is_same_sql_bind( [ # subselect - [ { sqlt_datatype => 'integer', dbic_colname => 'siblings.cdid' } - => 23414 ], - [ { sqlt_datatype => 'integer', dbic_colname => 'me.artist' } => 2 ], + [ { sqlt_datatype => 'integer', dbic_colname => 'siblings.cdid' } + => 23414 ], + # outher WHERE [ { sqlt_datatype => 'integer', dbic_colname => 'me.artist' } => 2 ], @@ -102,15 +102,15 @@ is_same_sql_bind( SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track, (SELECT COUNT( * ) FROM cd siblings - WHERE siblings.artist = me.artist + WHERE me.artist != ? + AND siblings.artist = me.artist AND siblings.cdid != me.cdid AND siblings.cdid != ? - AND me.artist != ? ), (SELECT MIN( year ), MAX( year ) FROM cd siblings - WHERE siblings.artist = me.artist - AND me.artist != ? + WHERE me.artist != ? + AND siblings.artist = me.artist ), tracks.trackid, tracks.cd, tracks.position, tracks.title, tracks.last_updated_on, tracks.last_updated_at FROM cd me @@ -121,12 +121,12 @@ is_same_sql_bind( [ # first subselect - [ { sqlt_datatype => 'integer', dbic_colname => 'siblings.cdid' } - => 23414 ], - [ { sqlt_datatype => 'integer', dbic_colname => 'me.artist' } => 2 ], + [ { sqlt_datatype => 'integer', dbic_colname => 'siblings.cdid' } + => 23414 ], + # second subselect [ { sqlt_datatype => 'integer', dbic_colname => 'me.artist' } => 2 ], diff --git a/t/relationship/update_or_create_multi.t b/t/relationship/update_or_create_multi.t index c7cce7a..dd022a1 100644 --- a/t/relationship/update_or_create_multi.t +++ b/t/relationship/update_or_create_multi.t @@ -97,7 +97,7 @@ is_same_sql ( $search_sql, 'SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me - WHERE ( me.artist = ? AND me.title = ? AND me.genreid = ? ) + WHERE ( me.artist = ? AND me.genreid = ? AND me.title = ? ) ', 'expected select issued', ); diff --git a/t/resultset/as_query.t b/t/resultset/as_query.t index efd5e6e..39bf88c 100644 --- a/t/resultset/as_query.t +++ b/t/resultset/as_query.t @@ -43,8 +43,8 @@ my $rank_resolved_bind = [ { is_same_sql_bind( $art_rs->as_query, - "(SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me WHERE ( ( ( rank = ? ) AND ( name = ? ) ) ) )", - [ $rank_resolved_bind, $name_resolved_bind ], + "(SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me WHERE name = ? AND rank = ? )", + [ $name_resolved_bind, $rank_resolved_bind ], ); } @@ -53,8 +53,8 @@ my $rscol = $art_rs->get_column( 'charfield' ); { is_same_sql_bind( $rscol->as_query, - "(SELECT me.charfield FROM artist me WHERE ( ( ( rank = ? ) AND ( name = ? ) ) ) )", - [ $rank_resolved_bind, $name_resolved_bind ], + "(SELECT me.charfield FROM artist me WHERE name = ? AND rank = ? )", + [ $name_resolved_bind, $rank_resolved_bind ], ); } diff --git a/t/resultset/bind_attr.t b/t/resultset/bind_attr.t index d93dcd1..71d1b97 100644 --- a/t/resultset/bind_attr.t +++ b/t/resultset/bind_attr.t @@ -16,8 +16,6 @@ my $where_bind = { my $rs; { - local $TODO = 'bind args order needs fixing (semifor)'; - # First, the simple cases... $rs = $schema->resultset('Artist')->search( { artistid => 1 }, @@ -37,7 +35,6 @@ my $rs; is ( $rs->count, 1, 'where/bind last' ); # and the complex case - local $TODO = 'bind args order needs fixing (semifor)'; $rs = $schema->resultset('CustomSql')->search({}, { bind => [ 1999 ] }) ->search({ 'artistid' => 1 }, { where => \'title like ?', diff --git a/t/resultset/update_delete.t b/t/resultset/update_delete.t index 3314b88..917e12f 100644 --- a/t/resultset/update_delete.t +++ b/t/resultset/update_delete.t @@ -133,13 +133,14 @@ is_same_sql_bind ( AND fourkeys_to_twokeys.f_foo = me.foo AND fourkeys_to_twokeys.f_goodbye = me.goodbye AND fourkeys_to_twokeys.f_hello = me.hello - WHERE fourkeys_to_twokeys.pilot_sequence != ? AND ( bar = ? OR bar = ? ) AND ( foo = ? OR foo = ? ) AND ( goodbye = ? OR goodbye = ? ) AND ( hello = ? OR hello = ? ) AND sensors != ? + WHERE ( bar = ? OR bar = ? ) AND ( foo = ? OR foo = ? ) AND fourkeys_to_twokeys.pilot_sequence != ? AND ( goodbye = ? OR goodbye = ? ) AND ( hello = ? OR hello = ? ) AND sensors != ? ) ) ', [ + ("'1'", "'2'") x 2, "'666'", - ("'1'", "'2'") x 4, + ("'1'", "'2'") x 2, "'c'", ], 'Correct update-SQL with multicolumn in support', diff --git a/t/sqlmaker/dbihacks_internals.t b/t/sqlmaker/dbihacks_internals.t new file mode 100644 index 0000000..7b4506d --- /dev/null +++ b/t/sqlmaker/dbihacks_internals.t @@ -0,0 +1,173 @@ +use strict; +use warnings; +use Test::More; +use Test::Warn; + +use lib qw(t/lib); +use DBICTest; + +use DBIC::SqlMakerTest; +use Data::Dumper; + +my $schema = DBICTest->init_schema( no_deploy => 1); +my $sm = $schema->storage->sql_maker; + +for my $t ( + { + where => { artistid => 1, charfield => undef }, + cc_result => { artistid => 1, charfield => undef }, + sql => 'WHERE artistid = ? AND charfield IS NULL', + efcc_result => [qw( artistid )], + }, + { + where => { -and => [ artistid => 1, charfield => undef, { rank => 13 } ] }, + cc_result => { artistid => 1, charfield => undef, rank => 13 }, + sql => 'WHERE artistid = ? AND charfield IS NULL AND rank = ?', + efcc_result => [qw( artistid rank )], + }, + { + where => { -and => [ { artistid => 1, charfield => undef}, { rank => 13 } ] }, + cc_result => { artistid => 1, charfield => undef, rank => 13 }, + sql => 'WHERE artistid = ? AND charfield IS NULL AND rank = ?', + efcc_result => [qw( artistid rank )], + }, + { + where => { -and => [ -or => { name => 'Caterwauler McCrae' }, 'rank' ] }, + cc_result => { name => 'Caterwauler McCrae', rank => undef }, + sql => 'WHERE name = ? AND rank IS NULL', + efcc_result => [qw( name )], + }, + { + where => { -and => [ [ [ artist => {'=' => \'foo' } ] ], { name => \[ '= ?', 'bar' ] } ] }, + cc_result => { artist => {'=' => \'foo' }, name => \[ '= ?', 'bar' ] }, + sql => 'WHERE artist = foo AND name = ?', + efcc_result => [qw( artist )], + }, + { + where => { -and => [ -or => { name => 'Caterwauler McCrae', artistid => 2 } ] }, + cc_result => { -or => [ artistid => 2, name => 'Caterwauler McCrae' ] }, + sql => 'WHERE artistid = ? OR name = ?', + efcc_result => [], + }, + { + where => { -and => [ \'foo=bar', [ { artistid => { '=', 3 } } ], { name => 'Caterwauler McCrae'} ] }, + cc_result => { '' => \'foo=bar', name => 'Caterwauler McCrae', artistid => 3 }, + sql => 'WHERE foo=bar AND artistid = ? AND name = ?', + efcc_result => [qw( artistid name )], + }, + { + where => { artistid => [ 1 ], rank => [ 13, 2, 3 ], charfield => [ undef ] }, + cc_result => { artistid => 1, charfield => undef, rank => [13, 2, 3] }, + sql => 'WHERE artistid = ? AND charfield IS NULL AND ( rank = ? OR rank = ? OR rank = ? )', + efcc_result => [qw( artistid )], + }, + { + where => { artistid => { '=' => 1 }, rank => { '>' => 12 }, charfield => { '=' => undef } }, + cc_result => { artistid => 1, charfield => undef, rank => { '>' => 12 } }, + sql => 'WHERE artistid = ? AND charfield IS NULL AND rank > ?', + efcc_result => [qw( artistid )], + }, + { + where => { artistid => { '=' => [ 1 ], }, charfield => { '=' => [-and => \'1', \['?',2] ] }, rank => { '=' => [ 1, 2 ] } }, + cc_result => { artistid => 1, charfield => [-and => { '=' => \'1' }, { '=' => \['?',2] } ], rank => { '=' => [1, 2] } }, + sql => 'WHERE artistid = ? AND charfield = 1 AND charfield = ? AND ( rank = ? OR rank = ? )', + efcc_result => [qw( artistid charfield )], + }, + { + where => { -and => [ artistid => 1, artistid => 2 ], name => [ -and => { '!=', 1 }, 2 ], charfield => [ -or => { '=', 2 } ], rank => [-and => undef, { '=', undef }, { '!=', 2 } ] }, + cc_result => { artistid => [ -and => 1, 2 ], name => [ -and => { '!=', 1 }, 2 ], charfield => 2, rank => [ -and => undef, undef, { '!=', 2 } ] }, + sql => 'WHERE artistid = ? AND artistid = ? AND charfield = ? AND name != ? AND name = ? AND rank IS NULL AND rank IS NULL AND rank != ?', + efcc_result => [qw( artistid charfield name )], + }, + { + where => { -and => [ + [ '_macro.to' => { -like => '%correct%' }, '_wc_macros.to' => { -like => '%correct%' } ], + { -and => [ { 'group.is_active' => 1 }, { 'me.is_active' => 1 } ] } + ] }, + cc_result => { + 'group.is_active' => 1, + 'me.is_active' => 1, + -or => [ + '_macro.to' => { -like => '%correct%' }, + '_wc_macros.to' => { -like => '%correct%' }, + ], + }, + sql => 'WHERE ( _macro.to LIKE ? OR _wc_macros.to LIKE ? ) AND group.is_active = ? AND me.is_active = ?', + efcc_result => [qw( group.is_active me.is_active )], + }, + { + where => { artistid => [] }, + cc_result => { artistid => [] }, + efcc_result => [], + }, + (map { + { + where => { -and => $_ }, + cc_result => undef, + efcc_result => [], + sql => '', + }, + { + where => { -or => $_ }, + cc_result => undef, + efcc_result => [], + sql => '', + }, + } ( + # bare + [], {}, + # singles + [ {} ], [ [] ], + # doubles + [ [], [] ], [ {}, {} ], [ [], {} ], [ {}, [] ], + # tripples + [ {}, [], {} ], [ [], {}, [] ] + )), + + # FIXME legacy compat crap, possibly worth undef/dieing in SQLMaker + { where => { artistid => {} }, sql => '', cc_result => undef, efcc_result => [] }, + + # batshit insanity, just to be thorough + { + where => { -and => [ [ 'artistid' ], [ -and => [ artistid => { '!=', 69 }, artistid => undef, artistid => { '=' => 200 } ]], artistid => [], { -or => [] }, { -and => [] }, [ 'charfield' ], { name => [] }, 'rank' ] }, + cc_result => { artistid => [ -and => undef, { '!=', 69 }, undef, 200, [] ], charfield => undef, name => [], rank => undef }, + sql => 'WHERE artistid IS NULL AND artistid != ? AND artistid IS NULL AND artistid = ? AND 0=1 AND charfield IS NULL AND 0=1 AND rank IS NULL', + efcc_result => [qw( artistid )], + }, +) { + + for my $w ( + $t->{where}, + [ -and => $t->{where} ], + ( keys %{$t->{where}} <= 1 ) ? [ %{$t->{where}} ] : () + ) { + my $name = do { local ($Data::Dumper::Indent, $Data::Dumper::Terse, $Data::Dumper::Sortkeys) = (0, 1, 1); Dumper $w }; + + my @orig_sql_bind = $sm->where($w); + + is_same_sql ( $orig_sql_bind[0], $t->{sql}, "Expected SQL from $name" ) + if exists $t->{sql}; + + my $collapsed_cond = $schema->storage->_collapse_cond($w); + + is_same_sql_bind( + \[ $sm->where($collapsed_cond) ], + \\@orig_sql_bind, + "Collapse did not alter final SQL based on $name", + ); + + is_deeply( + $collapsed_cond, + $t->{cc_result}, + "Expected collapsed condition produced on $name", + ); + + is_deeply( + $schema->storage->_extract_fixed_condition_columns($w), + $t->{efcc_result}, + "Expected fixed_condition produced on $name", + ); + } +} + +done_testing; diff --git a/t/sqlmaker/limit_dialects/torture.t b/t/sqlmaker/limit_dialects/torture.t index f273189..74e60a2 100644 --- a/t/sqlmaker/limit_dialects/torture.t +++ b/t/sqlmaker/limit_dialects/torture.t @@ -11,9 +11,11 @@ use DBIC::SqlMakerTest; my $schema = DBICTest->init_schema; my $native_limit_dialect = $schema->storage->sql_maker->{limit_dialect}; +my $where_string = 'me.title = ? AND source != ? AND source = ?'; + my @where_bind = ( - [ {} => 'Study' ], [ {} => 'kama sutra' ], + [ {} => 'Study' ], [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'source' } => 'Library' ], ); my @select_bind = ( @@ -37,16 +39,16 @@ my $tests = { LimitOffset => { limit => [ - '( + "( SELECT me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? LIMIT ? - )', + )", [ @select_bind, @where_bind, @@ -56,17 +58,17 @@ my $tests = { ], ], limit_offset => [ - '( + "( SELECT me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? LIMIT ? OFFSET ? - )', + )", [ @select_bind, @where_bind, @@ -77,17 +79,17 @@ my $tests = { ], ], ordered_limit => [ - '( + "( SELECT me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY ? / ?, ? LIMIT ? - )', + )", [ @select_bind, @where_bind, @@ -98,18 +100,18 @@ my $tests = { ] ], ordered_limit_offset => [ - '( + "( SELECT me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY ? / ?, ? LIMIT ? OFFSET ? - )', + )", [ @select_bind, @where_bind, @@ -121,7 +123,7 @@ my $tests = { ], ], limit_offset_prefetch => [ - '( + "( SELECT me.name, books.id, books.source, books.owner, books.title, books.price FROM ( SELECT me.name, me.id @@ -130,7 +132,7 @@ my $tests = { ) me LEFT JOIN books books ON books.owner = me.id - )', + )", [ [ { sqlt_datatype => 'integer' } => 3 ], [ { sqlt_datatype => 'integer' } => 1 ], @@ -140,17 +142,17 @@ my $tests = { LimitXY => { ordered_limit_offset => [ - '( + "( SELECT me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY ? / ?, ? LIMIT ?, ? - )', + )", [ @select_bind, @where_bind, @@ -162,7 +164,7 @@ my $tests = { ], ], limit_offset_prefetch => [ - '( + "( SELECT me.name, books.id, books.source, books.owner, books.title, books.price FROM ( SELECT me.name, me.id @@ -171,7 +173,7 @@ my $tests = { ) me LEFT JOIN books books ON books.owner = me.id - )', + )", [ [ { sqlt_datatype => 'integer' } => 1 ], [ { sqlt_datatype => 'integer' } => 3 ], @@ -181,16 +183,16 @@ my $tests = { SkipFirst => { ordered_limit_offset => [ - '( + "( SELECT SKIP ? FIRST ? me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY ? / ?, ? - )', + )", [ [ { sqlt_datatype => 'integer' } => 3 ], [ { sqlt_datatype => 'integer' } => 4 ], @@ -202,7 +204,7 @@ my $tests = { ], ], limit_offset_prefetch => [ - '( + "( SELECT me.name, books.id, books.source, books.owner, books.title, books.price FROM ( SELECT SKIP ? FIRST ? me.name, me.id @@ -210,7 +212,7 @@ my $tests = { ) me LEFT JOIN books books ON books.owner = me.id - )', + )", [ [ { sqlt_datatype => 'integer' } => 1 ], [ { sqlt_datatype => 'integer' } => 3 ], @@ -220,16 +222,16 @@ my $tests = { FirstSkip => { ordered_limit_offset => [ - '( + "( SELECT FIRST ? SKIP ? me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY ? / ?, ? - )', + )", [ [ { sqlt_datatype => 'integer' } => 4 ], [ { sqlt_datatype => 'integer' } => 3 ], @@ -241,7 +243,7 @@ my $tests = { ], ], limit_offset_prefetch => [ - '( + "( SELECT me.name, books.id, books.source, books.owner, books.title, books.price FROM ( SELECT FIRST ? SKIP ? me.name, me.id @@ -249,7 +251,7 @@ my $tests = { ) me LEFT JOIN books books ON books.owner = me.id - )', + )", [ [ { sqlt_datatype => 'integer' } => 3 ], [ { sqlt_datatype => 'integer' } => 1 ], @@ -258,7 +260,7 @@ my $tests = { }, RowNumberOver => do { - my $unordered_sql = '( + my $unordered_sql = "( SELECT me.id, owner__id, owner__name, bar, baz FROM ( SELECT me.id, owner__id, owner__name, bar, baz, ROW_NUMBER() OVER() AS rno__row__index @@ -267,15 +269,15 @@ my $tests = { FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ) me ) me WHERE rno__row__index >= ? AND rno__row__index <= ? - )'; + )"; - my $ordered_sql = '( + my $ordered_sql = "( SELECT me.id, owner__id, owner__name, bar, baz FROM ( SELECT me.id, owner__id, owner__name, bar, baz, ROW_NUMBER() OVER( ORDER BY ORDER__BY__001, ORDER__BY__002 ) AS rno__row__index @@ -285,13 +287,13 @@ my $tests = { FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ) me ) me WHERE rno__row__index >= ? AND rno__row__index <= ? - )'; + )"; { limit => [$unordered_sql, @@ -337,7 +339,7 @@ my $tests = { ], ], limit_offset_prefetch => [ - '( + "( SELECT me.name, books.id, books.source, books.owner, books.title, books.price FROM ( SELECT me.name, me.id @@ -351,7 +353,7 @@ my $tests = { ) me LEFT JOIN books books ON books.owner = me.id - )', + )", [ [ { sqlt_datatype => 'integer' } => 2 ], [ { sqlt_datatype => 'integer' } => 4 ], @@ -362,20 +364,20 @@ my $tests = { RowNum => do { my $limit_sql = sub { - sprintf '( + sprintf "( SELECT me.id, owner__id, owner__name, bar, baz FROM ( SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? %s ) me WHERE ROWNUM <= ? - )', $_[0] || ''; + )", $_[0] || ''; }; { @@ -389,7 +391,7 @@ my $tests = { ], ], limit_offset => [ - '( + "( SELECT me.id, owner__id, owner__name, bar, baz FROM ( SELECT me.id, owner__id, owner__name, bar, baz, ROWNUM AS rownum__index @@ -398,13 +400,13 @@ my $tests = { FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ) me ) me WHERE rownum__index BETWEEN ? AND ? - )', + )", [ @select_bind, @where_bind, @@ -425,7 +427,7 @@ my $tests = { ], ], ordered_limit_offset => [ - '( + "( SELECT me.id, owner__id, owner__name, bar, baz FROM ( SELECT me.id, owner__id, owner__name, bar, baz, ROWNUM AS rownum__index @@ -434,7 +436,7 @@ my $tests = { FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY ? / ?, ? @@ -442,7 +444,7 @@ my $tests = { WHERE ROWNUM <= ? ) me WHERE rownum__index >= ? - )', + )", [ @select_bind, @where_bind, @@ -454,7 +456,7 @@ my $tests = { ], ], limit_offset_prefetch => [ - '( + "( SELECT me.name, books.id, books.source, books.owner, books.title, books.price FROM ( SELECT me.name, me.id @@ -468,7 +470,7 @@ my $tests = { ) me LEFT JOIN books books ON books.owner = me.id - )', + )", [ [ { sqlt_datatype => 'integer' } => 2 ], [ { sqlt_datatype => 'integer' } => 4 ], @@ -479,16 +481,16 @@ my $tests = { FetchFirst => { limit => [ - '( + "( SELECT me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? FETCH FIRST 4 ROWS ONLY - )', + )", [ @select_bind, @where_bind, @@ -497,14 +499,14 @@ my $tests = { ], ], limit_offset => [ - '( + "( SELECT me.id, owner__id, owner__name, bar, baz FROM ( SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY me.id @@ -512,7 +514,7 @@ my $tests = { ) me ORDER BY me.id DESC FETCH FIRST 4 ROWS ONLY - )', + )", [ @select_bind, @where_bind, @@ -521,17 +523,17 @@ my $tests = { ], ], ordered_limit => [ - '( + "( SELECT me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY ? / ?, ? FETCH FIRST 4 ROWS ONLY - )', + )", [ @select_bind, @where_bind, @@ -541,7 +543,7 @@ my $tests = { ], ], ordered_limit_offset => [ - '( + "( SELECT me.id, owner__id, owner__name, bar, baz FROM ( SELECT me.id, owner__id, owner__name, bar, baz, ORDER__BY__001, ORDER__BY__002 @@ -550,7 +552,7 @@ my $tests = { FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY ? / ?, ? @@ -560,7 +562,7 @@ my $tests = { FETCH FIRST 4 ROWS ONLY ) me ORDER BY ORDER__BY__001, ORDER__BY__002 - )', + )", [ @select_bind, @order_bind, @@ -571,7 +573,7 @@ my $tests = { ], ], limit_offset_prefetch => [ - '( + "( SELECT me.name, books.id, books.source, books.owner, books.title, books.price FROM ( SELECT me.name, me.id @@ -586,22 +588,22 @@ my $tests = { ) me LEFT JOIN books books ON books.owner = me.id - )', + )", [], ], }, Top => { limit => [ - '( + "( SELECT TOP 4 me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? - )', + )", [ @select_bind, @where_bind, @@ -610,20 +612,20 @@ my $tests = { ], ], limit_offset => [ - '( + "( SELECT TOP 4 me.id, owner__id, owner__name, bar, baz FROM ( SELECT TOP 7 me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY me.id ) me ORDER BY me.id DESC - )', + )", [ @select_bind, @where_bind, @@ -632,16 +634,16 @@ my $tests = { ], ], ordered_limit => [ - '( + "( SELECT TOP 4 me.id, owner.id, owner.name, ? * ?, ? FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY ? / ?, ? - )', + )", [ @select_bind, @where_bind, @@ -651,7 +653,7 @@ my $tests = { ], ], ordered_limit_offset => [ - '( + "( SELECT me.id, owner__id, owner__name, bar, baz FROM ( SELECT TOP 4 me.id, owner__id, owner__name, bar, baz, ORDER__BY__001, ORDER__BY__002 @@ -660,7 +662,7 @@ my $tests = { FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ORDER BY ? / ?, ? @@ -668,7 +670,7 @@ my $tests = { ORDER BY ORDER__BY__001 DESC, ORDER__BY__002 DESC ) me ORDER BY ORDER__BY__001, ORDER__BY__002 - )', + )", [ @select_bind, @order_bind, @@ -679,7 +681,7 @@ my $tests = { ], ], limit_offset_prefetch => [ - '( + "( SELECT me.name, books.id, books.source, books.owner, books.title, books.price FROM ( SELECT TOP 3 me.name, me.id @@ -692,21 +694,21 @@ my $tests = { ) me LEFT JOIN books books ON books.owner = me.id - )', + )", [], ], }, GenericSubQ => { ordered_limit => [ - '( + "( SELECT me.id, owner__id, owner__name, bar, baz FROM ( SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz, me.price FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ) me @@ -735,7 +737,7 @@ my $tests = { ) ) < ? ORDER BY me.price DESC, me.id ASC - )', + )", [ @select_bind, @where_bind, @@ -745,14 +747,14 @@ my $tests = { ], ], ordered_limit_offset => [ - '( + "( SELECT me.id, owner__id, owner__name, bar, baz FROM ( SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz, me.price FROM books me JOIN owners owner ON owner.id = me.owner - WHERE source != ? AND me.title = ? AND source = ? + WHERE $where_string GROUP BY (me.id / ?), owner.id HAVING ? ) me @@ -781,7 +783,7 @@ my $tests = { ) ) BETWEEN ? AND ? ORDER BY me.price DESC, me.id ASC - )', + )", [ @select_bind, @where_bind, @@ -792,7 +794,7 @@ my $tests = { ], ], limit_offset_prefetch => [ - '( + "( SELECT me.name, books.id, books.source, books.owner, books.title, books.price FROM ( SELECT me.name, me.id @@ -819,7 +821,7 @@ my $tests = { LEFT JOIN books books ON books.owner = me.id ORDER BY me.name ASC, me.id DESC - )', + )", [ [ { sqlt_datatype => 'integer' } => 1 ], [ { sqlt_datatype => 'integer' } => 3 ],