sub _adjust_select_args_for_complex_prefetch {
my ($self, $from, $select, $where, $attrs) = @_;
- $self->throw_exception ('Nothing to prefetch... how did we get here?!')
- if not @{$attrs->{_prefetch_selector_range}||[]};
-
$self->throw_exception ('Complex prefetches are not supported on resultsets with a custom from attribute')
if (ref $from ne 'ARRAY' || ref $from->[0] ne 'HASH' || ref $from->[1] ne 'ARRAY');
+ my $root_alias = $attrs->{alias};
+
# generate inner/outer attribute lists, remove stuff that doesn't apply
my $outer_attrs = { %$attrs };
- delete $outer_attrs->{$_} for qw/where bind rows offset group_by having/;
+ delete $outer_attrs->{$_} for qw/where bind rows offset group_by _grouped_by_distinct having/;
my $inner_attrs = { %$attrs };
- delete $inner_attrs->{$_} for qw/for collapse _prefetch_selector_range select as/;
+ delete $inner_attrs->{$_} for qw/from for collapse select as _related_results_construction/;
- # if the user did not request it, there is no point using it inside
- delete $inner_attrs->{order_by} if delete $inner_attrs->{_order_is_artificial};
+ # there is no point of ordering the insides if there is no limit
+ delete $inner_attrs->{order_by} if (
+ delete $inner_attrs->{_order_is_artificial}
+ or
+ ! $inner_attrs->{rows}
+ );
# generate the inner/outer select lists
# for inside we consider only stuff *not* brought in by the prefetch
: next
;
- if ( ($h->{-alias}||'') eq $attrs->{alias} and $h->{-rsrc} ) {
+ if ( ($h->{-alias}||'') eq $root_alias and $h->{-rsrc} ) {
$root_node = $h;
$root_node_offset = $i;
last;
my $colinfo = $self->_resolve_column_info($from);
my $selected_root_columns;
- my ($p_start, $p_end) = @{$outer_attrs->{_prefetch_selector_range}};
- for my $i (0 .. $p_start - 1, $p_end + 1 .. $#$outer_select) {
+ for my $i (0 .. $#$outer_select) {
my $sel = $outer_select->[$i];
+ next if (
+ $colinfo->{$sel} and $colinfo->{$sel}{-source_alias} ne $root_alias
+ );
+
if (ref $sel eq 'HASH' ) {
$sel->{-as} ||= $attrs->{as}[$i];
- $outer_select->[$i] = join ('.', $attrs->{alias}, ($sel->{-as} || "inner_column_$i") );
+ $outer_select->[$i] = join ('.', $root_alias, ($sel->{-as} || "inner_column_$i") );
}
elsif (! ref $sel and my $ci = $colinfo->{$sel}) {
$selected_root_columns->{$ci->{-colname}} = 1;
for (sort map { keys %{$_->{-seen_columns}||{}} } map { values %$_ } values %$connecting_aliastypes) {
my $ci = $colinfo->{$_} or next;
if (
- $ci->{-source_alias} eq $attrs->{alias}
+ $ci->{-source_alias} eq $root_alias
and
! $selected_root_columns->{$ci->{-colname}}++
) {
my $inner_aliastypes =
$self->_resolve_aliastypes_from_select_args( $inner_from, $inner_select, $where, $inner_attrs );
- # we need to simulate collapse in the subq if a multiplying join is pulled
- # by being a non-selecting restrictor
+ # uh-oh a multiplier (which is not us) left in, this is a problem
if (
- ! $inner_attrs->{group_by}
+ $inner_aliastypes->{multiplying}
+ and
+ !$inner_aliastypes->{grouping} # if there are groups - assume user knows wtf they are up to
and
- first {
- $inner_aliastypes->{restricting}{$_}
- and
- ! $inner_aliastypes->{selecting}{$_}
- } ( keys %{$inner_aliastypes->{multiplying}||{}} )
+ my @multipliers = grep { $_ ne $root_alias } keys %{$inner_aliastypes->{multiplying}}
) {
- my $unprocessed_order_chunks;
- ($inner_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection (
- $inner_from, $inner_select, $inner_attrs->{order_by}
- );
-
- $self->throw_exception (
- 'A required group_by clause could not be constructed automatically due to a complex '
- . 'order_by criteria. Either order_by columns only (no functions) or construct a suitable '
- . 'group_by by hand'
- ) if $unprocessed_order_chunks;
+
+ # if none of the multipliers came from an order_by (guaranteed to have been combined
+ # with a limit) - easy - just slap a group_by to simulate a collape and be on our way
+ if (
+ ! $inner_aliastypes->{ordering}
+ or
+ ! first { $inner_aliastypes->{ordering}{$_} } @multipliers
+ ) {
+ my $unprocessed_order_chunks;
+ ($inner_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection (
+ $inner_from, $inner_select, $inner_attrs->{order_by}
+ );
+
+ $self->throw_exception (
+ 'A required group_by clause could not be constructed automatically due to a complex '
+ . 'order_by criteria. Either order_by columns only (no functions) or construct a suitable '
+ . 'group_by by hand'
+ ) if $unprocessed_order_chunks;
+ }
+ else {
+ # We need to order by external columns and group at the same time
+ # so we can calculate the proper limit
+ # This doesn't really make sense in SQL, however from DBICs point
+ # of view is rather valid (order the leftmost objects by whatever
+ # criteria and get the offset/rows many). There is a way around
+ # this however in SQL - we simply tae the direction of each piece
+ # of the foreign order and convert them to MIN(X) for ASC or MAX(X)
+ # for DESC, and group_by the root columns. The end result should be
+ # exactly what we expect
+
+ # FIXME REMOVE LATER - (just a sanity check)
+ if (defined ( my $impostor = first
+ { $_ ne $root_alias }
+ keys %{ $inner_aliastypes->{selecting} }
+ ) ) {
+ $self->throw_exception(sprintf
+ 'Unexpected inner selection during complex prefetch (%s)...',
+ join ', ', keys %{ $inner_aliastypes->{joining}{$impostor}{-seen_columns} || {} }
+ );
+ }
+
+ # supplement the main selection with pks if not already there,
+ # as they will have to be a part of the group_by to colapse
+ # things properly
+ my $cur_sel = { map { $_ => 1 } @$inner_select };
+ my @pks = map { "$root_alias.$_" } $root_node->{-rsrc}->primary_columns
+ or $self->throw_exception( sprintf
+ 'Unable to perform complex limited prefetch off %s without declared primary key',
+ $root_node->{-rsrc}->source_name,
+ );
+ for my $col (@pks) {
+ push @$inner_select, $col
+ unless $cur_sel->{$col}++;
+ }
+
+ # wrap any part of the order_by that "responds" to an ordering alias
+ # into a MIN/MAX
+ # FIXME - this code is a joke, will need to be completely rewritten in
+ # the DQ branch. But I need to push a POC here, otherwise the
+ # pesky tests won't pass
+ my $sql_maker = $self->sql_maker;
+ my ($lquote, $rquote, $sep) = map { quotemeta $_ } ($sql_maker->_quote_chars, $sql_maker->name_sep);
+ my $own_re = qr/ $lquote \Q$root_alias\E $rquote $sep | \b \Q$root_alias\E $sep /x;
+ my @order = @{$attrs->{order_by}};
+ my @order_chunks = map { ref $_ eq 'ARRAY' ? $_ : [ $_ ] } $sql_maker->_order_by_chunks (\@order);
+ $self->throw_exception ('Order By parsing failed...') if @order != @order_chunks;
+ for my $i (0 .. $#order) {
+ # skip ourselves, and anything that looks like a literal
+ next if $order_chunks[$i][0] =~ $own_re;
+ next if (ref $order[$i] and ref $order[$i] ne 'HASH');
+
+ my $is_desc = $order_chunks[$i][0] =~ s/\sDESC$//i;
+ $order_chunks[$i][0] =~ s/\sASC$//i;
+
+ $order[$i] = \[
+ sprintf(
+ '%s(%s)%s',
+ ($is_desc ? 'MAX' : 'MIN'),
+ $order_chunks[$i][0],
+ ($is_desc ? ' DESC' : ''),
+ ),
+ @ {$order_chunks[$i]} [ 1 .. $#{$order_chunks[$i]} ]
+ ];
+ }
+
+ $inner_attrs->{order_by} = \@order;
+ ($inner_attrs->{group_by}) = $self->_group_over_selection (
+ $inner_from, $inner_select, $inner_attrs->{order_by}
+ );
+ }
}
# we already optimized $inner_from above
push @outer_from, [
{
- -alias => $attrs->{alias},
+ -alias => $root_alias,
-rsrc => $root_node->{-rsrc},
- $attrs->{alias} => $inner_subq,
+ $root_alias => $inner_subq,
},
@{$from->[0]}[1 .. $#{$from->[0]}],
];
}
else {
@outer_from = {
- -alias => $attrs->{alias},
+ -alias => $root_alias,
-rsrc => $root_node->{-rsrc},
- $attrs->{alias} => $inner_subq,
+ $root_alias => $inner_subq,
};
}
$self->_resolve_aliastypes_from_select_args( $from, $outer_select, $where, $outer_attrs );
# unroll parents
- my ($outer_select_chain, $outer_restrict_chain) = map { +{
- map { $_ => 1 } map { values %$_} map { @{$_->{-parents}} } values %{ $outer_aliastypes->{$_} }
- } } qw/selecting restricting/;
+ my ($outer_select_chain, @outer_nonselecting_chains) = map { +{
+ map { $_ => 1 } map { values %$_} map { @{$_->{-parents}} } values %{ $outer_aliastypes->{$_} || {} }
+ } } qw/selecting restricting grouping ordering/;
# see what's left - throw away if not selecting/restricting
# also throw in a group_by if a non-selecting multiplier,
) {
push @outer_from, $j
}
- elsif ($outer_restrict_chain->{$alias}) {
+ elsif (first { $_->{$alias} } @outer_nonselecting_chains ) {
push @outer_from, $j;
$need_outer_group_by ||= $outer_aliastypes->{multiplying}{$alias} ? 1 : 0;
}
}
- if ($need_outer_group_by and ! $outer_attrs->{group_by}) {
+ if ( $need_outer_group_by and $attrs->{_grouped_by_distinct} ) {
my $unprocessed_order_chunks;
($outer_attrs->{group_by}, $unprocessed_order_chunks) = $self->_group_over_selection (
my $to_scan = {
restricting => [
$sql_maker->_recurse_where ($where),
- $sql_maker->_parse_rs_attrs ({
- map { $_ => $attrs->{$_} } (qw/group_by having/)
- }),
+ $sql_maker->_parse_rs_attrs ({ having => $attrs->{having} }),
+ ],
+ grouping => [
+ $sql_maker->_parse_rs_attrs ({ group_by => $attrs->{group_by} }),
],
joining => [
$sql_maker->_recurse_from (
],
selecting => [
$sql_maker->_recurse_fields ($select),
- ( map { $_->[0] } $self->_extract_order_criteria ($attrs->{order_by}, $sql_maker) ),
+ ],
+ ordering => [
+ map { $_->[0] } $self->_extract_order_criteria ($attrs->{order_by}, $sql_maker),
],
};
for my $piece (@{$to_scan->{$type}}) {
if (my @matches = $piece =~ /$al_re/g) {
$aliases_by_type->{$type}{$alias} ||= { -parents => $alias_list->{$alias}{-join_path}||[] };
- $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = 1
+ $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = "$alias.$_"
for grep { defined $_ } @matches;
}
}
if (my @matches = $piece =~ /$col_re/g) {
my $alias = $colinfo->{$col}{-source_alias};
$aliases_by_type->{$type}{$alias} ||= { -parents => $alias_list->{$alias}{-join_path}||[] };
- $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = 1
+ $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = $_
for grep { defined $_ } @matches;
}
}
);
}
+ for (keys %$aliases_by_type) {
+ delete $aliases_by_type->{$_} unless keys %{$aliases_by_type->{$_}};
+ }
+
return $aliases_by_type;
}