DBIx::Class::Storage::DBIHacks;
#
-# This module contains code that should never have seen the light of day,
-# does not belong in the Storage, or is otherwise unfit for public
-# display. The arrival of SQLA2 should immediately obsolete 90% of this
+# This module contains code supporting a battery of special cases and tests for
+# many corner cases pushing the envelope of what DBIC can do. When work on
+# these utilities began in mid 2009 (51a296b402c) it wasn't immediately obvious
+# that these pieces, despite their misleading on-first-sighe-flakiness, will
+# become part of the generic query rewriting machinery of DBIC, allowing it to
+# both generate and process queries representing incredibly complex sets with
+# reasonable efficiency.
+#
+# Now (end of 2015), more than 6 years later the routines in this class have
+# stabilized enough, and are meticulously covered with tests, to a point where
+# an effort to formalize them into user-facing APIs might be worthwhile.
+#
+# An implementor working on publicizing and/or replacing the routines with a
+# more modern SQL generation framework should keep in mind that pretty much all
+# existing tests are constructed on the basis of real-world code used in
+# production somewhere.
+#
+# Please hack on this responsibly ;)
#
use strict;
use base 'DBIx::Class::Storage';
use mro 'c3';
-use List::Util 'first';
use Scalar::Util 'blessed';
-use DBIx::Class::_Util qw(UNRESOLVABLE_CONDITION serialize);
+use DBIx::Class::_Util qw(UNRESOLVABLE_CONDITION serialize dump_value);
use SQL::Abstract qw(is_plain_value is_literal_value);
use DBIx::Class::Carp;
use namespace::clean;
my $inner_subq = do {
# must use it here regardless of user requests (vastly gentler on optimizer)
- local $self->{_use_join_optimizer} = 1;
+ local $self->{_use_join_optimizer} = 1
+ unless $self->{_use_join_optimizer};
# throw away multijoins since we def. do not care about those inside the subquery
# $inner_aliastypes *will* be redefined at this point
) {
push @outer_from, $j
}
- elsif (first { $_->{$alias} } @outer_nonselecting_chains ) {
+ elsif (grep { $_->{$alias} } @outer_nonselecting_chains ) {
push @outer_from, $j;
$may_need_outer_group_by ||= $outer_aliastypes->{multiplying}{$alias} ? 1 : 0;
}
});
}
- # This is totally horrific - the {where} ends up in both the inner and outer query
- # Unfortunately not much can be done until SQLA2 introspection arrives, and even
- # then if where conditions apply to the *right* side of the prefetch, you may have
- # to both filter the inner select (e.g. to apply a limit) and then have to re-filter
- # the outer select to exclude joins you didn't want in the first place
+ # FIXME: The {where} ends up in both the inner and outer query, i.e. *twice*
+ #
+ # This is rather horrific, and while we currently *do* have enough
+ # introspection tooling available to attempt a stab at properly deciding
+ # whether or not to include the where condition on the outside, the
+ # machinery is still too slow to apply it here.
+ # Thus for the time being we do not attempt any sanitation of the where
+ # clause and just pass it through on both sides of the subquery. This *will*
+ # be addressed at a later stage, most likely after folding the SQL generator
+ # into SQLMaker proper
#
# OTOH it can be seen as a plus: <ash> (notes that this query would make a DBA cry ;)
+ #
return $outer_attrs;
}
+# This is probably the ickiest, yet most relied upon part of the codebase:
+# this is the place where we take arbitrary SQL input and break it into its
+# constituent parts, making sure we know which *sources* are used in what
+# *capacity* ( selecting / restricting / grouping / ordering / joining, etc )
+# Although the method is pretty horrific, the worst thing that can happen is
+# for a classification failure, which in turn will result in a vocal exception,
+# and will lead to a relatively prompt fix.
+# The code has been slowly improving and is covered with a formiddable battery
+# of tests, so can be considered "reliably stable" at this point (Oct 2015).
+#
+# A note to implementors attempting to "replace" this - keep in mind that while
+# there are multiple optimization avenues, the actual "scan literal elements"
+# part *MAY NEVER BE REMOVED*, even if it is limited only ot the (future) AST
+# nodes that are deemed opaque (i.e. contain literal expressions). The use of
+# blackbox literals is at this point firmly a user-facing API, and is one of
+# *the* reasons DBIC remains as flexible as it is. In other words, when working
+# on this keep in mind that the following is widespread and *encouraged* way
+# of using DBIC in the wild when push comes to shove:
#
-# I KNOW THIS SUCKS! GET SQLA2 OUT THE DOOR SO THIS CAN DIE!
+# $rs->search( {}, {
+# select => \[ $random, @stuff],
+# from => \[ $random, @stuff ],
+# where => \[ $random, @stuff ],
+# group_by => \[ $random, @stuff ],
+# order_by => \[ $random, @stuff ],
+# } )
+#
+# Various incarnations of the above are reflected in many of the tests. If one
+# gets to fail, you get to fix it. A "this is crazy, nobody does that" is not
+# acceptable going forward.
#
-# Due to a lack of SQLA2 we fall back to crude scans of all the
-# select/where/order/group attributes, in order to determine what
-# aliases are needed to fulfill the query. This information is used
-# throughout the code to prune unnecessary JOINs from the queries
-# in an attempt to reduce the execution time.
-# Although the method is pretty horrific, the worst thing that can
-# happen is for it to fail due to some scalar SQL, which in turn will
-# result in a vocal exception.
sub _resolve_aliastypes_from_select_args {
my ( $self, $attrs ) = @_;
( $_ = join ' ', map {
( ! defined $_ ) ? ()
- : ( length ref $_ ) ? (require Data::Dumper::Concise && $self->throw_exception(
- "Unexpected ref in scan-plan: " . Data::Dumper::Concise::Dumper($_)
- ))
+ : ( length ref $_ ) ? $self->throw_exception(
+ "Unexpected ref in scan-plan: " . dump_value $_
+ )
: ( $_ =~ /^\s*$/ ) ? ()
: $_
# 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
+ # both populated on the first loop over $o_idx
$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]);
+ # we reached that far - wrap any part of the order_by that "responded"
+ # to an ordering alias into a MIN/MAX
$new_order_by[$o_idx] = \[
sprintf( '%s( %s )%s',
- ($is_desc ? 'MAX' : 'MIN'),
+ $self->_minmax_operator_for_datatype($chunk_ci->{data_type}, $is_desc),
$chunk,
($is_desc ? ' DESC' : ''),
),
);
}
+sub _minmax_operator_for_datatype {
+ #my ($self, $datatype, $want_max) = @_;
+
+ $_[2] ? 'MAX' : 'MIN';
+}
+
sub _resolve_ident_sources {
my ($self, $ident) = @_;
# resultset {where} stacks
#
# FIXME - while relatively robust, this is still imperfect, one of the first
-# things to tackle with DQ
+# things to tackle when we get access to a formalized AST. Note that this code
+# is covered by a *ridiculous* amount of tests, so starting with porting this
+# code would be a rather good exercise
sub _collapse_cond {
my ($self, $where, $where_is_anded_array) = @_;
for (sort keys %$chunk) {
# Match SQLA 1.79 behavior
- if ($_ eq '') {
+ unless( length $_ ) {
is_literal_value($chunk->{$_})
? carp 'Hash-pairs consisting of an empty string with a literal are deprecated, use -and => [ $literal ] instead'
: $self->throw_exception("Supplying an empty left hand side argument is not supported in hash-pairs")
# Match SQLA 1.79 behavior
$self->throw_exception("Supplying an empty left hand side argument is not supported in array-pairs")
- if $where_is_anded_array and (! defined $chunk or $chunk eq '');
+ if $where_is_anded_array and (! defined $chunk or ! length $chunk);
push @pairs, $chunk, shift @pieces;
}
while (@$pairs) {
my ($lhs, $rhs) = splice @$pairs, 0, 2;
- if ($lhs eq '') {
+ if (! length $lhs) {
push @conds, $self->_collapse_cond($rhs);
}
elsif ( $lhs =~ /^\-and$/i ) {
# 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 }
+ . dump_value { in => { $lhs => $rhs }, out => $p }
);
}