use List::Util 'first';
use Scalar::Util 'blessed';
-use Sub::Name 'subname';
-use DBIx::Class::_Util qw(is_plain_value is_literal_value);
+use DBIx::Class::_Util qw(UNRESOLVABLE_CONDITION serialize);
+use SQL::Abstract qw(is_plain_value is_literal_value);
use namespace::clean;
#
my $outer_attrs = { %$attrs };
delete @{$outer_attrs}{qw(from bind rows offset group_by _grouped_by_distinct having)};
- my $inner_attrs = { %$attrs };
- delete @{$inner_attrs}{qw(for collapse select as _related_results_construction)};
+ my $inner_attrs = { %$attrs, _simple_passthrough_construction => 1 };
+ delete @{$inner_attrs}{qw(for collapse select as)};
# there is no point of ordering the insides if there is no limit
delete $inner_attrs->{order_by} if (
sub _inner_join_to_node {
my ($self, $from, $alias) = @_;
- # subqueries and other oddness are naturally not supported
- return $from if (
- ref $from ne 'ARRAY'
- ||
- @$from <= 1
- ||
- ref $from->[0] ne 'HASH'
- ||
- ! $from->[0]{-alias}
- ||
- $from->[0]{-alias} eq $alias # this last bit means $alias is the head of $from - nothing to do
- );
+ my $switch_branch = $self->_find_join_path_to_node($from, $alias);
- # find the current $alias in the $from structure
- my $switch_branch;
- JOINSCAN:
- for my $j (@{$from}[1 .. $#$from]) {
- if ($j->[0]{-alias} eq $alias) {
- $switch_branch = $j->[0]{-join_path};
- last JOINSCAN;
- }
- }
-
- # something else went quite wrong
- return $from unless $switch_branch;
+ return $from unless @{$switch_branch||[]};
# So it looks like we will have to switch some stuff around.
# local() is useless here as we will be leaving the scope
return \@new_from;
}
+sub _find_join_path_to_node {
+ my ($self, $from, $target_alias) = @_;
+
+ # subqueries and other oddness are naturally not supported
+ return undef if (
+ ref $from ne 'ARRAY'
+ ||
+ ref $from->[0] ne 'HASH'
+ ||
+ ! defined $from->[0]{-alias}
+ );
+
+ # no path - the head is the alias
+ return [] if $from->[0]{-alias} eq $target_alias;
+
+ for my $i (1 .. $#$from) {
+ return $from->[$i][0]{-join_path} if ( ($from->[$i][0]{-alias}||'') eq $target_alias );
+ }
+
+ # something else went quite wrong
+ return undef;
+}
+
sub _extract_order_criteria {
my ($self, $order_by, $sql_maker) = @_;
my @cols = (
( map { $_->[0] } $self->_extract_order_criteria($order_by) ),
- ( $where ? @{ $self->_extract_fixed_condition_columns($where) || [] } : () ),
+ ( $where ? keys %{ $self->_extract_fixed_condition_columns($where) } : () ),
) or return 0;
my $colinfo = $self->_resolve_column_info($ident, \@cols);
# by is stable.
# returns that portion as a colinfo hashref on success
sub _extract_colinfo_of_stable_main_source_order_by_portion {
- my ($self, $main_rsrc, $order_by, $where) = @_;
+ my ($self, $attrs) = @_;
+
+ my $nodes = $self->_find_join_path_to_node($attrs->{from}, $attrs->{alias});
- die "Huh... I expect a blessed result_source..."
- if ref($main_rsrc) eq 'ARRAY';
+ return unless defined $nodes;
my @ord_cols = map
{ $_->[0] }
- ( $self->_extract_order_criteria($order_by) )
+ ( $self->_extract_order_criteria($attrs->{order_by}) )
;
return unless @ord_cols;
- my $colinfos = $self->_resolve_column_info($main_rsrc);
+ my $valid_aliases = { map { $_ => 1 } (
+ $attrs->{from}[0]{-alias},
+ map { values %$_ } @$nodes,
+ ) };
- for (0 .. $#ord_cols) {
- if (
- ! $colinfos->{$ord_cols[$_]}
- or
- $colinfos->{$ord_cols[$_]}{-result_source} != $main_rsrc
- ) {
- $#ord_cols = $_ - 1;
- last;
- }
- }
+ my $colinfos = $self->_resolve_column_info($attrs->{from});
- # we just truncated it above
- return unless @ord_cols;
+ my ($colinfos_to_return, $seen_main_src_cols);
- my $order_portion_ci = { map {
- $colinfos->{$_}{-colname} => $colinfos->{$_},
- $colinfos->{$_}{-fq_colname} => $colinfos->{$_},
- } @ord_cols };
+ for my $col (@ord_cols) {
+ # if order criteria is unresolvable - there is nothing we can do
+ my $colinfo = $colinfos->{$col} or last;
- # since all we check here are the start of the order_by belonging to the
- # top level $rsrc, a present identifying set will mean that the resultset
- # is ordered by its leftmost table in a stable manner
- #
- # RV of _identifying_column_set contains unqualified names only
- my $unqualified_idset = $main_rsrc->_identifying_column_set({
- ( $where ? %{
- $self->_resolve_column_info(
- $main_rsrc, $self->_extract_fixed_condition_columns($where)||[]
- )
- } : () ),
- %$order_portion_ci
- }) or return;
-
- my $ret_info;
- my %unqualified_idcols_from_order = map {
- $order_portion_ci->{$_} ? ( $_ => $order_portion_ci->{$_} ) : ()
- } @$unqualified_idset;
-
- # extra optimization - cut the order_by at the end of the identifying set
- # (just in case the user was stupid and overlooked the obvious)
- for my $i (0 .. $#ord_cols) {
- my $col = $ord_cols[$i];
- my $unqualified_colname = $order_portion_ci->{$col}{-colname};
- $ret_info->{$col} = { %{$order_portion_ci->{$col}}, -idx_in_order_subset => $i };
- delete $unqualified_idcols_from_order{$ret_info->{$col}{-colname}};
-
- # we didn't reach the end of the identifying portion yet
- return $ret_info unless keys %unqualified_idcols_from_order;
+ # if we reached the end of the allowed aliases - also nothing we can do
+ last unless $valid_aliases->{$colinfo->{-source_alias}};
+
+ $colinfos_to_return->{$col} = $colinfo;
+
+ $seen_main_src_cols->{$colinfo->{-colname}} = 1
+ if $colinfo->{-source_alias} eq $attrs->{alias};
}
- die 'How did we get here...';
+ # FIXME the condition may be singling out things on its own, so we
+ # conceivable could come back wi "stable-ordered by nothing"
+ # not confient enough in the parser yet, so punt for the time being
+ return unless $seen_main_src_cols;
+
+ my $main_src_fixed_cols_from_cond = [ $attrs->{where}
+ ? (
+ map
+ {
+ ( $colinfos->{$_} and $colinfos->{$_}{-source_alias} eq $attrs->{alias} )
+ ? $colinfos->{$_}{-colname}
+ : ()
+ }
+ keys %{ $self->_extract_fixed_condition_columns($attrs->{where}) }
+ )
+ : ()
+ ];
+
+ return $attrs->{result_source}->_identifying_column_set([
+ keys %$seen_main_src_cols,
+ @$main_src_fixed_cols_from_cond,
+ ]) ? $colinfos_to_return : ();
}
# Attempts to flatten a passed in SQLA condition as much as possible towards
sub _collapse_cond {
my ($self, $where, $where_is_anded_array) = @_;
+ my $fin;
+
if (! $where) {
return;
}
my $chunk = shift @pieces;
if (ref $chunk eq 'HASH') {
- push @pairs, map { [ $_ => $chunk->{$_} ] } sort keys %$chunk;
+ push @pairs, map { $_ => $chunk->{$_} } sort keys %$chunk;
}
elsif (ref $chunk eq 'ARRAY') {
- push @pairs, [ -or => $chunk ]
+ push @pairs, -or => $chunk
if @$chunk;
}
- elsif ( ! ref $chunk) {
- push @pairs, [ $chunk, shift @pieces ];
+ elsif ( ! length ref $chunk) {
+ push @pairs, $chunk, shift @pieces;
}
else {
- push @pairs, [ '', $chunk ];
+ push @pairs, '', $chunk;
}
}
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} ];
- }
+ # consolidate all -and nodes
+ if ($col =~ /^\-and$/i) {
+ push @{$fin->{-and}},
+ ref $c->{$col} eq 'ARRAY' ? @{$c->{$col}}
+ : ref $c->{$col} eq 'HASH' ? %{$c->{$col}}
+ : { $col => $c->{$col} }
+ ;
+ }
+ elsif ($col =~ /^\-/) {
+ push @{$fin->{-and}}, { $col => $c->{$col} };
+ }
+ elsif (exists $fin->{$col}) {
+ $fin->{$col} = [ -and => map {
+ (ref $_ eq 'ARRAY' and ($_->[0]||'') =~ /^\-and$/i )
+ ? @{$_}[1..$#$_]
+ : $_
+ ;
+ } ($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;
+ # we are always at top-level here, it is safe to dump empty *standalone* pieces
+ my $fin_idx;
- while ( @w and (
- (ref $w[0] eq 'ARRAY' and ! @{$w[0]} )
- or
- (ref $w[0] eq 'HASH' and ! keys %{$w[0]})
- )) { shift @w };
+ for (my $i = 0; $i <= $#$where; $i++ ) {
- return unless @w;
+ my $logic_mod = lc ( ($where->[$i] =~ /^(\-(?:and|or))$/i)[0] || '' );
- 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] ]")
- ;
+ if ($logic_mod) {
+ $i++;
+ $self->throw_exception("Unsupported top-level op/arg pair: [ $logic_mod => $where->[$i] ]")
+ unless ref $where->[$i] eq 'HASH' or ref $where->[$i] eq 'ARRAY';
+
+ my $sub_elt = $self->_collapse_cond({ $logic_mod => $where->[$i] })
+ or next;
+
+ $fin_idx->{ "SER_" . serialize $sub_elt } = $sub_elt;
+ }
+ elsif (! length ref $where->[$i] ) {
+ my $sub_elt = $self->_collapse_cond({ @{$where}[$i, $i+1] })
+ or next;
+
+ $fin_idx->{ "COL_$where->[$i]_" . serialize $sub_elt } = $sub_elt;
+ $i++;
}
else {
- return $self->_collapse_cond({ @w });
+ $fin_idx->{ "SER_" . serialize $where->[$i] } = $self->_collapse_cond( $where->[$i] ) || next;
}
}
+
+ return unless $fin_idx;
+
+ $fin = ( keys %$fin_idx == 1 ) ? (values %$fin_idx)[0] : {
+ -or => [ map {
+ # unroll single-element hashes
+ ( ref $fin_idx->{$_} eq 'HASH' and keys %{$fin_idx->{$_}} == 1 )
+ ? %{$fin_idx->{$_}}
+ : $fin_idx->{$_}
+ } sort keys %$fin_idx ]
+ };
}
else {
# not a hash not an array
- return { '' => $where };
+ $fin = { '' => $where };
}
- # catchall, some of the things above fall through
- return $where;
+ # unroll single-element -and's
+ while (
+ $fin->{-and}
+ and
+ @{$fin->{-and}} < 2
+ ) {
+ my $and = delete $fin->{-and};
+ last if @$and == 0;
+
+ # at this point we have @$and == 1
+ if (
+ ref $and->[0] eq 'HASH'
+ and
+ ! grep { exists $fin->{$_} } keys %{$and->[0]}
+ ) {
+ $fin = {
+ %$fin, %{$and->[0]}
+ };
+ }
+ }
+
+ # compress same-column conds found in $fin
+ for my $col ( grep { $_ !~ /^\-/ } keys %$fin ) {
+ next unless ref $fin->{$col} eq 'ARRAY' and ($fin->{$col}[0]||'') =~ /^\-and$/i;
+ my $val_bag = { map {
+ (! defined $_ ) ? ( UNDEF => undef )
+ : ( ! length ref $_ or is_plain_value $_ ) ? ( "VAL_$_" => $_ )
+ : ( ( 'SER_' . serialize $_ ) => $_ )
+ } @{$fin->{$col}}[1 .. $#{$fin->{$col}}] };
+
+ if (keys %$val_bag == 1 ) {
+ ($fin->{$col}) = values %$val_bag;
+ }
+ else {
+ $fin->{$col} = [ -and => map { $val_bag->{$_} } sort keys %$val_bag ];
+ }
+ }
+
+ return keys %$fin ? $fin : ();
}
sub _collapse_cond_unroll_pairs {
my @conds;
while (@$pairs) {
- my ($lhs, $rhs) = @{ shift @$pairs };
+ my ($lhs, $rhs) = splice @$pairs, 0, 2;
if ($lhs eq '') {
push @conds, $self->_collapse_cond($rhs);
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->{-ident}) {
+ push @conds, { $lhs => { '=', $rhs } };
+ }
+ elsif (ref $rhs eq 'HASH' and keys %$rhs == 1 and exists $rhs->{-value} and is_plain_value $rhs->{-value}) {
+ push @conds, { $lhs => $rhs->{-value} };
+ }
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 }
- );
- }
+ if( is_literal_value $rhs->{'='}) {
+ push @conds, { $lhs => $rhs };
+ }
+ else {
+ 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;
+ my ($l, $r) = %$p;
- push @conds, ( ! length ref $r or is_plain_value($r) )
- ? { $l => $r }
- : { $l => { '=' => $r } }
- ;
+ push @conds, ( ! length ref $r or is_plain_value($r) )
+ ? { $l => $r }
+ : { $l => { '=' => $r } }
+ ;
+ }
}
}
elsif (ref $rhs eq 'ARRAY') {
if @$rhs == 1;
if( $rhs->[0] =~ /^\-and$/i ) {
- unshift @$pairs, map { [ $lhs => $_ ] } @{$rhs}[1..$#$rhs];
+ unshift @$pairs, map { $lhs => $_ } @{$rhs}[1..$#$rhs];
}
# if not an AND then it's an OR
elsif(@$rhs == 2) {
- unshift @$pairs, [ $lhs => $rhs->[1] ];
+ unshift @$pairs, $lhs => $rhs->[1];
}
else {
- push @conds, { $lhs => $rhs };
+ push @conds, { $lhs => [ @{$rhs}[1..$#$rhs] ] };
}
}
elsif (@$rhs == 1) {
- unshift @$pairs, [ $lhs => $rhs->[0] ];
+ unshift @$pairs, $lhs => $rhs->[0];
}
else {
push @conds, { $lhs => $rhs };
}
}
+ # unroll func + { -value => ... }
+ elsif (
+ ref $rhs eq 'HASH'
+ and
+ ( my ($subop) = keys %$rhs ) == 1
+ and
+ length ref ((values %$rhs)[0])
+ and
+ my $vref = is_plain_value( (values %$rhs)[0] )
+ ) {
+ push @conds, { $lhs => { $subop => $$vref } }
+ }
else {
push @conds, { $lhs => $rhs };
}
return @conds;
}
-
-# returns an arrayref of column names which *definitely* have some
-# 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
-# etc.
+# Analyzes a given condition and attempts to extract all columns
+# with a definitive fixed-condition criteria. Returns a hashref
+# of k/v pairs suitable to be passed to set_columns(), with a
+# MAJOR CAVEAT - multi-value (contradictory) equalities are still
+# represented as a reference to the UNRESOVABLE_CONDITION constant
+# The reason we do this is that some codepaths only care about the
+# codition being stable, as opposed to actually making sense
+#
+# The normal mode 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 etc.
+#
+# With the optional "consider_nulls" boolean argument, the function
+# is instead used to infer inambiguous values from conditions
+# (e.g. the inheritance of resultset conditions on new_result)
#
-# 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
sub _extract_fixed_condition_columns {
- 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 (
- ! length ref $v
- or
- is_plain_value ($v)
- or
- (
- ref $v eq 'HASH'
- and
- keys %$v == 1
- and
- ref $v->{'='}
- and
- is_literal_value($v->{'='})
- )
- ) {
- $res->{$c} = 1;
+ my ($self, $where, $consider_nulls) = @_;
+ my $where_hash = $self->_collapse_cond($_[1]);
+
+ my $res = {};
+ my ($c, $v);
+ for $c (keys %$where_hash) {
+ my $vals;
+
+ if (!defined ($v = $where_hash->{$c}) ) {
+ $vals->{UNDEF} = $v if $consider_nulls
+ }
+ elsif (
+ ref $v eq 'HASH'
+ and
+ keys %$v == 1
+ ) {
+ if (exists $v->{-value}) {
+ if (defined $v->{-value}) {
+ $vals->{"VAL_$v->{-value}"} = $v->{-value}
+ }
+ elsif( $consider_nulls ) {
+ $vals->{UNDEF} = $v->{-value};
+ }
+ }
+ # do not need to check for plain values - _collapse_cond did it for us
+ elsif(length ref $v->{'='} and is_literal_value($v->{'='}) ) {
+ $vals->{ 'SER_' . serialize $v->{'='} } = $v->{'='};
}
- elsif (ref $v eq 'ARRAY' and ($v->[0]||'') eq '-and') {
- $res->{$_} = 1 for map { @{ $self->_extract_fixed_condition_columns({ $c => $_ }) } } @{$v}[1..$#$v];
+ }
+ elsif (
+ ! length ref $v
+ or
+ is_plain_value ($v)
+ ) {
+ $vals->{"VAL_$v"} = $v;
+ }
+ elsif (ref $v eq 'ARRAY' and ($v->[0]||'') eq '-and') {
+ for ( @{$v}[1..$#$v] ) {
+ my $subval = $self->_extract_fixed_condition_columns({ $c => $_ }, 'consider nulls'); # always fish nulls out on recursion
+ next unless exists $subval->{$c}; # didn't find anything
+ $vals->{
+ ! defined $subval->{$c} ? 'UNDEF'
+ : ( ! length ref $subval->{$c} or is_plain_value $subval->{$c} ) ? "VAL_$subval->{$c}"
+ : ( 'SER_' . serialize $subval->{$c} )
+ } = $subval->{$c};
}
}
+
+ if (keys %$vals == 1) {
+ ($res->{$c}) = (values %$vals)
+ unless !$consider_nulls and exists $vals->{UNDEF};
+ }
+ elsif (keys %$vals > 1) {
+ $res->{$c} = UNRESOLVABLE_CONDITION;
+ }
}
- return [ sort keys %$res ];
+ $res;
}
1;