[Jifty-commit] r6279 - Jifty-DBI/branches/tisql-joins-refactoring/lib/Jifty/DBI

Jifty commits jifty-commit at lists.jifty.org
Thu Jan 29 07:52:14 EST 2009


Author: ruz
Date: Thu Jan 29 07:52:14 2009
New Revision: 6279

Modified:
   Jifty-DBI/branches/tisql-joins-refactoring/lib/Jifty/DBI/Tisql.pm

Log:
* first pass, it's deaddly broken and is not complete, but it's getting
  close for good

Modified: Jifty-DBI/branches/tisql-joins-refactoring/lib/Jifty/DBI/Tisql.pm
==============================================================================
--- Jifty-DBI/branches/tisql-joins-refactoring/lib/Jifty/DBI/Tisql.pm	(original)
+++ Jifty-DBI/branches/tisql-joins-refactoring/lib/Jifty/DBI/Tisql.pm	Thu Jan 29 07:52:14 2009
@@ -3,9 +3,11 @@
 use strict;
 use warnings;
 
-use Scalar::Util qw(refaddr blessed);
+use Scalar::Util qw(refaddr blessed weaken);
 
 use Data::Dumper;
+use Carp ();
+
 
 use Parse::BooleanLogic 0.07;
 my $parser = new Parse::BooleanLogic;
@@ -91,7 +93,7 @@
     }
     my $operand_cb = sub {
         return $self->parse_condition( 
-            $_[0], sub { $self->find_column( $_[0], $tree->{'aliases'} ) }
+            'query', $_[0], sub { $self->find_column( $_[0], $tree->{'aliases'} ) }
         );
     };
     $self->{'bindings'} = \@binds;
@@ -120,11 +122,11 @@
             $self->apply_query_tree( $element, $join, $ea );
             next;
         }
-        elsif ( ref $element ne 'HASH' ) {
+        elsif ( ref $element eq 'HASH' ) {
+            $self->apply_query_condition( $collection, $ea, $element, $join );
+        } else {
             die "wrong query tree";
         }
-
-        $self->apply_query_condition( $collection, $ea, $element, $join );
     }
     $collection->close_paren('tisql', $join);
 }
@@ -275,41 +277,49 @@
     $refers = $refers->new_item
         if UNIVERSAL::isa( $refers, 'Jifty::DBI::Collection' );
 
