[Jifty-commit] r3505 - Jifty-DBI/trunk/lib/Jifty/DBI
jifty-commit at lists.jifty.org
jifty-commit at lists.jifty.org
Fri Jun 15 08:55:57 EDT 2007
Author: ruz
Date: Fri Jun 15 08:55:44 2007
New Revision: 3505
Modified:
Jifty-DBI/trunk/lib/Jifty/DBI/Handle.pm
Log:
* add LEFT joins optimizer
* all tests now pass
Modified: Jifty-DBI/trunk/lib/Jifty/DBI/Handle.pm
==============================================================================
--- Jifty-DBI/trunk/lib/Jifty/DBI/Handle.pm (original)
+++ Jifty-DBI/trunk/lib/Jifty/DBI/Handle.pm Fri Jun 15 08:55:44 2007
@@ -949,6 +949,8 @@
my $self = shift;
my $sb = shift;
+ $self->_optimize_joins( collection => $sb );
+
my $join_clause = CORE::join " CROSS JOIN ", ($sb->table ." main"), @{ $sb->{'aliases'} };
my %processed = map { /^\S+\s+(\S+)$/; $1 => 1 } @{ $sb->{'aliases'} };
$processed{'main'} = 1;
@@ -985,6 +987,167 @@
return $join_clause;
}
+sub _optimize_joins {
+ my $self = shift;
+ my %args = ( collection => undef, @_ );
+ my $joins = $args{'collection'}->{'leftjoins'};
+
+ my %processed = map { /^\S+\s+(\S+)$/; $1 => 1 } @{ $args{'collection'}->{'aliases'} };
+ $processed{ $_ }++ foreach grep $joins->{ $_ }{'type'} ne 'LEFT', keys %$joins;
+ $processed{'main'}++;
+
+ my @ordered;
+ # get a @list of joins that have not been processed yet, but depend on processed join
+ # if we are talking about forest then we'll get the second level of the forest,
+ # but we should process nodes on this level at the end, so we build FILO ordered list.
+ # finally we'll get ordered list with leafes in the beginning and top most nodes at
+ # the end.
+ while ( my @list = grep !$processed{ $_ }
+ && $processed{ $joins->{ $_ }{'depends_on'} }, keys %$joins )
+ {
+ unshift @ordered, @list;
+ $processed{ $_ }++ foreach @list;
+ }
+
+ foreach my $join ( @ordered ) {
+ next if $self->may_be_null( collection => $args{'collection'}, alias => $join );
+
+ $joins->{ $join }{'alias_string'} =~ s/^\s*LEFT\s+/ /i;
+ $joins->{ $join }{'type'} = 'NORMAL';
+ }
+
+}
+
+=head2 may_be_null
+
+Takes a C<collection> and C<alias> in a hash and returns
+true if restrictions of the query allow NULLs in a table joined with
+the alias, otherwise returns false value which means that you can
+use normal join instead of left for the aliased table.
+
+Works only for queries have been built with L<Jifty::DBI::Collection/join> and
+L<Jifty::DBI::Collection/limit> methods, for other cases return true value to
+avoid fault optimizations.
+
+=cut
+
+sub may_be_null {
+ my $self = shift;
+ my %args = (collection => undef, alias => undef, @_);
+ # if we have at least one subclause that is not generic then we should get out
+ # of here as we can't parse subclauses
+ return 1 if grep $_ ne 'generic_restrictions', keys %{ $args{'collection'}->{'subclauses'} };
+
+ # build full list of generic conditions
+ my @conditions;
+ foreach ( grep @$_, values %{ $args{'collection'}->{'restrictions'} } ) {
+ push @conditions, 'AND' if @conditions;
+ push @conditions, '(', @$_, ')';
+ }
+
+ # find tables that depends on this alias and add their join conditions
+ foreach my $join ( values %{ $args{'collection'}->{'leftjoins'} } ) {
+ # left joins on the left side so later we'll get 1 AND x expression
+ # which equal to x, so we just skip it
+ next if $join->{'type'} eq 'LEFT';
+ next unless $join->{'depends_on'} eq $args{'alias'};
+
+ my @tmp = map { ('(', @$_, ')', $join->{'entry_aggregator'}) } values %{ $join->{'criteria'} };
+ pop @tmp;
+
+ @conditions = ('(', @conditions, ')', 'AND', '(', @tmp ,')');
+
+ }
+ return 1 unless @conditions;
+
+ # replace conditions with boolean result: 1 - allow nulls, 0 - doesn't
+ foreach ( splice @conditions ) {
+ unless ( ref $_ ) {
+ push @conditions, $_;
+ } elsif ( $_->{'column'} =~ /^\Q$args{'alias'}./ ) {
+ # only operator IS allows NULLs in the aliased table
+ push @conditions, lc $_->{'operator'} eq 'is';
+ } elsif ( $_->{'value'} && $_->{'value'} =~ /^\Q$args{'alias'}./ ) {
+ # right operand is our alias, such condition don't allow NULLs
+ push @conditions, 0;
+ } else {
+ # conditions on other aliases
+ push @conditions, 1;
+ }
+ }
+
+ # returns index of closing paren by index of openning paren
+ my $closing_paren = sub {
+ my $i = shift;
+ my $count = 0;
+ for ( ; $i < @conditions; $i++ ) {
+ if ( $conditions[$i] eq '(' ) {
+ $count++;
+ }
+ elsif ( $conditions[$i] eq ')' ) {
+ $count--;
+ }
+ return $i unless $count;
+ }
+ die "lost in parens";
+ };
+
+ # solve boolean expression we have, an answer is our result
+ my @tmp = ();
+ while ( defined ( my $e = shift @conditions ) ) {
+ #warn "@tmp >>>$e<<< @conditions";
+ return $e if !@conditions && !@tmp;
+
+ unless ( $e ) {
+ if ( $conditions[0] eq ')' ) {
+ push @tmp, $e;
+ next;
+ }
+
+ my $aggreg = uc shift @conditions;
+ if ( $aggreg eq 'OR' ) {
+ # 0 OR x == x
+ next;
+ } elsif ( $aggreg eq 'AND' ) {
+ # 0 AND x == 0
+ my $close_p = $closing_paren->(0);
+ splice @conditions, 0, $close_p + 1, (0);
+ } else {
+ die "lost @tmp >>>$e $aggreg<<< @conditions";
+ }
+ } elsif ( $e eq '1' ) {
+ if ( $conditions[0] eq ')' ) {
+ push @tmp, $e;
+ next;
+ }
+
+ my $aggreg = uc shift @conditions;
+ if ( $aggreg eq 'OR' ) {
+ # 1 OR x == 1
+ my $close_p = $closing_paren->(0);
+ splice @conditions, 0, $close_p + 1, (1);
+ } elsif ( $aggreg eq 'AND' ) {
+ # 1 AND x == x
+ next;
+ } else {
+ die "lost @tmp >>>$e $aggreg<<< @conditions";
+ }
+ } elsif ( $e eq '(' ) {
+ if ( $conditions[1] eq ')' ) {
+ splice @conditions, 1, 1;
+ } else {
+ push @tmp, $e;
+ }
+ } elsif ( $e eq ')' ) {
+ unshift @conditions, @tmp, $e;
+ @tmp = ();
+ } else {
+ die "lost: @tmp >>>$e<<< @conditions";
+ }
+ }
+ return 1;
+}
+
=head2 distinct_query STATEMENTREF
takes an incomplete SQL SELECT statement and massages it to return a DISTINCT result set.
More information about the Jifty-commit
mailing list