package UR::DataSource::QueryPlan;
use strict;
use warnings;
use UR;
our $VERSION = "0.41"; # UR $VERSION;

# this class is an evolving attempt to formalize
# the blob of cached value used for query construction

class UR::DataSource::QueryPlan {
    is => 'UR::Value',
    id_by => [ 
        rule_template => { is => 'UR::BoolExpr::Template', id_by => ['subject_class_name','logic_type','logic_detail','constant_values_id'] }, 
        data_source   => { is => 'UR::DataSource', id_by => 'data_source_id' },
    ],
    has_transient => [
        _is_initialized => { is => 'Boolean' },

        needs_further_boolexpr_evaluation_after_loading => { is => 'Boolean' },
        
        # data tracked for the whole query by property,alias,join_id
        _delegation_chain_data                          => { is => 'HASH' },
        _alias_data                                     => { is => 'HASH' },
        _join_data                                      => { is => 'HASH' },

        # the old $alias_num
        _alias_count                                    => { is => 'Number' },
        
        # the old @sql_joins
        _db_joins                                       => { is => 'ARRAY' },
        
        # the new @obj_joins
        _obj_joins                                      => { is => 'ARRAY' },

        # the old all_table_properties, which has a small array of loading info
        _db_column_data                                 => { is => 'ARRAY' },

        # the old hashes by the same names
        _group_by_property_names                        => { is => 'HASH' },
        _order_by_property_names                        => { is => 'HASH' },

        _sql_filters    => { is => 'ARRAY' },
        _sql_params     => { is => 'ARRAY' },
        
        lob_column_names                            => {},
        lob_column_positions                        => {},
        query_config                                => {},
        post_process_results_callback               => {},
        
        select_clause                               => {},
        select_hint                                 => {},
        from_clause                                 => {},
        where_clause                                => {},
        connect_by_clause                           => {},
        group_by_clause                             => {},
        order_by_columns                            => {},
        order_by_non_column_data                    => {}, # flag that's true if asked to order_by something not in the data source
       
        sql_params                                  => {},
        filter_specs                                => {},
        
        property_names_in_resultset_order           => {},

        rule_template_id                            => {},
        rule_template_id_without_recursion_desc     => {},
        rule_template_without_recursion_desc        => {},

        joins                                       => {},
        recursion_desc                              => {},
        recurse_property_on_this_row                => {},
        recurse_property_referencing_other_rows     => {},
        recurse_resolution_by_iteration             => {},  # For data sources that don't support recursive queries
        
        joins_across_data_sources                   => {}, # context _resolve_query_plan_for_ds_and_bxt
        loading_templates                           => {},
        class_name                                  => {},
        rule_matches_all                            => {},
        rule_template_is_id_only                    => {},

        sub_typing_property                         => {},
        class_table_name                            => {},
        rule_template_specifies_value_for_subtype   => {},
        sub_classification_meta_class_name          => {},
    ]
};


sub _load {
        my $class = shift;
    my $rule = shift;

    # See if the requested object is loaded.
    my @loaded = $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0);
    return $class->context_return(@loaded) if @loaded;

    # Auto generate the object on the fly.
    my $id = $rule->value_for_id;
    unless (defined $id) {
        #$DB::single = 1;
        Carp::croak "No id specified for loading members of an infinite set ($class)!"
    }
    my $class_meta = $class->__meta__;
    my @p = (id => $id);
    if (my $alt_ids = $class_meta->{id_by}) {
        if (@$alt_ids == 1) {
            push @p, $alt_ids->[0] => $id;
        }
        else {
            my ($rule, %extra) = UR::BoolExpr->resolve_normalized($class, $rule);
            push @p, $rule->params_list;
        }
    }

    my $obj = $UR::Context::current->_construct_object($class, @p);

    if (my $method_name = $class_meta->sub_classification_method_name) {
        my($rule, %extra) = UR::BoolExpr->resolve_normalized($class, $rule);
        my $sub_class_name = $obj->$method_name;
        if ($sub_class_name ne $class) {
            # delegate to the sub-class to create the object
            $UR::Context::current->_abandon_object($obj);
            $obj = $UR::Context::current->_construct_object($sub_class_name,$rule);
            $obj->__signal_change__("load");
            return $obj;
        }
        # fall through if the class names match
    }

    $obj->__signal_change__("load");
    return $obj;
}

# these hash keys are probably removable
# because they are not above, they will be deleted if _init sets them
# this exists primarily as a cleanup target list
my @extra = qw(
    id_properties
    direct_table_properties
    all_table_properties
    sub_classification_method_name
    subclassify_by
    properties_meta_in_resultset_order
    all_properties
    rule_specifies_id
    all_id_property_names
    id_property_sorter
    properties_for_params
    first_table_name
    base_joins
    parent_class_objects
);

sub _init {
    my $self = shift;
  
    Carp::confess("already initialized???") if $self->_is_initialized;

    # We could have this sub-classify by data source type, but right
    # now it's conditional logic because we'll likely remove the distinctions.
    # This will work because we'll separate out the ds-specific portion
    # and call methods on the DS to get that part.
    my $ds = $self->data_source;
    if ($ds->isa("UR::DataSource::RDBMS")) {
        $self->_init_light();
        $self->_init_rdbms();
    }
    elsif ($ds->isa('UR::DataSource::Filesystem')) {
        $self->_init_core();
        $self->_init_filesystem();
    }
    else {
        # Once all callers are using the API for this we won't need "_init".
        $self->_init_core();
        $self->_init_default() if $ds->isa("UR::DataSource::Default");
        #$self->_init_remote_cache() if $ds->isa("UR::DataSource::RemoteCache");
    }

    # This object is currently still used as a hashref, but the properties
    # are a declaration of the part of the hashref data we are still dependent upon.
    # This removes the other properties to ensure this is the case.
    # Next steps are to clean up the code below to not produce the data,
    # then this loop can throw an exception if extra untracked data is found.
    for my $key (keys %$self) {
        next if $self->can($key);
        delete $self->{$key};
    }

    $self->_is_initialized(1);
    return $self;
}


sub _determine_complete_order_by_list {
    my($self, $rule_template, $class_data, $db_property_data) = @_;

    my $class_meta       = $rule_template->subject_class_name->__meta__;
    my $order_by_columns = $class_data->{order_by_columns} || [];
    my $order_by         = $rule_template->order_by;
    my $ds               = $self->data_source;

    my %order_by_property_names;
    my $order_by_non_column_data;
    if ($order_by) {
        my %db_property_data_map = map { $_->[1]->property_name => $_ } @$db_property_data;

        # we only pull back columns we're ordering by if there is ordering happening
        my %is_descending;
        my @column_data;
        for my $name (@$order_by) {
            my $order_by_prop = $name;
            if ($order_by_prop =~ m/^(-|\+)(.*)$/) {
                $order_by_prop = $2;
                $is_descending{$order_by_prop} = $1 eq '-';
            }

            my($order_by_prop_meta) = $class_meta->_concrete_property_meta_for_class_and_name($order_by_prop);
            unless ($order_by_prop_meta) {
                Carp::croak("Cannot order by '$name': Class "
                            . $class_meta->class_name
                            . " has no property named '$order_by_prop'");
            }

            $name = ( $is_descending{$order_by_prop} ? '-' : '' ) . $order_by_prop_meta->property_name;
            if ($order_by_property_names{$name} = $db_property_data_map{$order_by_prop_meta->property_name}) {  # yes, single =
                push @column_data, $order_by_property_names{$name};

                my $table_column_names = $ds->_select_clause_columns_for_table_property_data($column_data[-1]);
                $is_descending{$table_column_names->[0]} = $is_descending{$order_by_prop}; # copy for table.column designation
                $order_by_property_names{$table_column_names->[0]} = $order_by_property_names{$name};
            } else {
                $order_by_non_column_data = 1;
            }
        }

        if (@column_data) {
            my $additional_order_by_columns = $ds->_select_clause_columns_for_table_property_data(@column_data);

            # Strip out columns named in the original $order_by_columns list that now appear in the
            # additional order by list so we don't duplicate columns names, and the additional columns
            # appear earlier in the list
            my %additional_order_by_columns = map { $_ => 1 } @$additional_order_by_columns;
            my @existing_order_by_columns = grep { ! $additional_order_by_columns{$_} } @$order_by_columns;
            $order_by_columns = [ map { $is_descending{$_} ? '-'. $_  : $_ } ( @$additional_order_by_columns, @existing_order_by_columns ) ];
        }
    }
    $self->_order_by_property_names(\%order_by_property_names);
    return ($order_by_columns, $order_by_non_column_data);
}