-    unless ( UNIVERSAL::isa( $refers, 'Jifty::DBI::Record' ) ) { 
+    unless ( UNIVERSAL::isa( $refers, 'Jifty::DBI::Record' ) ) {
         die "Column '". $column->name ."' refers to '"
             . (ref($refers) || $refers)
             ."' that is not record or collection";
     }
 
-    my $res = $collection->new_alias( $refers, 'LEFT' );
+    my $sql_alias = $meta->{'sql_alias'}
+        = $collection->new_alias( $refers, 'LEFT' );
+
     if ( $column->tisql ) {
-        $self->resolve_tisql_join( $res, $meta );
+        local $self->{'right_part_of_join'} = {
+            %$meta
+        };
+        $self->resolve_tisql_join( $sql_alias, $meta );
     } else {
-        $collection->limit(
-            leftjoin    => $res,
+        my %limit = (
             subclause   => 'tisql-join',
             alias       => $prev_alias,
             column      => $column->virtual? 'id' : $column->name,
             operator    => '=',
             quote_value => 0,
-            value       => $res .'.'. ($column->by || 'id')
+            value       => $sql_alias .'.'. ($column->by || 'id')
         );
+
+        if ( $self->{'inside_left_join'} ) {
+            $limit{'leftjoin'} = $sql_alias;
+        } else {
+            $limit{'leftjoin'} = $sql_alias;
+        }
+        $collection->limit( %limit );
     }
-    return $meta->{'sql_alias'} = $res;
+    return $sql_alias;
 }
 
-    use Data::Dumper; use Carp ();
-
 sub resolve_tisql_join {
     my $self = shift;
     my $alias = shift;
     my $meta = shift;
-    Test::More::diag( Carp::cluck( Dumper($meta) ) );
 
     my $tree = $parser->as_array(
         $meta->{'column'}->tisql,
-        operand_cb => sub { return $self->parse_condition( 
-            $_[0], sub { return $self->find_column(
+        operand_cb => sub { return $self->parse_condition(
+            'join', $_[0], sub { return $self->find_column(
                 $_[0],
                 {
                     '' => $meta->{'previous'},
@@ -348,60 +358,365 @@
         return 1;
     } );
 
-    $self->apply_query_tree( $tree, $alias );
+#    Test::More::diag( Dumper $tree );
 
-    return $alias;
+    $self->apply_join_tree( $tree, undef, $alias );
 }
 
-sub parse_condition {
+sub apply_join_tree {
+    my ($self, $tree, $ea, $join, @rest) = @_;
+    $ea ||= 'AND';
+
+    my $collection = $self->{'collection'};
+    $collection->open_paren('tisql', $join);
+    foreach my $element ( @$tree ) {
+        unless ( ref $element ) {
+            $ea = $element;
+            next;
+        }
+        elsif ( ref $element eq 'ARRAY' ) {
+            $self->apply_join_tree( $element, $ea, $join, @rest );
+            next;
+        }
+        elsif ( ref $element eq 'HASH' ) {
+            Test::More::diag( Dumper($element) );
+            if ( $element->{'lhs'}{'string'} =~ /^([^.]+)\.[^.]+\./ ) {
+                # it's subjoin in join: a column described using tisql and has more
+                # than just target.x = .source, but something like: target.x.y = ...
+
+                # here we have
+                my $alias = $1;
+
+                $collection->open_paren('tisql', $join);
+
+                die "here we are";
+
+                $collection->close_paren('tisql', $join);
+            }
+            $self->apply_join_condition( $collection, $ea, $element, $join, @rest );
+        } else {
+            die "wrong query tree";
+        }
+    }
+    $collection->close_paren('tisql', $join);
+}
+
+sub apply_join_condition {
+    my ($self, $collection, $ea, $condition, $join) = @_;
+
+    die "left hand side must be always column specififcation"
+        unless ref $condition->{'lhs'} eq 'HASH';
+
+
+    my $op = $condition->{'op'};
+    if ( $condition->{'prefix'} && $condition->{'prefix'} eq 'has no' ) {
+        die "'has' and 'has no' prefixes are only allowed in query, not in joins";
+    }
+
+    my %limit = (
+        subclause        => 'tisql',
+        leftjoin         => $join,
+        entry_aggregator => $ea,
+        alias            => $self->resolve_join( $condition->{'lhs'} ),
+        column           => $condition->{'lhs'}{'column'}->name,
+        operator         => $op,
+    );
+    if ( ref $condition->{'rhs'} eq 'HASH' ) {
+        $limit{'quote_value'} = 0;
+        $limit{'value'} =
+            $self->resolve_join( $condition->{'rhs'} )
+            .'.'. $condition->{'rhs'}{'column'}->name;
+    } else {
+        if ( ref $condition->{'rhs'} eq 'ARRAY' ) {
+            $parser->dq( $_ ) foreach @{ $condition->{'rhs'} };
+        } else {
+            $parser->dq( $condition->{'rhs'} );
+        }
+        $limit{'value'} = $condition->{'rhs'};
+    }
+
+    $collection->limit( %limit );
+}
+
+sub describe_join {
+    my $self  = shift;
+    my $model = shift;
+    my $via   = shift;
+
+    $model = UNIVERSAL::isa( $model, 'Jifty::DBI::Collection' )
+        ? $model->new_item
+        : $model;
+
+    my $column = $model->column( $via )
+        or die "no column";
+
+    my $refers_to = $column->refers_to->new;
+    $refers_to = $refers_to->new_item
+        if $refers_to->isa('Jifty::DBI::Collection');
+
+    my $tree;
+    if ( my $tisql = $column->tisql ) {
+        $tree = $parser->as_array( $tisql, operand_cb => sub {
+            return $self->parse_condition(
+                'join', $_[0], sub { $self->parse_column( $_[0] ) }
+            )
+        } );
+    } else {
+        $tree = [ {
+            type    => 'join',
+            op_type => 'col_op_col',
+            lhs => {
+                alias  => '',
+                chain  => [{ name => $via }],
+            },
+            op => '=',
+            rhs => {
+                alias  => $via,
+                chain  => [{ name => $column->by || 'id' }],
+            },
+        } ];
+        foreach ( map $tree->[0]{$_}, qw(lhs rhs) ) {
+            $_->{'chain'}[0]{'string'} = $_->{'alias'} .'.'. $_->{'chain'}[0]{'name'};
+            $_->{'string'} = $_->{'chain'}[-1]{'string'};
+        }
+        $tree->[0]{'string'} =
+            join ' ',
+            $tree->[0]{'lhs'}{'string'},
+            $tree->[0]{'op'},
+            $tree->[0]{'rhs'}{'string'};
+    }
+    my $res = {
+        left => {
+            model => $model,
+            column => $column,
+        },
+        right => {
+            model => $refers_to,
+        },
+        tree => $tree,
+    };
+    return $res;
+}
+
+sub linearize_join {
     my $self = shift;
-    my $string = shift;
-    my $cb = shift;
+    my $join = shift;
+    my $inverse = shift;
+    my $attach_to = shift;
+    my $place_of_attachment = shift;
+
+    my @res (
+        $attach_to || { model => $join->{'left'}{'model'} },
+        { model => $join->{'right'}{'model'} },
+    );
+    my ($left, $right) = @res;
 
-    if ( $string =~ /^(has(\s+no)?\s+)?($re_column)\s*($re_sql_op_bin)\s*($re_value_ph_b)$/io ) {
-        my ($lhs, $op, $rhs) = ($cb->($3), $4, $5);
-        $parser->fq( $rhs = shift @{ $self->{'bindings'} } ) if $rhs eq '?';
-        my $prefix;
-        $prefix = 'has' if $1;
-        $prefix .= ' no' if $2;
-        die "Last column in '". $lhs->{'string'} ."' is virtual and can not be used in condition '$string'" 
-            if $lhs->{'column'}->virtual;
-        return { string => $string, prefix => $prefix, lhs => $lhs, op => $op, rhs => $rhs };
-    }
-    elsif ( $string =~ /^($re_column)\s*($re_sql_op_un)$/o ) {
-        my ($lhs, $op, $rhs) = ($cb->($1), $2, $3);
-        ($op, $rhs) = split /\s*(?=null)/i, $op;
-        die "Last column in '". $lhs->{'string'} ."' is virtual and can not be used in condition '$string'" 
-            if $lhs->{'column'}->virtual;
-        return { string => $string, lhs => $lhs, op => $op, rhs => $rhs };
-    }
-    elsif ( $string =~ /^(has(\s+no)?\s+)?($re_column)\s*($re_sql_op_bin)\s*($re_column)$/o ) {
-        my ($lhs, $op, $rhs) = ($cb->($3), $4, $cb->($5));
-        my $prefix;
-        $prefix = 'has' if $1;
-        $prefix .= ' no' if $2;
-        die "Last column in '". $lhs->{'string'} ."' is virtual and can not be used in condition '$string'" 
-            if $lhs->{'column'}->virtual;
-        die "Last column in '". $rhs->{'string'} ."' is virtual and can not be used in condition '$string'" 
-            if $rhs->{'column'}->virtual;
-        return { string => $string, prefix => $prefix, lhs => $lhs, op => $op, rhs => $rhs };
+    my $transfer_short = sub {
+        my %new = ();
+        $new{'table'} = $_[0]->{'alias'}? $right : $left;
+        weaken($new{'table'});
+        $new{'column'} = $_[0]->{'chain'}[0]{'name'};
+        return \%new;
+    };
+
+
+    my ($tree, $node, @pnodes);
+    my %callback;
+    $callback{'open_paren'} = sub {
+        push @pnodes, $node;
+        push @{ $pnodes[-1] }, $node = []
+    };
+    $callback{'close_paren'} = sub { $node = pop @pnodes };
+    $callback{'operator'}    = sub { push @$node, $_[0] };
+    $callback{'operand'}     = sub {
+        my $cond = $_[0];
+        my %new_cond = %$cond;
+
+        if ( $cond->{'op_type'} eq 'col_op_col' ) {
+            if ( !$cond->{'lhs'}{'is_long'} && !$cond->{'rhs'}{'is_long'} ) {
+                foreach my $side (qw(lhs rhs)) {
+                    $new_cond{$side} = $transfer_short->( $cond->{$side} );
+                }
+            }
+        } else {
+            unless ( $cond->{'lhs'}{'is_long'} ) {
+                $new_cond{'lhs'} = $transfer_short->( $cond->{'lhs'} );
+            } else {
+                my @chain = @{ $cond->{'lhs'}{'chain'} };
+                my $last_column = pop @chain;
+
+                my $conditions = [];
+
+                my $model = ($cond->{'lhs'}{'alias'}? $right : $left)->{'model'};
+                foreach my $ref ( @chain ) {
+                    my $description = $self->describe_join( $model => $ref->{'name'} );
+                    my $linear = $self->linearize_join(
+                        $description,
+                        $cond->{'lhs'}{'alias'}
+                            ? ('inverse', $right, $conditions)
+                            : (undef, $left)
+                    );
+
+                    $model = $model->column( $ref->{'name'} )->refers_to->new;
+                }
+            }
+        }
+        push @$node, \%new_cond;
+    };
+
+    $tree = $node = [];
+    $parser->walk( $join->{'tree'}, \%callback );
+
+    @res = reverse @res if $inverse;
+
+    if ( $place_of_attachment ) {
+        push @{ $place_of_attachment }, $tree;
+    } else {
+        $res[-1]{'conditions'} = $tree;
+    }
+    return \@res;
+}
+
+sub _linearize_join {
+    my ($self, $tree, $ea, $join, @rest) = @_;
+    $ea ||= 'AND';
+
+    my $collection = $self->{'collection'};
+    $collection->open_paren('tisql', $join);
+    foreach my $element ( @$tree ) {
+        unless ( ref $element ) {
+            $ea = $element;
+            next;
+        }
+        elsif ( ref $element eq 'ARRAY' ) {
+            $self->apply_join_tree( $element, $ea, $join, @rest );
+            next;
+        }
+        elsif ( ref $element eq 'HASH' ) {
+            Test::More::diag( Dumper($element) );
+            if ( $element->{'lhs'}{'string'} =~ /^([^.]+)\.[^.]+\./ ) {
+                # it's subjoin in join: a column described using tisql and has more
+                # than just target.x = .source, but something like: target.x.y = ...
+
+                # here we have
+                my $alias = $1;
+
+                $collection->open_paren('tisql', $join);
+
+                die "here we are";
+
+                $collection->close_paren('tisql', $join);
+            }
+            $self->apply_join_condition( $collection, $ea, $element, $join, @rest );
+        } else {
+            die "wrong query tree";
+        }
+    }
+    $collection->close_paren('tisql', $join);
+}
+
+sub parse_condition {
+    my ($self, $type, $string, $cb) = @_;
+
+    my %res = (
+        string   => $string,
+        type     => $type,
+        op_type  => undef,    # 'col_op', 'col_op_val' or 'col_op_col'
+        modifier => '',       # '', 'has' or 'has no'
+        lhs      => undef,
+        op       => undef,
+        rhs      => undef,
+    );
+
+    if ( $type eq 'query' ) {
+        # TODO: query can not have placeholders %##
+        if ( $string =~ /^(has(\s+no)?\s+)?($re_column)\s*($re_sql_op_bin)\s*($re_value_ph_b)$/io ) {
+            $res{'modifier'} = $2? 'has no': $1? 'has': '';
+            @res{qw(op_type lhs op rhs)} = ('col_op_val', $cb->($3), $4, $5);
+        }
+        elsif ( $string =~ /^($re_column)\s*($re_sql_op_un)$/o ) {
+            my ($lhs, $op) = ($cb->($1), $2);
+            @res{qw(op_type lhs op rhs)} = ('col_op', $lhs, split /\s*(?=null)/i, $op );
+        }
+        elsif ( $string =~ /^(has(\s+no)?\s+)?($re_column)\s*($re_sql_op_bin)\s*($re_column)$/o ) {
+            $res{'modifier'} = $2? 'has no': $1? 'has': '';
+            @res{qw(op_type lhs op rhs)} = ('col_op_col', $cb->($3), $4, $cb->($5));
+        }
+        elsif ( $string =~ /^has(\s+no)?\s+($re_column)$/o ) {
+            @res{qw(op_type lhs op rhs)} = ('col_op', $cb->( $2 .'.id' ), $1? 'IS': 'IS NOT', 'NULL');
+        }
+        else {
+            die "$string is not a tisql $type condition";
+        }
     }
-    elsif ( $string =~ /^has(\s+no)?\s+($re_column)$/o ) {
-        return { string => $string, lhs => $cb->( $2 .'.id' ), op => $1? 'IS': 'IS NOT', rhs => 'NULL' };
+    elsif ( $type eq 'join' ) {
+        # TODO: join can not have bindings (?)
+        if ( $string =~ /^($re_column)\s*($re_sql_op_bin)\s*($re_value_ph_b)$/io ) {
+            @res{qw(op_type lhs op rhs)} = ('col_op_val', $cb->($1), $2, $3);
+        }
+        elsif ( $string =~ /^($re_column)\s*($re_sql_op_un)$/o ) {
+            my ($lhs, $op) = ($cb->($1), $2);
+            @res{qw(op_type lhs op rhs)} = ('col_op', $lhs, split /\s*(?=null)/i, $op );
+        }
+        elsif ( $string =~ /^($re_column)\s*($re_sql_op_bin)\s*($re_column)$/o ) {
+            @res{qw(op_type lhs op rhs)} = ('col_op_col', $cb->($1), $2, $cb->($3));
+        }
+        else {
+            die "$string is not a tisql $type condition";
+        }
     }
     else {
-        die "$string is not a tisql condition";
+        die "$type is not valid type of a condition";
     }
+    return \%res;
 }
 
+sub check_query_condition {
+    my ($self, $cond) = @_;
+
+    die "Last column in '". $cond->{'lhs'}{'string'} ."' is virtual" 
+        if $cond->{'lhs'}{'column'}->virtual;
+
+    if ( $cond->{'op_type'} eq 'col_op_col' ) {
+        die "Last column in '". $cond->{'rhs'}{'string'} ."' is virtual" 
+            if $cond->{'rhs'}{'column'}->virtual;
+    }
+
+    return $cond;
+}
+
+
+# returns something like:
+# {
+#   'string'  => 'nodes.attr{"category"}.value',
+#   'alias'   => 'nodes',                            # alias or ''
+#   'is_long' => 1,                                  # 1 or 0
+#   'chain'   => [
+#        {
+#          'name' => 'attr',
+#          'string' => 'nodes.attr{"category"}',
+#          'placeholders' => ['"category"'],
+#        },
+#        {
+#          'name' => 'value',
+#          'string' => 'nodes.attr{"category"}.value'
+#        }
+#   ],
+# }
+# no look ups, everything returned as is,
+# even placeholders' strings are not de-escaped
+
 sub parse_column {
     my $self = shift;
     my $string = shift;
 
     my (%res, @columns);
+    $res{'string'} = $string;
     ($res{'alias'}, @columns) = split /\.($re_field$re_ph_access*)/o, $string;
     @columns = grep defined && length, @columns;
-    my $prev;
+    $res{'is_long'} = @columns > 1? 1 : 0;
+
+    my $prev = $res{'alias'};
     foreach my $col (@columns) {
         my $string = $col;
         $col =~ s/^($re_field)//;
@@ -410,7 +725,7 @@
         @phs = grep !defined || length, @phs;
         $col = {
             name => $field,
-            string => ($prev? $prev->{'string'} : $res{'alias'}) .".$string",
+            string => $prev .".$string",
         };
         $col->{'placeholders'} = \@phs if @phs;
         foreach my $ph ( grep defined, @phs ) {
@@ -418,7 +733,7 @@
                 $ph = $1;
             }
             elsif ( $ph eq '?' ) {
-                $parser->fq( $ph = shift @{ $self->{'bindings'} } );
+                $ph = '?';
             }
             else {
                 my @values;
@@ -428,7 +743,7 @@
                 $ph = \@values;
             }
         }
-        $prev = $col;
+        $prev = $col->{'string'};
     }
     $res{'chain'} = \@columns;
     return \%res;
@@ -459,7 +774,6 @@
     }
 
     my @chain = @{ $meta->{'chain'} };
-
     while ( my $joint = shift @chain ) {
         my $name = $joint->{'name'};
         my $column =
@@ -533,7 +847,7 @@
     my $conditions = $parser->as_array(
         $column->tisql,
         operand_cb => sub {
-            return $self->parse_condition( $_[0], $column_cb )
+            return $self->parse_condition( 'join', $_[0], $column_cb )
         },
     );
     $conditions = [


More information about the Jifty-commit mailing list