# We will need to fetch all native columns in the inner subquery, which may
# be a part of an *outer* join condition, or an order_by (which needs to be
- # preserved outside)
+ # preserved outside), or wheres. In other words everything but the inner
+ # selector
# We can not just fetch everything because a potential has_many restricting
# join collapse *will not work* on heavy data types.
- my $connecting_aliastypes = $self->_resolve_aliastypes_from_select_args(
- $from,
- undef,
- $where,
- $inner_attrs
- );
+ my $connecting_aliastypes = $self->_resolve_aliastypes_from_select_args({
+ %$inner_attrs,
- select => [],
++ select => undef,
+ });
for (sort map { keys %{$_->{-seen_columns}||{}} } map { values %$_ } values %$connecting_aliastypes) {
my $ci = $colinfo->{$_} or next;
# generate sql chunks
my $to_scan = {
restricting => [
- ($where
- ? ($sql_maker->_recurse_where($where))[0]
- $sql_maker->_recurse_where ($attrs->{where}),
- $sql_maker->_parse_rs_attrs ({ having => $attrs->{having} }),
++ ($attrs->{where}
++ ? ($sql_maker->_recurse_where($attrs->{where}))[0]
+ : ()
+ ),
+ ($attrs->{having}
+ ? ($sql_maker->_recurse_where($attrs->{having}))[0]
+ : ()
+ ),
],
grouping => [
- $sql_maker->_parse_rs_attrs ({ group_by => $attrs->{group_by} }),
+ ($attrs->{group_by}
+ ? ($sql_maker->_render_sqla(group_by => $attrs->{group_by}))[0]
+ : (),
+ )
],
joining => [
$sql_maker->_recurse_from (
),
],
selecting => [
- ($select
- ? ($sql_maker->_render_sqla(select_select => $select))[0]
- $sql_maker->_recurse_fields ($attrs->{select}),
++ ($attrs->{select}
++ ? ($sql_maker->_render_sqla(select_select => $attrs->{select}))[0]
+ : ()),
],
ordering => [
map { $_->[0] } $self->_extract_order_criteria ($attrs->{order_by}, $sql_maker),
}
}
- # add any order_by parts *from the main source* that are not already
- # present in the group_by
- # we need to be careful not to add any named functions/aggregates
- # i.e. order_by => [ ... { count => 'foo' } ... ]
- my @leftovers;
- for ($self->_extract_order_criteria($attrs->{order_by})) {
- my @order_by = $self->_extract_order_criteria($attrs->{order_by})
++ my $sql_maker = $self->sql_maker;
++ my @order_by = $self->_extract_order_criteria($attrs->{order_by}, $sql_maker)
+ or return (\@group_by, $attrs->{order_by});
+
+ # add any order_by parts that are not already present in the group_by
+ # to maintain SQL cross-compatibility and general sanity
+ #
+ # also in case the original selection is *not* unique, or in case part
+ # of the ORDER BY refers to a multiplier - we will need to replace the
+ # skipped order_by elements with their MIN/MAX equivalents as to maintain
+ # the proper overall order without polluting the group criteria (and
+ # possibly changing the outcome entirely)
+
- my ($leftovers, $sql_maker, @new_order_by, $order_chunks, $aliastypes);
++ my ($leftovers, @new_order_by, $order_chunks, $aliastypes);
+
+ my $group_already_unique = $self->_columns_comprise_identifying_set($colinfos, \@group_by);
+
+ for my $o_idx (0 .. $#order_by) {
+
+ # if the chunk is already a min/max function - there is nothing left to touch
+ next if $order_by[$o_idx][0] =~ /^ (?: min | max ) \s* \( .+ \) $/ix;
+
# only consider real columns (for functions the user got to do an explicit group_by)
- if (@$_ != 1) {
- push @leftovers, $_;
- next;
+ my $chunk_ci;
+ if (
+ @{$order_by[$o_idx]} != 1
+ or
+ # only declare an unknown *plain* identifier as "leftover" if we are called with
+ # aliastypes to examine. If there are none - we are still in _resolve_attrs, and
+ # can just assume the user knows what they want
+ ( ! ( $chunk_ci = $colinfos->{$order_by[$o_idx][0]} ) and $attrs->{_aliastypes} )
+ ) {
+ push @$leftovers, $order_by[$o_idx][0];
}
- my $chunk = $_->[0];
- if (
- !$colinfos->{$chunk}
+ next unless $chunk_ci;
+
+ # no duplication of group criteria
+ next if $group_index{$chunk_ci->{-fq_colname}};
+
+ $aliastypes ||= (
+ $attrs->{_aliastypes}
or
- $colinfos->{$chunk}{-source_alias} ne $attrs->{alias}
+ $self->_resolve_aliastypes_from_select_args({
+ from => $attrs->{from},
+ order_by => $attrs->{order_by},
+ })
+ ) if $group_already_unique;
+
+ # check that we are not ordering by a multiplier (if a check is requested at all)
+ if (
+ $group_already_unique
+ and
+ ! $aliastypes->{multiplying}{$chunk_ci->{-source_alias}}
+ and
+ ! $aliastypes->{premultiplied}{$chunk_ci->{-source_alias}}
) {
- push @leftovers, $_;
- next;
+ push @group_by, $chunk_ci->{-fq_colname};
+ $group_index{$chunk_ci->{-fq_colname}}++
}
+ else {
+ # We need to order by external columns without adding them to the group
+ # (eiehter a non-unique selection, or a multi-external)
+ #
+ # This doesn't really make sense in SQL, however from DBICs point
+ # of view is rather valid (e.g. 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 external 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 - 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
+ # wrap any part of the order_by that "responds" to an ordering alias
+ # into a MIN/MAX
- $sql_maker ||= $self->sql_maker;
- $order_chunks ||= [
- map { ref $_ eq 'ARRAY' ? $_ : [ $_ ] } $sql_maker->_order_by_chunks($attrs->{order_by})
- ];
+
- my ($chunk, $is_desc) = $sql_maker->_split_order_chunk($order_chunks->[$o_idx][0]);
++ $order_chunks ||= do {
++ my @c;
++ my $dq_node = $sql_maker->converter->_order_by_to_dq($attrs->{order_by});
+
- $new_order_by[$o_idx] = \[
- sprintf( '%s( %s )%s',
- ($is_desc ? 'MAX' : 'MIN'),
- $chunk,
- ($is_desc ? ' DESC' : ''),
- ),
- @ {$order_chunks->[$o_idx]} [ 1 .. $#{$order_chunks->[$o_idx]} ]
- ];
++ while (is_Order($dq_node)) {
++ push @c, {
++ is_desc => $dq_node->{reverse},
++ dq_node => $dq_node->{by},
++ };
++
++ @{$c[-1]}{qw(sql bind)} = $sql_maker->_render_dq($dq_node->{by});
++
++ $dq_node = $dq_node->{from};
++ }
+
- $chunk = $colinfos->{$chunk}{-fq_colname};
- push @group_by, $chunk unless $group_index{$chunk}++;
++ \@c;
++ };
++
++ $new_order_by[$o_idx] = {
++ ($order_chunks->[$o_idx]{is_desc} ? '-desc' : '-asc') => \[
++ sprintf ( '%s( %s )',
++ ($order_chunks->[$o_idx]{is_desc} ? 'MAX' : 'MIN'),
++ $order_chunks->[$o_idx]{sql},
++ ),
++ @{ $order_chunks->[$o_idx]{bind} || [] }
++ ]
++ };
+ }
}
- return wantarray
- ? (\@group_by, (@leftovers ? \@leftovers : undef) )
- : \@group_by
- ;
+ $self->throw_exception ( sprintf
+ 'A required group_by clause could not be constructed automatically due to a complex '
+ . 'order_by criteria (%s). Either order_by columns only (no functions) or construct a suitable '
+ . 'group_by by hand',
+ join ', ', map { "'$_'" } @$leftovers,
+ ) if $leftovers;
+
+ # recreate the untouched order parts
+ if (@new_order_by) {
- $new_order_by[$_] ||= \ $order_chunks->[$_] for ( 0 .. $#$order_chunks );
++ $new_order_by[$_] ||= {
++ ( $order_chunks->[$_]{is_desc} ? '-desc' : '-asc' )
++ => \ $order_chunks->[$_]{dq_node}
++ } for ( 0 .. $#$order_chunks );
+ }
+
+ return (
+ \@group_by,
+ (@new_order_by ? \@new_order_by : $attrs->{order_by} ), # same ref as original == unchanged
+ );
}
sub _resolve_ident_sources {
sub _order_by_is_stable {
my ($self, $ident, $order_by, $where) = @_;
- my $colinfo = $self->_resolve_column_info($ident, [
+ my @cols = (
- (map { $_->[0] } $self->_extract_order_criteria($order_by)),
+ (map { $_->[0] } $self->_extract_order_criteria($order_by, undef, 1)),
$where ? @{$self->_extract_fixed_condition_columns($where)} :(),
- ]);
+ ) or return undef;
+
+ my $colinfo = $self->_resolve_column_info($ident, \@cols);
+
+ return keys %$colinfo
+ ? $self->_columns_comprise_identifying_set( $colinfo, \@cols )
+ : undef
+ ;
+ }
- return undef unless keys %$colinfo;
+ sub _columns_comprise_identifying_set {
+ my ($self, $colinfo, $columns) = @_;
my $cols_per_src;
- $cols_per_src->{$_->{-source_alias}}{$_->{-colname}} = $_ for values %$colinfo;
+ $cols_per_src -> {$_->{-source_alias}} -> {$_->{-colname}} = $_
+ for grep { defined $_ } @{$colinfo}{@$columns};
for (values %$cols_per_src) {
my $src = (values %$_)[0]->{-result_source};