sub _init_rdbms {
    my $self = shift;
    my $rule_template = $self->rule_template;
    my $ds = $self->data_source;

    # class-based values
    my $class_name = $rule_template->subject_class_name;
    my $class_meta = $class_name->__meta__;
    my $class_data = $ds->_get_class_data_for_loading($class_meta);       

    my @parent_class_objects                = @{ $class_data->{parent_class_objects} };
    my @all_id_property_names               = @{ $class_data->{all_id_property_names} };
    my @id_properties                       = @{ $class_data->{id_properties} };   
    
    #my $first_table_name                    = $class_data->{first_table_name};
    
    #my $id_property_sorter                  = $class_data->{id_property_sorter};    
    #my @lob_column_names                    = @{ $class_data->{lob_column_names} };
    my @lob_column_positions                = @{ $class_data->{lob_column_positions} };
    #my $query_config                        = $class_data->{query_config}; 
    #my $post_process_results_callback       = $class_data->{post_process_results_callback};
    #my $class_table_name                    = $class_data->{class_table_name};

    # individual template based
    my $hints    = $rule_template->hints;
    my %hints    = map { $_ => 1 } @$hints;
    my $order_by = $rule_template->order_by;
    my $group_by = $rule_template->group_by;
    my $limit    = $rule_template->limit;
    my $aggregate = $rule_template->aggregate;
    my $recursion_desc = $rule_template->recursion_desc;

    my ($first_table_name, @db_joins) =  _resolve_db_joins_for_inheritance($class_meta);
 
    $self->_db_joins(\@db_joins);
    $self->_obj_joins([]);

    # an array of arrays, containing $table_name, $column_name, $alias, $object_num
    # as joins are done we extend this, and then condense it into object fabricators
    my @db_property_data                    = @{ $class_data->{all_table_properties} };    

    my %group_by_property_names;
    if ($group_by) {
        # we only pull back columns we're grouping by or aggregating if there is grouping happening
        for my $name (@$group_by) {
            unless ($class_name->can($name)) {
                Carp::croak("Cannot group by '$name': Class $class_name has no property/method by that name");
            }
            $group_by_property_names{$name} = 1;
        }
        for my $data (@db_property_data) {
            my $name = $data->[1]->property_name;
            if ($group_by_property_names{$name}) {
                $group_by_property_names{$name} = $data;
            }
        }
        @db_property_data = grep { ref($_) } values %group_by_property_names; 
    }

    my($order_by_columns, $order_by_non_column_data)
        = $self->_determine_complete_order_by_list($rule_template, $class_data,\@db_property_data);

    $self->_db_column_data(\@db_property_data);
    $self->_group_by_property_names(\%group_by_property_names);

    # Find out what delegated properties we'll be dealing with
    my @sql_filters; 
    my @delegated_properties;
    do {         
        my %filters =     
            map { $_ => 0 }
            grep { substr($_,0,1) ne '-' }
            $rule_template->_property_names;
        
        unless (@all_id_property_names == 1 && $all_id_property_names[0] eq "id") {
            delete $filters{'id'};
        }

        # Remove the flag for descending/ascending sort
        my @order_by_properties = $order_by ? @$order_by : ();;
        s/^-|\+//  foreach @order_by_properties;

        my %properties_involved = map { $_ => 1 }
                                    keys(%filters),
                                    ($hints ? @$hints : ()),
                                    @order_by_properties,
                                    ($group_by ? @$group_by : ());
        
        my @properties_involved = sort keys(%properties_involved);
        my @errors;
        while (my $property_name = shift @properties_involved) {
            if (index($property_name,'.') != -1) {
                push @delegated_properties, $property_name;
                next;
            }

            my (@pmeta) = $class_meta->property_meta_for_name($property_name);
            unless (@pmeta) {
                if ($class_name->can($property_name)) {
                    # method, not property
                    next;
                }
                else {
                    push @errors, "Class ".$class_meta->id." has no property or method named '$property_name'";
                    next;
                }
            }
            
            # For each property in this list, go up the inheritance and find the right property
            # to query on.  Give priority to properties that actually have columns
            FIND_PROPERTY_WITH_COLUMN:
            foreach my $pmeta ( @pmeta ) {
                foreach my $candidate_class ( $class_meta->all_class_metas ) {
                    my $candidate_prop_meta = UR::Object::Property->get(class_name => $candidate_class->class_name,
                                                                        property_name => $property_name);
                    next unless $candidate_prop_meta;
                    if ($candidate_prop_meta->column_name) {
                        $pmeta = $candidate_prop_meta;
                        next FIND_PROPERTY_WITH_COLUMN;
                    }
                }
            }

            my $property = $pmeta[0];
            my $table_name = $property->class_meta->first_table_name;
            my $operator       = $rule_template->operator_for($property_name);
            my $value_position = $rule_template->value_position_for_property_name($property_name);

            if ($property->can("expr_sql")) {
                unless ($table_name) {
                    $ds->warning_message("Property '$property_name' of class '$class_name' can 'expr_sql' but has no table!");
                    next;
                }
                my $expr_sql = $property->expr_sql;
                if (exists $filters{$property_name}) {
                    push @sql_filters, 
                        $table_name => { 
                            # cheap hack of prefixing with a whitespace differentiates 
                            # from a regular column below
                            " " . $expr_sql => { operator => $operator, value_position => $value_position }
                        };
                }
                next;
            }

            # If the property is calculate and has a calculate_from list, add the
            # calculate_from things to the internal hints list, but not the template
            if ($property->is_calculated and $property->calculate_from) {
                my $calculate_from = $property->calculate_from;
                push @properties_involved, @$calculate_from;
                push @$hints, @$calculate_from;
                $hints{$_} = 1 foreach @$calculate_from;
            }

            if (my $column_name = $property->column_name) {
                # normal column: filter on it
                unless ($table_name) {
                    $ds->warning_message("Property '$property_name' of class '$class_name'  has column '$column_name' but has no table!");
                    next;
                }
                if (exists $filters{$property_name}) {
                    push @sql_filters, 
                        $table_name => { 
                            $column_name => { operator => $operator, value_position => $value_position } 
                        };
                }
            }
            elsif ($property->is_delegated) {
                push @delegated_properties, $property->property_name;
            }
            elsif ( ! exists($hints{$property_name}) or exists($filters{$property_name}) ) {
                $self->needs_further_boolexpr_evaluation_after_loading(1);
            }
            else {
                next;
            }

        } # end of properties in the expression which control the query content 

        if (@errors) { 
            my $class_name = $class_meta->class_name;
            $ds->error_message("ERRORS PROCESSING PARAMTERS: (" . join("\n", @errors) . ") used to generate SQL for $class_name!");
            #print Data::Dumper::Dumper($rule_template);
            Carp::croak("Can't continue");
        }
    };
    
    my $object_num = 0; 
    $self->_alias_count(0);
    
    my %hints_included;
    my @select_hint;

    # FIXME - this needs to be broken out into delegated-property-join-resolver
    # and inheritance-join-resolver methods that can be called recursively.
    # It would better encapsulate what's going on and avoid bugs with complicated
    # get()s
   
    # one iteration per target value involved in the query,
    # including values needed for filtering, ordering, grouping, and hints (selecting more)
    # these "properties" may be a single property name or an ad-hoc "chain"
    DELEGATED_PROPERTY:
    for my $delegated_property (sort @delegated_properties) {
        my $property_name = $delegated_property;
        my $delegation_chain_data           = $self->_delegation_chain_data || $self->_delegation_chain_data({});
        $delegation_chain_data->{"__all__"}{table_alias} = {};
        $delegation_chain_data->{"__all__"}{class_alias} = { $first_table_name => $class_meta };

        my ($final_accessor, $is_optional, @joins) = _resolve_object_join_data_for_property_chain($rule_template,$property_name,$property_name);
        
        # when there is no "final_accessor" it often means we have an object-accessor in a hint
        # we want that to go through the join process, and only be left out at filter construction time
        #unless ($final_accessor) {
            #$self->needs_further_boolexpr_evaluation_after_loading(1);
            #next;
        #}

        # this is gathered here and used below, but previously was gathered internally to the methods which take it
        # since it is no longer needed directly in this method it might be refactored into the places which use it
        my %ds_for_class;
        for my $join (@joins) {
            my $source_class_object = $join->{'source_class'}->__meta__;
            my ($source_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($source_class_object, $rule_template);
            $ds_for_class{$join->{'source_class'}} = $source_data_source;
            
            my $foreign_class_object = $join->{'foreign_class'}->__meta__;
            my ($foreign_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template);
            $ds_for_class{$join->{'foreign_class'}} = $foreign_data_source;
        }


        # Splice out joins that go through a UR::Value class and back out to the DB, since UR::Value-types
        # don't get stored in the DB
        # TODO: move this into the join creation logic
        for (my $i = 0; $i < @joins; $i++) {
            if (
                $i < $#joins
                and 
                (
                    # db -> UR::Value -> db : shortcut
                    $joins[$i]->{'foreign_class'}->isa('UR::Value')
                    and $joins[$i+1]->{'source_class'}->isa('UR::Value')
                    #and $joins[$i]->{'foreign_class'}->isa($joins[$i+1]->{'source_class'})  ## remove this?
                )
            ) { 
                my $fixed_join = UR::Object::Join->_get_or_define(
                                      source_class => $joins[$i]->{'source_class'},
                                      source_property_names => $joins[$i]->{'source_property_names'},
                                      foreign_class => $joins[$i+1]->{'foreign_class'},
                                      foreign_property_names => $joins[$i+1]->{'foreign_property_names'},
                                      is_optional => $joins[$i]->{'is_optional'},
                                      id => $joins[$i]->{id} . "->" . $joins[$i+1]->{id});
                if ($joins[$i+1]->{where}) {
                    # If there's a where involved, it will always be on the second thing,
                    # where the foreign_class is NOT a UR::Value
                    $fixed_join->{where} = $joins[$i+1]->{where};
                }
                splice(@joins, $i, 2, $fixed_join);
            }
        }

        if (@joins and $joins[-1]{foreign_class}->isa("UR::Value")) {
            # the final join in a chain is often the link between a primitive value
            # and the UR::Value subclass into which it falls ...irrelevent for db joins
            $final_accessor = $joins[-1]->source_property_names->[0]; 
            pop @joins;
            next DELEGATED_PROPERTY unless @joins;
        }

        my $last_class_object_excluding_inherited_joins;
        my $alias_for_property_value;

        # one iteration per table between the start table and target
        while (my $object_join = shift @joins) { 
            $object_num++;
            my @joins_for_object = ($object_join);

            # one iteration per layer of inheritance for this object 
            # or per case of a join having additional filtering
            my $current_inheritance_depth_for_this_target_join = 0;
            while (my $join = shift @joins_for_object) { 

                my $where = $join->{where};

                $current_inheritance_depth_for_this_target_join++;

                my $foreign_class_name = $join->{foreign_class};
                my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__;

                if ($foreign_class_object->join_hint and !($hints_included{$foreign_class_name}++)) {
                    push @select_hint, $foreign_class_object->join_hint;
                }

                if (not exists $ds_for_class{$foreign_class_name}) {
                    # error: we should have at least a key with an empty value if we tried to find the ds
                    die "no data source key for $foreign_class_name when adding a join?"
                }

                my $ds = $ds_for_class{$foreign_class_name};

                if (not $ds) {
                    # no ds for the next piece of data: we will have to resolve this on the client side
                    # this is where things may get slow if the query is insufficiently filtered
                    $self->needs_further_boolexpr_evaluation_after_loading(1);
                    next DELEGATED_PROPERTY;
                }

                my $alias = $self->_add_join(
                    $delegated_property,
                    $join,
                    $object_num,
                    $is_optional,
                    $final_accessor,
                    $ds_for_class{$foreign_class_name},
                );

                if (not $alias) {
                    # unable to add a join for another reason
                    # TODO: is the above the only valid case of a join being impossible?
                    # Can we remove this?
                    $self->needs_further_boolexpr_evaluation_after_loading(1);
                    next DELEGATED_PROPERTY;
                }

                # set these for after all of the joins are done
                my $last_class_name = $foreign_class_name;
                my $last_class_object = $foreign_class_object;

                # on the first iteration, we figure out the remaining inherited iterations
                # if there is inheritance to do, unshift those onto the stack ahead of other things
                if ($current_inheritance_depth_for_this_target_join == 1) {
                    if ($final_accessor and $last_class_object->property_meta_for_name($final_accessor)) {
                        $last_class_object_excluding_inherited_joins = $last_class_object;
                    }
                    my @parents = grep { $_->table_name } $foreign_class_object->ancestry_class_metas;
                    if (@parents) {
                        my @last_id_property_names = $foreign_class_object->id_property_names;
                        for my $parent (@parents) {
                            my @parent_id_property_names = $parent->id_property_names;
                            die if @parent_id_property_names > 1;
                            my $parent_join_foreign_class_name = $parent->class_name;
                            my $inheritance_join = UR::Object::Join->_get_or_define( 
                                source_class => $last_class_name,
                                source_property_names => [@last_id_property_names], # we change content below
                                foreign_class => $parent_join_foreign_class_name,
                                foreign_property_names => \@parent_id_property_names,
                                is_optional => $is_optional,
                                id => "${last_class_name}::" . join(',',@last_id_property_names),
                            );
                            unshift @joins_for_object, $inheritance_join; 
                            @last_id_property_names = @parent_id_property_names;
                            $last_class_name = $foreign_class_name;

                            my $foreign_class_object = $parent_join_foreign_class_name->__meta__;
                            my ($foreign_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template);
                            $ds_for_class{$parent_join_foreign_class_name} = $foreign_data_source;
                        }
                        next;
                    }
                }

                if (!@joins and not $alias_for_property_value) {
                    # we are out of joins for this delegated property
                    # setting $alias_for_property_value helps map to exactly where we do real filter/order/etc.
                    my $foreign_class_loading_data = $ds->_get_class_data_for_loading($foreign_class_object);
                    if ($final_accessor and
                        grep { $_->[1]->property_name eq $final_accessor } @{ $foreign_class_loading_data->{direct_table_properties} }
                    ) {
                        $alias_for_property_value = $alias;
                        #print "found alias for $property_name on $foreign_class_name: $alias\n";
                    }
                    else {
                        # The thing we're joining to isn't a database-backed column (maybe calculated?)
                        $self->needs_further_boolexpr_evaluation_after_loading(1);
                        next DELEGATED_PROPERTY;
                    }
                }

            } # next join in the inheritance for this object

        } # next join across objects from the query subject to the delegated property target

        # done adding any new joins for this delegated property/property-chain

        # now see if anything in the where-clause needs to filter on the item joined-to
        my $value_position = $rule_template->value_position_for_property_name($property_name);
        if (defined $value_position) {
            # this property _is_ used to filter results 
            if (not $final_accessor) {
                # on the client side :(
                $self->needs_further_boolexpr_evaluation_after_loading(1);
                next;
            }
            else {
                # at the database level :)
                my $final_accessor_property_meta = $last_class_object_excluding_inherited_joins->property_meta_for_name($final_accessor);
                unless ($final_accessor_property_meta) {
                    Carp::croak("No property metadata for property named '$final_accessor' in class "
                                . $last_class_object_excluding_inherited_joins->class_name
                                . " while resolving joins for property '" . $delegated_property->property_name . "' in class "
                                . $delegated_property->class_name);
                }

                my $sql_lvalue;
                if ($final_accessor_property_meta->is_calculated) {
                    $sql_lvalue = $final_accessor_property_meta->calculate_sql;
                    unless (defined($sql_lvalue)) {
                        $self->needs_further_boolexpr_evaluation_after_loading(1);
                        next;
                    }
                }
                else {
                    $sql_lvalue = $final_accessor_property_meta->column_name;
                    unless (defined($sql_lvalue)) {
                        Carp::confess("No column name set for non-delegated/calculated property $property_name of $class_name");
                    }
                }

                my $operator       = $rule_template->operator_for($property_name);

                unless ($alias_for_property_value) {
                    die "No alias found for $property_name?!";
                }

                push @sql_filters, 
                    $alias_for_property_value => { 
                        $sql_lvalue => { operator => $operator, value_position => $value_position } 
                    };
            }
        }
        
    } # next delegated property

    # the columns to query
    my $db_property_data = $self->_db_column_data;

    # the following two sets of variables hold the net result of the logic
    my $select_clause;
    my $from_clause;
    my $connect_by_clause;
    my $group_by_clause;

    # Build the SELECT clause explicitly.
    $select_clause = $ds->_select_clause_for_table_property_data(@$db_property_data);

    # Oracle places group_by in a comment in the select 
    unshift(@select_hint, $class_meta->select_hint) if $class_meta->select_hint;

    # Build the FROM clause base.
    # Add joins to the from clause as necessary, then
    $from_clause = (defined $first_table_name ? "$first_table_name" : '');        

    my $cnt = 0;
    my @sql_params;
    my @sql_joins = @{ $self->_db_joins };
    while (@sql_joins) {
        my $table_name = shift (@sql_joins);
        my $condition  = shift (@sql_joins);
        my ($table_alias) = ($table_name =~ /(\S+)\s*$/s);

        my $join_type;
        if ($condition->{-is_required}) {
            $join_type = 'INNER';
        }
        else {
            $join_type = 'LEFT';
        }

        $from_clause .= "\n$join_type join " . $table_name . " on ";
        # Restart the counter on each join for the from clause,
        # but for the where clause keep counting w/o reset.
        $cnt = 0;

        for my $column_name (keys %$condition) {
            next if substr($column_name,0,1) eq '-';

            my $linkage_data = $condition->{$column_name};
            my $expr_sql = (substr($column_name,0,1) eq " " ? $column_name : "${table_alias}.${column_name}");
            my ($operator, $value_position, $value, $link_table_name, $link_column_name, $left_coercion, $right_coercion)
                = @$linkage_data{qw/operator value_position value link_table_name link_column_name left_coercion right_coercion/};

            $expr_sql = sprintf($right_coercion, $expr_sql) if ($right_coercion);

            $from_clause .= "\n    and " if ($cnt++);

            if ($link_table_name and $link_column_name) {
                # the linkage data is a join specifier
                my $link_sql = "${link_table_name}.${link_column_name}";
                $link_sql = sprintf($left_coercion, $link_sql) if ($left_coercion);
                $from_clause .= "$link_sql = $expr_sql";
            }
            elsif (defined $value_position) {
                Carp::croak("Joins cannot use variable values currently!");
            }
            else {
                my ($more_sql, @more_params) = $ds->_extend_sql_for_column_operator_and_value($expr_sql, $operator, $value);   
                if ($more_sql) {
                    $from_clause .= $more_sql;
                    push @sql_params, @more_params;
                }
                else {
                    # error
                    return;
                }
            }
        } # next column
    } # next db join

    # build the WHERE clause by making a data structure which will be parsed outside of this module
    # special handling of different size lists, and NULLs, make a completely reusable SQL template very hard.
    my @filter_specs;
    while (@sql_filters) {
        my $table_name = shift (@sql_filters);
        my $condition  = shift (@sql_filters);
        my ($table_alias) = ($table_name =~ /(\S+)\s*$/s);

        for my $column_name (keys %$condition) {
            my $linkage_data = $condition->{$column_name};
            my $expr_sql = (substr($column_name,0,1) eq " " ? $column_name : "${table_alias}.${column_name}");                                
            my ($operator, $value_position, $value, $link_table_name, $link_column_name)
                = @$linkage_data{qw/operator value_position value link_table_name link_column_name/};

            if ($link_table_name and $link_column_name) {
                # the linkage data is a join specifier
                Carp::confess("explicit column linkage in where clause?");
                #$sql .= "${link_table_name}.${link_column_name} = $expr_sql";
            }
            else {         
                # the linkage data is a value position from the @values list       
                unless (defined $value_position) {
                    Carp::confess("No value position for $column_name in query!");
                }                
                push @filter_specs, [$expr_sql, $operator, $value_position];
            }
        } # next column                
    } # next db filter

    $connect_by_clause = ''; 
    my $recurse_resolution_by_iteration = 0;
    if ($recursion_desc) {
        unless (ref($recursion_desc) eq 'ARRAY') {
            Carp::croak("Recursion description must be an arrayref with exactly 2 items");
        }
        if (@$recursion_desc != 2) {
            Carp::croak("Recursion description must contain exactly 2 items; got ".scalar(@$recursion_desc)
                        . ': ' . join(', ',@$recursion_desc));
        }

        # Oracle supports "connect by" queries.
        if ($ds->does_support_recursive_queries eq 'connect by') {
            my ($this,$prior) = @{ $recursion_desc };

            my $this_property_meta = $class_meta->property_meta_for_name($this);
            unless ($this_property_meta) {
                Carp::croak("Class ".$class_meta->class_name." has no property named '$this', named in the recursion description");
            }
            my $prior_property_meta = $class_meta->property_meta_for_name($prior);
            unless ($prior_property_meta) {
                Carp::croak("Class ".$class_meta->class_name." has no property named '$prior', named in the recursion description");
            }

            my $this_class_meta = $this_property_meta->class_meta;
            my $prior_class_meta = $prior_property_meta->class_meta;

            my $this_table_name = $this_class_meta->table_name;
            unless ($this_table_name) {
                Carp::croak("Cannot resolve table name from class ".$class_meta->class_name." and property '$this', named in the recursion description");
            }
            my $prior_table_name = $prior_class_meta->table_name;
            unless ($prior_table_name) {
                Carp::croak("Cannot resolve table name from class ".$class_meta->class_name." and property '$prior', named in the recursion description");
            }

            my $this_column_name = $this_property_meta->column_name || $this;
            my $prior_column_name = $prior_property_meta->column_name || $prior;

            $connect_by_clause = "connect by $this_table_name.$this_column_name = prior $prior_table_name.$prior_column_name\n";
        } else {
            $recurse_resolution_by_iteration = 1;
        }
    }    

    my @property_names_in_resultset_order;
    for my $property_meta_array (@$db_property_data) {
        push @property_names_in_resultset_order, $property_meta_array->[1]->property_name; 
    }

    # this is only used when making a real instance object instead of a "set"
    my $per_object_in_resultset_loading_detail;
    unless ($group_by) {
        $per_object_in_resultset_loading_detail = $ds->_generate_loading_templates_arrayref(\@$db_property_data, $self->_obj_joins);
    }

    if ($group_by) {
        # when grouping, we're making set objects instead of regular objects
        # this means that we re-constitute the select clause and add a group_by clause
        $group_by_clause = 'group by ' . $select_clause if (scalar(@$group_by));

        # Q: - does it even make sense for the user to specify an order_by in the
        #    get() request for Set objects?  If so, then we need to concatonate these order_by_columns
        #    with the ones that already exist in $order_by_columns from the class data
        # A: - yes, because group by means "return a list of subsets", and this lets you sort the subsets
        $order_by_columns = $ds->_select_clause_columns_for_table_property_data(@$db_property_data);

        $select_clause .= ', ' if $select_clause;
        $select_clause .= 'count(*) count';
        for my $ag (@$aggregate) {
            next if $ag eq 'count';
             # TODO: translate property names to column names, and skip non-column properties 
            $select_clause .= ', ' . $ag;
        }
        unless (@$group_by == @$db_property_data) {
            print "mismatch table properties vs group by!\n";
        }
    }

    %$self = (
        %$self,

        # custom for RDBMS
        select_clause                               => $select_clause,
        select_hint                                 => scalar(@select_hint) ? \@select_hint : undef,
        from_clause                                 => $from_clause,        
        connect_by_clause                           => $connect_by_clause,
        group_by_clause                             => $group_by_clause,
        order_by_columns                            => $order_by_columns,        
        order_by_non_column_data                    => $order_by_non_column_data,
        filter_specs                                => \@filter_specs,
        sql_params                                  => \@sql_params,
        recurse_resolution_by_iteration             => $recurse_resolution_by_iteration,

        # override defaults in the regular datasource $parent_template_data
        property_names_in_resultset_order           => \@property_names_in_resultset_order,
        properties_meta_in_resultset_order          => $db_property_data,  # duplicate?!
        loading_templates                           => $per_object_in_resultset_loading_detail,
    );

    my $template_data = $rule_template->{loading_data_cache} = $self; 
    return $self;
}

sub _init_filesystem {
    my $self = shift;
    my $rule_template = $self->rule_template;
    my $ds = $self->data_source;

    # class-based values
    my $class_name = $rule_template->subject_class_name;
    my $class_meta = $class_name->__meta__;
    my $class_data = $ds->_get_class_data_for_loading($class_meta);

    my @db_property_data                    = @{ $class_data->{all_table_properties} };

    my($order_by_columns, $order_by_non_column_data)
        = $self->_determine_complete_order_by_list($rule_template, $class_data, \@db_property_data);

    %$self = (
        %$self,

        order_by_columns            => $order_by_columns,
        order_by_non_column_data    => $order_by_non_column_data,
    );

    my $template_data = $rule_template->{loading_data_cache} = $self;
    return $self;
}

sub _add_join {
    my ($self, 
        $property_name,
        $join,
        $object_num,
        $is_optional,
        $final_accessor,
        $foreign_data_source,
    ) = @_;

    my $delegation_chain_data           = $self->_delegation_chain_data || $self->_delegation_chain_data({});
    my $table_alias                     = $delegation_chain_data->{"__all__"}{table_alias} ||= {};
    my $source_table_and_column_names   = $delegation_chain_data->{$property_name}{latest_source_table_and_column_names} ||= [];

    my $source_class_name = $join->{source_class};
    my $source_class_object = $join->{'source_class_meta'} || $source_class_name->__meta__;                    

    my $class_alias                     = $delegation_chain_data->{"__all__"}{class_alias} ||= {};
    if (! %$class_alias and $source_class_object->table_name) {
        $class_alias->{$source_class_object->table_name} = $source_class_object;
    }

    my $foreign_class_name = $join->{foreign_class};
    my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__;

    my $rule_template = $self->rule_template;
    my $ds = $self->data_source;

    my $group_by = $rule_template->group_by;
    
    #my($foreign_data_source) = UR::Context->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template);
    if (!$foreign_data_source or ($foreign_data_source ne $ds)) {
        # FIXME - do something smarter in the future where it can do a join-y thing in memory
        $self->needs_further_boolexpr_evaluation_after_loading(1);
        return; 
    }

    my $foreign_class_loading_data = $ds->_get_class_data_for_loading($foreign_class_object);

    # This will get filled in during the first pass, and every time after we've successfully
    # performed a join - ie. that the delegated property points directly to a class/property
    # that is a real table/column, and not a tableless class or another delegated property
    my @source_property_names;
    unless (@$source_table_and_column_names) {
        @source_property_names = @{ $join->{source_property_names} };

        @$source_table_and_column_names =
            map {
                if ($_->[0] =~ /^(.*)\s+(\w+)\s*$/s) {
                    # This "table_name" was actually a bit of SQL with an inline view and an alias
                    # FIXME - this won't work if they used the optional "as" keyword
                    $_->[0] = $1;
                    $_->[2] = $2;
                }
                $_;
            }
            map {
                my($p) = $source_class_object->_concrete_property_meta_for_class_and_name($_);
                unless ($p) {
                    Carp::croak("No property $_ for class ".$source_class_object->class_name);
                }
                my($table_name,$column_name) = $p->table_and_column_name_for_property();
                if ($table_name && $column_name) {
                    [$table_name, $column_name];
                } else {
                    #Carp::confess("Can't determine table and column for property $_ in class " .
                    #              $source_class_object->class_name);
                    ();
                }
            }
            @source_property_names;
    }
    return unless @$source_table_and_column_names;

    #my @source_property_names = @{ $join->{source_property_names} };
    #my ($source_table_name, $fcols, $fprops) = $self->_resolve_table_and_column_data($source_class_object, @source_property_names);
    #my @source_column_names = @$fcols;
    #my @source_property_meta = @$fprops;

    my @foreign_property_names = @{ $join->{foreign_property_names} };
    my ($foreign_table_name, $fcols, $fprops) = $self->_resolve_table_and_column_data($foreign_class_object, @foreign_property_names);
    my @foreign_column_names = @$fcols;
    my @foreign_property_meta = @$fprops;
    
    unless (@foreign_column_names) {
        # all calculated properties: don't try to join any further
        return;
    }

    unless ($foreign_table_name) {
        # If we can't make the join because there is no datasource representation
        # for this class, we're done following the joins for this property
        # and will NOT try to filter on it at the datasource level
        $self->needs_further_boolexpr_evaluation_after_loading(1);
        return; 
    }
    
    unless (@foreign_column_names == @foreign_property_meta) {
        # some calculated properties, be sure to re-check for a match after loading the object
        $self->needs_further_boolexpr_evaluation_after_loading(1);
    }

    my $alias = $self->_get_join_alias($join);

    unless ($alias) {
        my $alias_num = $self->_alias_count($self->_alias_count+1);
        
        my $alias_name = $join->sub_group_label || $property_name;
        if (substr($alias_name,-1) eq '?') {
            chop($alias_name) if substr($alias_name,-1) eq '?';
        }

        my $alias_length = length($alias_name)+length($alias_num)+1;
        my $alias_max_length = 29;
        if ($alias_length > $alias_max_length) {
            $alias = substr($alias_name,0,$alias_max_length-length($alias_num)-1); 
        }
        else {
            $alias = $alias_name;
        }
        $alias =~ s/\./_/g;
        $alias .= '_' . $alias_num; 

        $self->_set_join_alias($join, $alias);

        if ($foreign_class_object->table_name) {
            my @extra_db_filters;
            my @extra_obj_filters;

            # TODO: when "flatten" correctly feeds the "ON" clause we can remove this
            # This will crash if the "where" happens to use indirect things 
            my $where = $join->{where};
            if ($where) {
                for (my $n = 0; $n < @$where; $n += 2) {
                    my $key =$where->[$n];
                    my ($name,$op) = ($key =~ /^(\S+)\s*(.*)/);
                    
                    #my $meta = $foreign_class_object->property_meta_for_name($name);
                    #my $column = $meta->is_calculated ? (defined($meta->calculate_sql) ? ($meta->calculate_sql) : () ) : ($meta->column_name);
                    my ($table_name, $column_names, $property_metas) = $self->_resolve_table_and_column_data($foreign_class_object, $name);
                    my $column = $column_names->[0];

                    if (not $column) {
                        Carp::confess("No column for $foreign_class_object->{id} $name?  Indirect property flattening must be enabled to use indirect filters in where with via/to.");
                    }

                    my $value = $where->[$n+1];
                    push @extra_db_filters, $column => { value => $value, ($op ? (operator => $op) : ()) };
                    push @extra_obj_filters, $name  => { value => $value, ($op ? (operator => $op) : ()) };
                }
            }

            my @db_join_data;
            for (my $n = 0; $n < @foreign_column_names; $n++) {

                my $link_table_name = $table_alias->{$source_table_and_column_names->[$n][0]}
                                    || $source_table_and_column_names->[$n][2]
                                    || $source_table_and_column_names->[$n][0];

                my $link_column_name = $source_table_and_column_names->[$n][1];
                
                my $foreign_column_name = $foreign_column_names[$n];

                my $link_class_meta = $class_alias->{$link_table_name} || $source_class_object;
                my $link_property_name = $link_class_meta->property_for_column($link_column_name);

                my @coercion = $self->data_source->cast_for_data_conversion(
                                    $link_class_meta->_concrete_property_meta_for_class_and_name($link_property_name),
                                    $foreign_property_meta[$n],
                                );

                push @db_join_data,
                        $foreign_column_name => {
                            link_table_name     => $link_table_name,
                            link_column_name    => $link_column_name,
                            left_coercion       => $coercion[0],
                            right_coercion      => $coercion[1],
                        };
            }

            $self->_add_db_join(
                "$foreign_table_name $alias" => {
                    @db_join_data,
                    @extra_db_filters,
                }
            );
            
            $self->_add_obj_join( 
                "$alias" => {
                    (
                        map {
                            $foreign_property_names[$_] => {
                                link_class_name     => $source_class_name,
                                link_alias          => $table_alias->{$source_table_and_column_names->[$_][0]} # join alias
                                                        || $source_table_and_column_names->[$_][2]  # SQL inline view alias
                                                        || $source_table_and_column_names->[$_][0], # table_name
                                link_property_name    => $source_property_names[$_] 
                            }
                        }
                        (0..$#foreign_property_names)
                    ),
                    @extra_obj_filters,
                }
            );

            # Add all of the columns in the join table to the return list
            # Note that we increment the object numbers.
            # Note: we add grouping columns individually instead of in chunks
            unless ($group_by) {
                $self->_add_columns( 
                        map {
                            my $new = [@$_]; 
                            $new->[2] = $alias;
                            $new->[3] = $object_num; 
                            $new 
                        }
                        @{ $foreign_class_loading_data->{direct_table_properties} }
                );                
            }
        }


        if ($group_by) {
            if ($self->_groups_by_property($property_name)) {
                my ($p) = 
                    map {
                        my $new = [@$_]; 
                        $new->[2] = $alias;
                        $new->[3] = 0; 
                        $new 
                    }
                    grep { $_->[1]->property_name eq $final_accessor }
                    @{ $foreign_class_loading_data->{direct_table_properties} };
                $self->_add_columns($p); 
            }
        }


        if ($self->_orders_by_property($property_name)) {
            my ($p) = 
                map {
                    my $new = [@$_]; 
                    $new->[2] = $alias;
                    $new->[3] = 0; 
                    $new 
                }
                grep { $_->[1]->property_name eq $final_accessor }
                @{ $foreign_class_loading_data->{direct_table_properties} };
            # ??? what do we do here now with $p? 
        }

        unless ($is_optional) {
            # if _any_ part requires this, mark it required
            $self->_set_alias_required($alias); 
        }

    } # done adding a new join alias for a join which has not yet been done

    if ($foreign_class_object->table_name) {
        $table_alias->{$foreign_table_name} = $alias;
        $class_alias->{$alias} = $foreign_class_object;
        @$source_table_and_column_names = ();  # Flag that we need to re-derive this at the top of the loop
    }

    return $alias;
}

sub _resolve_table_and_column_data {
    my ($class, $class_meta, @property_names) = @_;
    my @property_meta = 
        map { $class_meta->_concrete_property_meta_for_class_and_name($_) }
        @property_names;
    my $table_name;
    my @column_names = 
        map {
            # TODO: encapsulate
            if ($_->is_calculated) {
                if ($_->calculate_sql) {
                    $_->calculate_sql;
                } else {
                    ();
                }
            } else {
                my $column_name;
                ($table_name, $column_name) = $_->table_and_column_name_for_property();
                $column_name;
            }
        }
        @property_meta;

    if ($table_name and $table_name =~ /^(.*)\s+(\w+)\s*$/s) {
        $table_name = $1;
    }

    return ($table_name, \@column_names, \@property_meta);
}

sub _set_join_alias {
    my ($self, $join, $alias) = @_;
    $self->_join_data->{$join->id}{alias} = $alias;
    $self->_alias_data({}) unless $self->_alias_data();
    $self->_alias_data->{$alias}{join_id} = $join->id;
}

sub _get_join_alias {
    my ($self,$join) = @_;
    $self->_join_data({}) unless $self->_join_data();
    return $self->_join_data->{$join->id}{alias};
}

sub _get_alias_join {
    my ($self,$alias) = @_;
    my $alias_data = $self->_alias_data;
    return if (! $alias_data or ! exists($alias_data->{$alias}));
    my $join_id = $self->_alias_data->{$alias}{join_id};
    UR::Object::Join->get($join_id);
}

sub _add_db_join {
    my ($self, $key, $data) = @_;
    
    my ($alias) = ($key =~/\w+$/);
    my $alias_data = $self->_alias_data || $self->_alias_data({});
    $alias_data->{$alias}{db_join} = $data;
    
    my $db_joins = $self->_db_joins || $self->_db_joins([]);
    push @$db_joins, $key, $data;
}

sub _add_obj_join {
    my ($self, $key, $data) = @_;
   
    Carp::confess() unless ref $data;
    my $alias_data = $self->_alias_data || $self->_alias_data({});
    $alias_data->{$key}{obj_join} = $data; # the key is the alias here
    
    my $obj_joins = $self->_obj_joins || $self->_obj_joins([]);
    push @$obj_joins, $key, $data;
}

sub _set_alias_required {
    my ($self, $alias) = @_;
    my $alias_data = $self->_alias_data || $self->_alias_data({});
    $alias_data->{$alias}{is_required} = 1;
    $alias_data->{$alias}{db_join}{-is_required} = 1;
    $alias_data->{$alias}{obj_join}{-is_required} = 1;
}

sub _add_columns {
    my $self = shift;
    my @new = @_;
    my $old = $self->_db_column_data;
    my $pos = @$old;
    my $lob_column_positions = $self->{lob_column_positions};
    my $lob_column_names = $self->{lob_column_names};
    for my $class_property (@new) {
        my ($sql_class,$sql_property,$sql_table_name) = @$class_property;
        my $data_type = $sql_property->data_type || '';             
        if ($data_type =~ /LOB$/) {
            push @$lob_column_names, $sql_property->column_name;
            push @$lob_column_positions, $pos;
        }
        $pos++;
    }
    push @$old, @new;
}

# Used by the object fabricator to find out which resultset column a
# property's data is stored
sub column_index_for_class_property_and_object_num {
    my($self, $class_name, $property_name, $object_num) = @_;

   $object_num ||= 0;

    my $db_column_data = $self->_db_column_data;
    for (my $resultset_col = 0; $resultset_col < @$db_column_data; $resultset_col++) {
        if ($db_column_data->[$resultset_col]->[1]->class_name eq $class_name
            and $db_column_data->[$resultset_col]->[1]->property_name eq $property_name
            and $db_column_data->[$resultset_col]->[3] == $object_num
        ) {
            return $resultset_col;
        }
    }
    return undef;
}

# used by the object fabricator to determine the resultset column
# the source property of a join is stored.
sub column_index_for_class_and_property_before_object_num {
    my($self, $class_name, $property_name, $object_num) = @_;
    return unless $object_num;

    my $db_column_data = $self->_db_column_data;
    my $index;
    for (my $resultset_col = 0; $resultset_col < @$db_column_data; $resultset_col++) {
        last if ($db_column_data->[$resultset_col]->[3] >= $object_num);
        if ($db_column_data->[$resultset_col]->[1]->class_name eq $class_name
            and
            $db_column_data->[$resultset_col]->[1]->property_name eq $property_name
        ) {
            $index = $resultset_col;
        }
    }
    return $index;
}


sub _groups_by_property {
    my ($self, $property_name) = @_;
    return $self->_group_by_property_names->{$property_name};
}

sub _orders_by_property {
    my ($self, $property_name) = @_;
    return $self->_order_by_property_names->{$property_name};
}

sub _resolve_db_joins_for_inheritance {
    my $class_meta = $_[0];

    my $first_table_name;
    my @sql_joins;

    my $prev_table_name; 
    my $prev_id_column_name; 
    my $prev_property_meta;

    my @parent_class_objects  = $class_meta->ancestry_class_metas;

    for my $co ( $class_meta, @parent_class_objects ) {
        my $class_name = $co->class_name;
        my @id_property_objects = $co->direct_id_property_metas;
        my %id_properties = map { $_->property_name => 1 } @id_property_objects;
        my @id_column_names =
            map { $_->column_name }
            @id_property_objects;

        my $table_name = $co->table_name;
        if ($table_name) {
            $first_table_name ||= $table_name;
            if ($prev_table_name) {
                die "Database-level inheritance cannot be used with multi-value-id classes ($class_name)!" if @id_property_objects > 1;
                my $prev_table_alias;
                if ($prev_table_name =~ /.*\s+(\w+)\s*$/) {
                    $prev_table_alias = $1;
                }
                else {
                    $prev_table_alias = $prev_table_name;
                }

                my @coercion = $co->data_source->cast_for_data_conversion(
                                    $prev_property_meta,
                                    $id_property_objects[0]);
                push @sql_joins,
                    $table_name =>
                    {
                        $id_property_objects[0]->column_name => { 
                            link_table_name => $prev_table_alias, 
                            link_column_name => $prev_id_column_name,
                            left_coercion   => $coercion[0],
                            right_coercion  => $coercion[1],
                        },
                        -is_required => 1,
                    };
            }
            $prev_table_name = $table_name;
            $prev_id_column_name = $id_property_objects[0]->column_name;
            $prev_property_meta = $id_property_objects[0];
        }
    }

    return ($first_table_name, @sql_joins);
}

sub _resolve_object_join_data_for_property_chain {
    my ($rule_template, $property_name, $join_label) = @_;
    my $class_meta = $rule_template->subject_class_name->__meta__;
    
    my @joins;
    my $is_optional;
    my $final_accessor;

    my @pmeta = $class_meta->_concrete_property_meta_for_class_and_name($property_name);

    my $last_class_meta = $class_meta;
    for my $meta (@pmeta) {
        if (!$meta) {
            Carp::croak "Can't resolve joins for ".$rule_template->subject_class_name . " property '$property_name': No property metadata found for that class and property_name";
        }
        #id is a special property that we want to look up, but isn't necessarily on a table
        #so if it aliases another property, we look at that instead
        if($meta->property_name eq 'id' and $meta->class_name eq 'UR::Object') {
            my @id_properties = grep {$_->class_name ne 'UR::Object'} $last_class_meta->id_properties;
            if(@id_properties == 1) {
                $meta = $id_properties[0];
                $last_class_meta = $meta->class_name->__meta__;
                next;
            }
            elsif (@id_properties > 1) {
                Carp::confess "can't join to class " . $last_class_meta->class_name . " with multiple id properties: @id_properties";
            }
        }
        if($meta->data_type and $meta->data_type =~ /::/) {
            $last_class_meta = UR::Object::Type->get($meta->data_type);
        } else {
            $last_class_meta = UR::Object::Type->get($meta->class_name);
        }
        last unless $last_class_meta;
    }

    # we can't actually get this from the joins because 
    # a bunch of optional things can be chained together to form
    # something non-optional
    $is_optional = 0;
    for my $pmeta (@pmeta) {
        push @joins, $pmeta->_resolve_join_chain($join_label);
        $is_optional = 1 if $pmeta->is_optional or $pmeta->is_many;
    }

    return unless @joins;
    return ($joins[-1]->{source_name_for_foreign}, $is_optional, @joins)
};

sub _init_light {
    my $self = shift;
    my $rule_template = $self->rule_template;
    my $ds = $self->data_source;

    my $class_name = $rule_template->subject_class_name;
    my $class_meta = $class_name->__meta__;
    my $class_data = $ds->_get_class_data_for_loading($class_meta);       

    my @parent_class_objects                = @{ $class_data->{parent_class_objects} };
    my @all_properties                      = @{ $class_data->{all_properties} };
    my $sub_classification_meta_class_name  = $class_data->{sub_classification_meta_class_name};
    my $subclassify_by    = $class_data->{subclassify_by};
    
    my @all_id_property_names               = @{ $class_data->{all_id_property_names} };
    my @id_properties                       = @{ $class_data->{id_properties} };   
    my $id_property_sorter                  = $class_data->{id_property_sorter};    
    my $sub_typing_property                 = $class_data->{sub_typing_property};
    my $class_table_name                    = $class_data->{class_table_name};
    
    my $recursion_desc = $rule_template->recursion_desc;
    my $recurse_property_on_this_row;
    my $recurse_property_referencing_other_rows;
    my $recurse_resolution_by_iteration;
    if ($recursion_desc) {
        ($recurse_property_on_this_row,$recurse_property_referencing_other_rows) = @$recursion_desc;        
        $recurse_resolution_by_iteration = ! $ds->does_support_recursive_queries;
    }        
    
    my $needs_further_boolexpr_evaluation_after_loading; 
    
    my $is_join_across_data_source;

    my @sql_params;
    my @filter_specs;         
    my @property_names_in_resultset_order;
    my $object_num = 0; # 0-based, usually zero unless there are joins
    
    my @filters = $rule_template->_property_names;
    my %filters =     
        map { $_ => 0 }
        grep { substr($_,0,1) ne '-' }
        @filters;
    
    unless (@all_id_property_names == 1 && $all_id_property_names[0] eq "id") {
        delete $filters{'id'};
    }
    
    my (
        @sql_joins,
        @sql_filters, 
        $prev_table_name, 
        $prev_id_column_name, 
        $eav_class, 
        @eav_properties,
        $eav_cnt, 
        %pcnt, 
        $pk_used,
        @delegated_properties,    
        %outer_joins,
        %chain_delegates,
    );

    for my $key (keys %filters) {
        if (index($key,'.') != -1) {
            $chain_delegates{$key} = delete $filters{$key};
        }
    }

    for my $co ( $class_meta, @parent_class_objects ) {
        my $class_name = $co->class_name;
        last if ( ($class_name eq 'UR::Object') or (not $class_name->isa("UR::Object")) );
        my @id_property_objects = $co->direct_id_property_metas;
        if (@id_property_objects == 0) {
            @id_property_objects = $co->property_meta_for_name("id");
            if (@id_property_objects == 0) {
                Carp::confess("Couldn't determine ID properties for $class_name\n");
            }
        }
        my %id_properties = map { $_->property_name => 1 } @id_property_objects;
        my @id_column_names =
            map { $_->column_name }
            @id_property_objects;
        for my $property_name (sort keys %filters) {
            my $property = UR::Object::Property->get(class_name => $class_name, property_name => $property_name);
            next unless $property;
            my $operator       = $rule_template->operator_for($property_name);
            my $value_position = $rule_template->value_position_for_property_name($property_name);
            delete $filters{$property_name};
            $pk_used = 1 if $id_properties{ $property_name };
            if ($property->is_legacy_eav) {
                die "Old GSC EAV can be handled with a via/to/where/is_mutable=1";
            }
            elsif ($property->is_delegated) {
                push @delegated_properties, $property;
            }
            elsif ($property->is_calculated || $property->is_transient) {
                $needs_further_boolexpr_evaluation_after_loading = 1;
            }
            else {
                push @sql_filters, 
                    $class_name => 
                        { 
                            $property_name => { operator => $operator, value_position => $value_position }
                        };
            }
        }
        $prev_id_column_name = $id_property_objects[0]->column_name;
    } # end of inheritance loop
        
    if ( my @errors = keys(%filters) ) { 
        my $class_name = $class_meta->class_name;
        $ds->error_message('Unknown param(s) (' . join(',',@errors) . ") used to generate SQL for $class_name!");
        Carp::confess();
    }

    my $last_class_name = $class_name;
    my $last_class_object = $class_meta;        
    my $alias_num = 1;
    my %joins_done;
    my $joins_across_data_sources;

    DELEGATED_PROPERTY:
    for my $delegated_property (@delegated_properties) {
        my $last_alias_for_this_chain;
        my $property_name = $delegated_property->property_name;
        my @joins = $delegated_property->_resolve_join_chain($property_name);
        my $relationship_name = $delegated_property->via;
        unless ($relationship_name) {
           $relationship_name = $property_name;
           $needs_further_boolexpr_evaluation_after_loading = 1;
        }

        my $delegate_class_meta = $delegated_property->class_meta;
        my($via_accessor_meta) = $delegate_class_meta->_concrete_property_meta_for_class_and_name($relationship_name);
        next unless $via_accessor_meta;
        my $final_accessor = $delegated_property->to;            

        my $data_type = $via_accessor_meta->data_type;
        unless ($data_type) {
            Carp::croak "Can't resolve delegation for $property_name on class $class_name: via property $relationship_name has no data type";
        }

        my $data_type_meta = UR::Object::Type->get($via_accessor_meta->data_type);
        unless ($data_type_meta) {
            Carp::croak "No class meta data for " . $via_accessor_meta->data_type . 
                " while resolving property $property_name on class $class_name";
        }
        my($final_accessor_meta) = $data_type_meta->_concrete_property_meta_for_class_and_name(
                                             $final_accessor
                                         );
        unless ($final_accessor_meta) {
            Carp::croak("No property '$final_accessor' on class " . $via_accessor_meta->data_type .
                          " while resolving property $property_name on class $class_name");
        }

        # Follow the chain of via/to delegation down to where the data ultimately lives
        while($final_accessor_meta->is_delegated) {
            # May have been 'to' an id_by/id_class_by property.  Stop chaining and do two queries
            # If we had access to the value at this point, we could continue joining through that
            # value's class and id
            next DELEGATED_PROPERTY if ($final_accessor_meta->id_by or $final_accessor_meta->id_class_by);

            my $prev_accessor_meta = $final_accessor_meta;
            $final_accessor_meta = $final_accessor_meta->to_property_meta();
            unless ($final_accessor_meta) {
                Carp::croak("Can't resolve property '$final_accessor' of class " . $via_accessor_meta->data_type
                            . ": Resolution involved property '" . $prev_accessor_meta->property_name . "' of class "
                            . $prev_accessor_meta->class_name
                            . " which is delegated, but its via/to metadata does not resolve to a known class and property");
            }
        }
        $final_accessor = $final_accessor_meta->property_name;
        for my $join (@joins) {
            my $source_class_name = $join->{source_class};
            my $source_class_object = $join->{'source_class_meta'} || $source_class_name->__meta__;

            my $foreign_class_name = $join->{foreign_class};
            next DELEGATED_PROPERTY if ($foreign_class_name->isa('UR::Value'));
            my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__;
            my($foreign_data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template);
            if (! $foreign_data_source) {
                $needs_further_boolexpr_evaluation_after_loading = 1;
                next DELEGATED_PROPERTY;

            } elsif ($foreign_data_source ne $ds or
                    ! $ds->does_support_joins or
                    ! $foreign_data_source->does_support_joins
                )
            {
                push(@{$joins_across_data_sources->{$foreign_data_source->id}}, $delegated_property);
                next DELEGATED_PROPERTY;
            }
            my @source_property_names = @{ $join->{source_property_names} };
            my @source_table_and_column_names = 
                map {
                    my($p) = $source_class_object->_concrete_property_meta_for_class_and_name($_);
                    unless ($p) {
                        Carp::confess("No property $_ for class $source_class_object->{class_name}\n");
                    }
                    unless ($p->class_name->__meta__) {
                        Carp::croak("Can't get class metadata for " . $p->class_name);
                    }
                    [$p->class_name->__meta__->class_name, $p->property_name];
                }
                @source_property_names;
            my $foreign_table_name = $foreign_class_name;
            unless ($foreign_table_name) {
                # If we can't make the join because there is no datasource representation
                # for this class, we're done following the joins for this property
                # and will NOT try to filter on it at the datasource level
                $needs_further_boolexpr_evaluation_after_loading = 1;
                next DELEGATED_PROPERTY;
            }
            my @foreign_property_names = @{ $join->{foreign_property_names} };
            my @foreign_property_meta = 
                map {
                    $foreign_class_object->_concrete_property_meta_for_class_and_name($_)
                }
                @foreign_property_names;
            
            my @foreign_column_names = 
                map {
                    # TODO: encapsulate
                    $_->is_calculated ? (defined($_->calculate_sql) ? ($_->calculate_sql) : () ) : ($_->property_name)
                }
                @foreign_property_meta;
                
            unless (@foreign_column_names) {
                # all calculated properties: don't try to join any further
                last;
            }
            unless (@foreign_column_names == @foreign_property_meta) {
                # some calculated properties, be sure to re-check for a match after loading the object
                $needs_further_boolexpr_evaluation_after_loading = 1;
            }
            my $alias = $joins_done{$join->{id}};
            unless ($alias) {            
                $alias = "${relationship_name}_${alias_num}";
                $alias_num++;
                $object_num++;
                
                push @sql_joins,
                    "$foreign_table_name $alias" =>
                        {
                            map {
                                $foreign_property_names[$_] => { 
                                    link_table_name     => $last_alias_for_this_chain || $source_table_and_column_names[$_][0],
                                    link_column_name    => $source_table_and_column_names[$_][1]
                                }
                            }
                            (0..$#foreign_property_names)
                        };
                    
                # Add all of the columns in the join table to the return list.                
                push @all_properties, 
                    map { [$foreign_class_object, $_, $alias, $object_num] }
                    map { $_->[1] }                    # These three lines are to get around a bug in perl
                    sort { $a->[0] cmp $b->[0] }       # 5.8's sort involving method calls within the sort
                    map { [ $_->property_name, $_ ] }  # sub that do sorts of their own
                    grep { defined($_->column_name) && $_->column_name ne '' }
                    UR::Object::Property->get( class_name => $foreign_class_name );
              
                $joins_done{$join->{id}} = $alias;
                
            }
            # Set these for after all of the joins are done
            $last_class_name = $foreign_class_name;
            $last_class_object = $foreign_class_object;
            $last_alias_for_this_chain = $alias;
        } # next join
        unless ($delegated_property->via) {
            next;
        }
        my($final_accessor_property_meta) = $last_class_object->_concrete_property_meta_for_class_and_name($final_accessor);
        unless ($final_accessor_property_meta) {
            Carp::croak("No property metadata for property named '$final_accessor' in class " . $last_class_object->class_name
                        . " while resolving joins for property '" .$delegated_property->property_name . "' in class "
                        . $delegated_property->class_name);
        }
        my $sql_lvalue;
        if ($final_accessor_property_meta->is_calculated) {
            $sql_lvalue = $final_accessor_property_meta->calculate_sql;
            unless (defined($sql_lvalue)) {
                $needs_further_boolexpr_evaluation_after_loading = 1;
                next;
            }
        }
        else {
            $sql_lvalue = $final_accessor_property_meta->column_name;
            unless (defined($sql_lvalue)) {
                Carp::confess("No column name set for non-delegated/calculated property $property_name of $class_name");
            }
        }
        my $operator       = $rule_template->operator_for($property_name);
        my $value_position = $rule_template->value_position_for_property_name($property_name);                
    } # next delegated property
    for my $property_meta_array (@all_properties) {
        push @property_names_in_resultset_order, $property_meta_array->[1]->property_name; 
    }
    my $rule_template_without_recursion_desc = ($recursion_desc ? $rule_template->remove_filter('-recurse') : $rule_template);
    my $rule_template_specifies_value_for_subtype;
    if ($sub_typing_property) {
        $rule_template_specifies_value_for_subtype = $rule_template->specifies_value_for($sub_typing_property)
    }
    #my $per_object_in_resultset_loading_detail = $ds->_generate_loading_templates_arrayref(\@all_properties);
    %$self = (
        %$self,
        %$class_data,
        properties_for_params                       => \@all_properties,  
        property_names_in_resultset_order           => \@property_names_in_resultset_order,
        joins                                       => \@sql_joins,
        rule_template_id                            => $rule_template->id,
        rule_template_without_recursion_desc        => $rule_template_without_recursion_desc,
        rule_template_id_without_recursion_desc     => $rule_template_without_recursion_desc->id,
        rule_matches_all                            => $rule_template->matches_all,
        rule_specifies_id                           => ($rule_template->specifies_value_for('id') || undef),
        rule_template_is_id_only                    => $rule_template->is_id_only,
        rule_template_specifies_value_for_subtype   => $rule_template_specifies_value_for_subtype,
        recursion_desc                              => $rule_template->recursion_desc,
        recurse_property_on_this_row                => $recurse_property_on_this_row,
        recurse_property_referencing_other_rows     => $recurse_property_referencing_other_rows,
        recurse_resolution_by_iteration             => $recurse_resolution_by_iteration,
        #loading_templates                           => $per_object_in_resultset_loading_detail,
        joins_across_data_sources                   => $joins_across_data_sources,
    );
    return $self;
}

sub _init_core {
    my $self = shift;
    my $rule_template = $self->rule_template;
    my $ds = $self->data_source;

    # TODO: most of this only applies to the RDBMS subclass,
    # but some applies to any datasource.  It doesn't hurt to have the RDBMS stuff
    # here and ignored, but it's not placed correctly.
        
    # class-based values
    
    my $class_name = $rule_template->subject_class_name;
    my $class_meta = $class_name->__meta__;
    my $class_data = $ds->_get_class_data_for_loading($class_meta);       

    my @parent_class_objects                = @{ $class_data->{parent_class_objects} };
    my @all_properties                      = @{ $class_data->{all_properties} };
#    my $first_table_name                    = $class_data->{first_table_name};
    my $sub_classification_meta_class_name  = $class_data->{sub_classification_meta_class_name};
    my $subclassify_by    = $class_data->{subclassify_by};
    
    my @all_id_property_names               = @{ $class_data->{all_id_property_names} };
    my @id_properties                       = @{ $class_data->{id_properties} };   
    my $id_property_sorter                  = $class_data->{id_property_sorter};    
    
#    my $order_by_clause                     = $class_data->{order_by_clause};
    
#    my @lob_column_names                    = @{ $class_data->{lob_column_names} };
#    my @lob_column_positions                = @{ $class_data->{lob_column_positions} };
    
#    my $query_config                        = $class_data->{query_config}; 
#    my $post_process_results_callback       = $class_data->{post_process_results_callback};

    my $sub_typing_property                 = $class_data->{sub_typing_property};
    my $class_table_name                    = $class_data->{class_table_name};
    
    # individual query/boolexpr based
    
    my $recursion_desc = $rule_template->recursion_desc;
    my $recurse_property_on_this_row;
    my $recurse_property_referencing_other_rows;
    if ($recursion_desc) {
        ($recurse_property_on_this_row,$recurse_property_referencing_other_rows) = @$recursion_desc;        
    }        
    
    # _usually_ items freshly loaded from the DB don't need to be evaluated through the rule
    # because the SQL gets constructed in such a way that all the items returned would pass anyway.
    # But in certain cases (a delegated property trying to match a non-object value (which is a bug
    # in the caller's code from one point of view) or with calculated non-sql properties, then the
    # sql will return a superset of the items we're actually asking for, and the loader needs to
    # validate them through the rule
    my $needs_further_boolexpr_evaluation_after_loading; 
    
    # Does fulfilling this request involve querying more than one data source?
    my $is_join_across_data_source;

    my @sql_params;
    my @filter_specs;         
    my @property_names_in_resultset_order;
    my $object_num = 0; # 0-based, usually zero unless there are joins
    
    my @filters = $rule_template->_property_names;
    my %filters =     
        map { $_ => 0 }
        grep { substr($_,0,1) ne '-' }
        @filters;
    
    unless (@all_id_property_names == 1 && $all_id_property_names[0] eq "id") {
        delete $filters{'id'};
    }
    
    my (
        @sql_joins,
        @sql_filters, 
        $prev_table_name, 
        $prev_id_column_name, 
        $eav_class, 
        @eav_properties,
        $eav_cnt, 
        %pcnt, 
        $pk_used,
        @delegated_properties,    
        %outer_joins,
        %chain_delegates,
    );

    for my $key (keys %filters) {
        if (index($key,'.') != -1) {
            $chain_delegates{$key} = delete $filters{$key};
        }
    }
    for my $co ( $class_meta, @parent_class_objects ) {
#        my $table_name = $co->table_name;
#        next unless $table_name;

#        $first_table_name ||= $table_name;

        my $class_name = $co->class_name;
        
        last if ( ($class_name eq 'UR::Object') or (not $class_name->isa("UR::Object")) );
        
        my @id_property_objects = $co->direct_id_property_metas;
        
        if (@id_property_objects == 0) {
            @id_property_objects = $co->property_meta_for_name("id");
            if (@id_property_objects == 0) {
                Carp::confess("Couldn't determine ID properties for $class_name\n");
            }
        }
        
        my %id_properties = map { $_->property_name => 1 } @id_property_objects;
        my @id_column_names =
            map { $_->column_name }
            @id_property_objects;
        
#        if ($prev_table_name)
#        {
#            # die "Database-level inheritance cannot be used with multi-value-id classes ($class_name)!" if @id_property_objects > 1;
#            Carp::confess("No table for class $co->{class_name}") unless $table_name; 
#            push @sql_joins,
#                $table_name =>
#                    {
#                        $id_property_objects[0]->column_name => { 
#                            link_table_name => $prev_table_name, 
#                            link_column_name => $prev_id_column_name 
#                        }
#                    };
#            delete $filters{ $id_property_objects[0]->property_name } if $pk_used;
#        }

        for my $property_name (sort keys %filters)
        {
            my $property = UR::Object::Property->get(class_name => $class_name, property_name => $property_name);
            next unless $property;

            my $operator       = $rule_template->operator_for($property_name);
            my $value_position = $rule_template->value_position_for_property_name($property_name);

            delete $filters{$property_name};
            $pk_used = 1 if $id_properties{ $property_name };

#            if ($property->can("expr_sql")) {
#                my $expr_sql = $property->expr_sql;
#                push @sql_filters, 
#                    $table_name => 
#                        { 
#                            # cheap hack of putting a whitespace differentiates 
#                            # from a regular column below
#                            " " . $expr_sql => { operator => $operator, value_position => $value_position }
#                        };
#                next;
#            }

            if ($property->is_legacy_eav) {
                die "Old GSC EAV can be handled with a via/to/where/is_mutable=1";
            }
            elsif ($property->is_transient) {
                die "Query by transient property $property_name on $class_name cannot be done!";
            }
            elsif ($property->is_delegated) {
                push @delegated_properties, $property;
            }
            elsif ($property->is_calculated) {
                $needs_further_boolexpr_evaluation_after_loading = 1;
            }
            else {
                # normal column: filter on it
                push @sql_filters, 
                    $class_name => 
                        { 
                            $property_name => { operator => $operator, value_position => $value_position }
                        };
            }
        }
        
#        $prev_table_name = $table_name;
        $prev_id_column_name = $id_property_objects[0]->column_name;
        
    } # end of inheritance loop
        
    if ( my @errors = keys(%filters) ) { 
        my $class_name = $class_meta->class_name;
        $ds->error_message('Unknown param(s) (' . join(',',@errors) . ") used to generate SQL for $class_name!");
        Carp::confess();
    }

    my $last_class_name = $class_name;
    my $last_class_object = $class_meta;        
    my $alias_num = 1;

    my %joins_done;
    my $joins_across_data_sources;

    DELEGATED_PROPERTY:
    for my $delegated_property (@delegated_properties) {
        my $last_alias_for_this_chain;
    
        my $property_name = $delegated_property->property_name;
        my @joins = $delegated_property->_resolve_join_chain($property_name);
        #pop @joins if $joins[-1]->{foreign_class}->isa("UR::Value");
        my $relationship_name = $delegated_property->via;
        unless ($relationship_name) {
           $relationship_name = $property_name;
           $needs_further_boolexpr_evaluation_after_loading = 1;
        }

        my $delegate_class_meta = $delegated_property->class_meta;
        my($via_accessor_meta) = $delegate_class_meta->_concrete_property_meta_for_class_and_name($relationship_name);
        my $final_accessor = $delegated_property->to;            
        my($final_accessor_meta) = $via_accessor_meta->data_type->__meta__->_concrete_property_meta_for_class_and_name(
                                             $final_accessor
                                         );
        unless ($final_accessor_meta) {
            Carp::croak("No property '$final_accessor' on class " . $via_accessor_meta->data_type .
                          " while resolving property $property_name on class $class_name");
        }
        while($final_accessor_meta->is_delegated) {
            $final_accessor_meta = $final_accessor_meta->to_property_meta();
            unless ($final_accessor_meta) {
                Carp::croak("No property '$final_accessor' on class " . $via_accessor_meta->data_type .
                              " while resolving property $property_name on class $class_name");
            }
        }
        $final_accessor = $final_accessor_meta->property_name;
        
        for my $join (@joins) {

            my $source_class_name = $join->{source_class};
            my $source_class_object = $join->{'source_class_meta'} || $source_class_name->__meta__;

            my $foreign_class_name = $join->{foreign_class};
            my $foreign_class_object = $join->{'foreign_class_meta'} || $foreign_class_name->__meta__;
            my($foreign_data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($foreign_class_object, $rule_template);
            if (! $foreign_data_source) {
                $needs_further_boolexpr_evaluation_after_loading = 1;
                next DELEGATED_PROPERTY;

            } elsif ($foreign_data_source ne $ds or
                    ! $ds->does_support_joins or
                    ! $foreign_data_source->does_support_joins
                )
            {
                push(@{$joins_across_data_sources->{$foreign_data_source->id}}, $delegated_property);
                next DELEGATED_PROPERTY;
            }

            my @source_property_names = @{ $join->{source_property_names} };

            my @source_table_and_column_names = 
                map {
                    my($p) = $source_class_object->_concrete_property_meta_for_class_and_name($_);
                    unless ($p) {
                        Carp::confess("No property $_ for class $source_class_object->{class_name}\n");
                    }
                    [$p->class_name->__meta__->class_name, $p->property_name];
                }
                @source_property_names;


            my $foreign_table_name = $foreign_class_name;

            unless ($foreign_table_name) {
                # If we can't make the join because there is no datasource representation
                # for this class, we're done following the joins for this property
                # and will NOT try to filter on it at the datasource level
                $needs_further_boolexpr_evaluation_after_loading = 1;
                next DELEGATED_PROPERTY;
            }

            my @foreign_property_names = @{ $join->{foreign_property_names} };
            my @foreign_property_meta = 
                map {
                    $foreign_class_object->_concrete_property_meta_for_class_and_name($_);
                }
                @foreign_property_names;
            
            my @foreign_column_names = 
                map {
                    # TODO: encapsulate
                    $_->is_calculated ? (defined($_->calculate_sql) ? ($_->calculate_sql) : () ) : ($_->property_name)
                }
                @foreign_property_meta;
                
            unless (@foreign_column_names) {
                # all calculated properties: don't try to join any further
                last;
            }
            unless (@foreign_column_names == @foreign_property_meta) {
                # some calculated properties, be sure to re-check for a match after loading the object
                $needs_further_boolexpr_evaluation_after_loading = 1;
            }
            
            my $alias = $joins_done{$join->{id}};
            unless ($alias) {            
                $alias = "${relationship_name}_${alias_num}";
                $alias_num++;
                $object_num++;

                my @source_property_meta = map { $source_class_object->_concrete_property_meta_for_class_and_name($_) }
                                            @source_property_names;
                push @sql_joins,
                    "$foreign_table_name $alias" =>
                        {
                            map {
                                my @coercion = $ds->cast_for_data_conversion(
                                        $source_property_meta[$_],
                                        $foreign_property_meta[$_]);
                                $foreign_property_names[$_] => { 
                                    link_table_name     => $last_alias_for_this_chain || $source_table_and_column_names[$_][0],
                                    link_column_name    => $source_table_and_column_names[$_][1],
                                    left_coercion       => $coercion[0],
                                    right_coercion      => $coercion[1],
                                }
                            }
                            (0..$#foreign_property_names)
                        };
                    
                # Add all of the columns in the join table to the return list.                
                push @all_properties, 
                    map { [$foreign_class_object, $_, $alias, $object_num] }
                    map { $_->[1] }                    # These three lines are to get around a bug in perl
                    sort { $a->[0] cmp $b->[0] }       # 5.8's sort involving method calls within the sort
                    map  { [ $_->property_name, $_ ] } # sub that do sorts of their own
                    grep { defined($_->column_name) && $_->column_name ne '' }
                    UR::Object::Property->get( class_name => $foreign_class_name );
              
                $joins_done{$join->{id}} = $alias;
                
            }
            
            # Set these for after all of the joins are done
            $last_class_name = $foreign_class_name;
            $last_class_object = $foreign_class_object;
            $last_alias_for_this_chain = $alias;
            
        } # next join

        unless ($delegated_property->via) {
            next;
        }

        my($final_accessor_property_meta) = $last_class_object->_concrete_property_meta_for_class_and_name($id_properties[0]);
        unless ($final_accessor_property_meta) {
            Carp::croak("No property metadata for property named '$final_accessor' in class " . $last_class_object->class_name
                        . " while resolving joins for property '" .$delegated_property->property_name . "' in class "
                        . $delegated_property->class_name);
        }
       
        my $sql_lvalue;
        if ($final_accessor_property_meta->is_calculated) {
            $sql_lvalue = $final_accessor_property_meta->calculate_sql;
            unless (defined($sql_lvalue)) {
                $needs_further_boolexpr_evaluation_after_loading = 1;
                next;
            }
        }
        else {
            $sql_lvalue = $final_accessor_property_meta->column_name;
            unless (defined($sql_lvalue)) {
                Carp::confess("No column name set for non-delegated/calculated property $property_name of $class_name");
            }
        }

        my $operator       = $rule_template->operator_for($property_name);
        my $value_position = $rule_template->value_position_for_property_name($property_name);                
        #push @sql_filters, 
        #    $final_table_name_with_alias => { 
        #        $sql_lvalue => { operator => $operator, value_position => $value_position } 
        #    };
    } # next delegated property
    
    for my $property_meta_array (@all_properties) {
        push @property_names_in_resultset_order, $property_meta_array->[1]->property_name; 
    }
    
    my $rule_template_without_recursion_desc = ($recursion_desc ? $rule_template->remove_filter('-recurse') : $rule_template);
    
    my $rule_template_specifies_value_for_subtype;
    if ($sub_typing_property) {
        $rule_template_specifies_value_for_subtype = $rule_template->specifies_value_for($sub_typing_property)
    }

    my @this_ds_properties = grep { ! $_->[1]->is_delegated
                                    and (! $_->[1]->is_calculated or $_->[1]->calculate_sql)
                                  }
                             @all_properties;

    my $per_object_in_resultset_loading_detail = $ds->_generate_loading_templates_arrayref(\@this_ds_properties);

    %$self = (
        %$self,

        %$class_data,
        
        properties_for_params                       => \@all_properties,  
        property_names_in_resultset_order           => \@property_names_in_resultset_order,
        joins                                       => \@sql_joins,
        
        rule_template_id                            => $rule_template->id,
        rule_template_without_recursion_desc        => $rule_template_without_recursion_desc,
        rule_template_id_without_recursion_desc     => $rule_template_without_recursion_desc->id,
        rule_matches_all                            => $rule_template->matches_all,
        rule_specifies_id                           => ($rule_template->specifies_value_for('id') || undef),
        rule_template_is_id_only                    => $rule_template->is_id_only,
        rule_template_specifies_value_for_subtype   => $rule_template_specifies_value_for_subtype,
        
        recursion_desc                              => $rule_template->recursion_desc,
        recurse_property_on_this_row                => $recurse_property_on_this_row,
        recurse_property_referencing_other_rows     => $recurse_property_referencing_other_rows,
        
        loading_templates                           => $per_object_in_resultset_loading_detail,

        joins_across_data_sources                   => $joins_across_data_sources,
    );
        
    return $self;
}

sub _init_default {
    my $self = shift;
    my $bx_template = $self->rule_template;
    $self->{needs_further_boolexpr_evaluation_after_loading} = 1;
    my $all_possible_headers = $self->{loading_templates}[0]{property_names};
    my $expected_headers;
    my $class_meta = $bx_template->subject_class_name->__meta__;
    for my $pname (@$all_possible_headers) {
        my $pmeta = $class_meta->property($pname);
        if ($pmeta->is_delegated) {
            next;
        }
        push @$expected_headers, $pname;
    }
    $self->{loading_templates}[0]{property_names} = $expected_headers;

    if ($bx_template->subject_class_name->isa('UR::Value')) {
        # Hack so the objects get blessed into the proper subclass in the Object Fabricator.
        # This is necessary so every possible UR::Value subclass doesn't need its
        # own "id" property defined.  Without it, the data shows that these objects get
        # loaded as the base UR::Value class (since its "id" is defined on UR:Value)
        # and then would get automagically subclassed.
        $self->{'loading_templates'}->[0]->{'final_class_name'} = $bx_template->subject_class_name
    }

    return $self;
}


sub _init_remote_cache {
    my $self = shift;
    my $rule_template = $self->rule_template;
    my $ds = $self->data_source;

    my $class_name = $rule_template->subject_class_name;
    my $class_meta = $class_name->__meta__;
    my $class_data = $ds->_get_class_data_for_loading($class_meta);

    my $recursion_desc = $rule_template->recursion_desc;
    my $rule_template_without_recursion_desc = ($recursion_desc ? $rule_template->remove_filter('-recurse') : $rule_template);
    my $rule_template_specifies_value_for_subtype;
    my $sub_typing_property = $class_data->{'sub_typing_property'};
    if ($sub_typing_property) {
        $rule_template_specifies_value_for_subtype = $rule_template->specifies_value_for($sub_typing_property)
    }

    my @property_names = $class_name->__meta__->all_property_names;

    %$self = (
        %$self,

        select_clause                               => '',
        select_hint                                 => undef,
        from_clause                                 => '',
        where_clause                                => '',
        connect_by_clause                           => '',
        order_by_clause                             => '',

        needs_further_boolexpr_evaluation_after_loading => undef,
        loading_templates                           => [],

        sql_params                                  => [],
        filter_specs                                => [],
        property_names_in_resultset_order           => \@property_names,
        properties_for_params                       => [],

        rule_template_id                            => $rule_template->id,
        rule_template_without_recursion_desc        => $rule_template_without_recursion_desc,
        rule_template_id_without_recursion_desc     => $rule_template_without_recursion_desc->id,
        rule_matches_all                            => $rule_template->matches_all,
        rule_specifies_id                           => ($rule_template->specifies_value_for('id') || undef),
        rule_template_is_id_only                    => $rule_template->is_id_only,
        rule_template_specifies_value_for_subtype   => $rule_template_specifies_value_for_subtype,

        recursion_desc                              => undef,
        recurse_property_on_this_row                => undef,
        recurse_property_referencing_other_rows     => undef,

        %$class_data,
    );

    return $self;
}

1;

