X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSource.pm;h=b4dc288dedd8922f6b95217e5d546f07f03238a0;hb=908aa1bb761ec1da5c061fe9f687598e3f1934bc;hp=055f89a44e6474af09765482ba0abeddbc4bb84c;hpb=f97f9210550436b841141e791ee25dd3c723ab63;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSource.pm b/lib/DBIx/Class/ResultSource.pm index 055f89a..b4dc288 100644 --- a/lib/DBIx/Class/ResultSource.pm +++ b/lib/DBIx/Class/ResultSource.pm @@ -3,20 +3,34 @@ package DBIx::Class::ResultSource; use strict; use warnings; +use base 'DBIx::Class'; + use DBIx::Class::ResultSet; use DBIx::Class::ResultSourceHandle; -use Carp::Clan qw/^DBIx::Class/; -use Storable; -use base qw/DBIx::Class/; +use DBIx::Class::Exception; +use DBIx::Class::Carp; +use DBIx::Class::GlobalDestruction; +use Try::Tiny; +use List::Util 'first'; +use Scalar::Util qw/blessed weaken isweak/; +use B 'perlstring'; + +use namespace::clean; -__PACKAGE__->mk_group_accessors('simple' => qw/_ordered_columns - _columns _primaries _unique_constraints name resultset_attributes - schema from _relationships column_info_from_storage source_info - source_name sqlt_deploy_callback/); +__PACKAGE__->mk_group_accessors(simple => qw/ + source_name name source_info + _ordered_columns _columns _primaries _unique_constraints + _relationships resultset_attributes + column_info_from_storage +/); -__PACKAGE__->mk_group_accessors('component_class' => qw/resultset_class - result_class/); +__PACKAGE__->mk_group_accessors(component_class => qw/ + resultset_class + result_class +/); + +__PACKAGE__->mk_classdata( sqlt_deploy_callback => 'default_sqlt_deploy_hook' ); =head1 NAME @@ -24,10 +38,73 @@ DBIx::Class::ResultSource - Result source object =head1 SYNOPSIS + # Create a table based result source, in a result class. + + package MyApp::Schema::Result::Artist; + use base qw/DBIx::Class::Core/; + + __PACKAGE__->table('artist'); + __PACKAGE__->add_columns(qw/ artistid name /); + __PACKAGE__->set_primary_key('artistid'); + __PACKAGE__->has_many(cds => 'MyApp::Schema::Result::CD'); + + 1; + + # Create a query (view) based result source, in a result class + package MyApp::Schema::Result::Year2000CDs; + use base qw/DBIx::Class::Core/; + + __PACKAGE__->load_components('InflateColumn::DateTime'); + __PACKAGE__->table_class('DBIx::Class::ResultSource::View'); + + __PACKAGE__->table('year2000cds'); + __PACKAGE__->result_source_instance->is_virtual(1); + __PACKAGE__->result_source_instance->view_definition( + "SELECT cdid, artist, title FROM cd WHERE year ='2000'" + ); + + =head1 DESCRIPTION -A ResultSource is a component of a schema from which results can be directly -retrieved, most usually a table (see L) +A ResultSource is an object that represents a source of data for querying. + +This class is a base class for various specialised types of result +sources, for example L. Table is the +default result source type, so one is created for you when defining a +result class as described in the synopsis above. + +More specifically, the L base class pulls in the +L component, which defines +the L method. +When called, C creates and stores an instance of +L. Luckily, to use tables as result +sources, you don't need to remember any of this. + +Result sources representing select queries, or views, can also be +created, see L for full details. + +=head2 Finding result source objects + +As mentioned above, a result source instance is created and stored for +you when you define a L. + +You can retrieve the result source at runtime in the following ways: + +=over + +=item From a Schema object: + + $schema->source($source_name); + +=item From a Row object: + + $row->result_source; + +=item From a ResultSet object: + + $rs->result_source; + +=back =head1 METHODS @@ -47,7 +124,6 @@ sub new { $new->{_relationships} = { %{$new->{_relationships}||{}} }; $new->{name} ||= "!!NAME NOT SET!!"; $new->{_columns_info_loaded} ||= 0; - $new->{sqlt_deploy_callback} ||= "default_sqlt_deploy_hook"; return $new; } @@ -67,14 +143,21 @@ sub new { $source->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...); -Adds columns to the result source. If supplied key => hashref pairs, uses -the hashref as the column_info for that column. Repeated calls of this -method will add more columns, not replace them. +Adds columns to the result source. If supplied colname => hashref +pairs, uses the hashref as the L for that column. Repeated +calls of this method will add more columns, not replace them. The column names given will be created as accessor methods on your -L objects, you can change the name of the accessor +L objects. You can change the name of the accessor by supplying an L in the column_info hash. +If a column name beginning with a plus sign ('+col1') is provided, the +attributes provided will be merged with any existing attributes for the +column, with the new attributes taking precedence in the case that an +attribute already exists. Using this without a hashref +(C<< $source->add_columns(qw/+col1 +col2/) >>) is legal, but useless -- +it does the same thing it would do without the plus. + The contents of the column_info are not set in stone. The following keys are currently recognised/used by DBIx::Class: @@ -82,62 +165,125 @@ keys are currently recognised/used by DBIx::Class: =item accessor + { accessor => '_name' } + + # example use, replace standard accessor with one of your own: + sub name { + my ($self, $value) = @_; + + die "Name cannot contain digits!" if($value =~ /\d/); + $self->_name($value); + + return $self->_name(); + } + Use this to set the name of the accessor method for this column. If unset, the name of the column will be used. =item data_type -This contains the column type. It is automatically filled by the -L producer, and the -L module. If you do not enter a -data_type, DBIx::Class will attempt to retrieve it from the -database for you, using L's column_info method. The values of this -key are typically upper-cased. + { data_type => 'integer' } + +This contains the column type. It is automatically filled if you use the +L producer, or the +L module. Currently there is no standard set of values for the data_type. Use whatever your database supports. =item size + { size => 20 } + The length of your column, if it is a column type that can have a size -restriction. This is currently only used by L. +restriction. This is currently only used to create tables from your +schema, see L. =item is_nullable -Set this to a true value for a columns that is allowed to contain -NULL values. This is currently only used by L. + { is_nullable => 1 } + +Set this to a true value for a columns that is allowed to contain NULL +values, default is false. This is currently only used to create tables +from your schema, see L. =item is_auto_increment + { is_auto_increment => 1 } + Set this to a true value for a column whose value is somehow -automatically set. This is used to determine which columns to empty -when cloning objects using C. It is also used by +automatically set, defaults to false. This is used to determine which +columns to empty when cloning objects using +L. It is also used by L. +=item is_numeric + + { is_numeric => 1 } + +Set this to a true or false value (not C) to explicitly specify +if this column contains numeric data. This controls how set_column +decides whether to consider a column dirty after an update: if +C is true a numeric comparison C<< != >> will take place +instead of the usual C + +If not specified the storage class will attempt to figure this out on +first access to the column, based on the column C. The +result will be cached in this attribute. + =item is_foreign_key + { is_foreign_key => 1 } + Set this to a true value for a column that contains a key from a -foreign table. This is currently only used by -L. +foreign table, defaults to false. This is currently only used to +create tables from your schema, see L. =item default_value -Set this to the default value which will be inserted into a column -by the database. Can contain either a value or a function. This is -currently only used by L. + { default_value => \'now()' } + +Set this to the default value which will be inserted into a column by +the database. Can contain either a value or a function (use a +reference to a scalar e.g. C<\'now()'> if you want a function). This +is currently only used to create tables from your schema, see +L. + +See the note on L for more information about possible +issues related to db-side default values. =item sequence + { sequence => 'my_table_seq' } + Set this on a primary key column to the name of the sequence used to generate a new key value. If not specified, L will attempt to retrieve the name of the sequence from the database automatically. +=item retrieve_on_insert + + { retrieve_on_insert => 1 } + +For every column where this is set to true, DBIC will retrieve the RDBMS-side +value upon a new row insertion (normally only the autoincrement PK is +retrieved on insert). C is used automatically if +supported by the underlying storage, otherwise an extra SELECT statement is +executed to retrieve the missing data. + =item auto_nextval -Set this to a true value for a column whose value is retrieved -automatically from an oracle sequence. If you do not use an oracle -trigger to get the nextval, you have to set sequence as well. + { auto_nextval => 1 } + +Set this to a true value for a column whose value is retrieved automatically +from a sequence or function (if supported by your Storage driver.) For a +sequence, if you do not use a trigger to get the nextval, you have to set the +L value as well. + +Also set this for MSSQL columns with the 'uniqueidentifier' +L whose values you want to +automatically generate using C, unless they are a primary key in which +case this will be done anyway. =item extra @@ -153,13 +299,13 @@ L. =over -=item Arguments: $colname, [ \%columninfo ] +=item Arguments: $colname, \%columninfo? =item Return value: 1/0 (true/false) =back - $source->add_column('col' => \%info?); + $source->add_column('col' => \%info); Add a single column and optional column info. Uses the same column info keys as L. @@ -173,9 +319,17 @@ sub add_columns { my @added; my $columns = $self->_columns; while (my $col = shift @cols) { + my $column_info = {}; + if ($col =~ s/^\+//) { + $column_info = $self->column_info($col); + } + # If next entry is { ... } use that for the column info, if not # use an empty hashref - my $column_info = ref $cols[0] ? shift(@cols) : {}; + if (ref $cols[0]) { + my $new_info = shift(@cols); + %$column_info = (%$column_info, %$new_info); + } push(@added, $col) unless exists $columns->{$col}; $columns->{$col} = $column_info; } @@ -219,8 +373,8 @@ sub has_column { my $info = $source->column_info($col); Returns the column metadata hashref for a column, as originally passed -to L. See the description of L for information -on the contents of the hashref. +to L. See L above for information on the +contents of the hashref. =cut @@ -228,29 +382,31 @@ sub column_info { my ($self, $column) = @_; $self->throw_exception("No such column $column") unless exists $self->_columns->{$column}; - #warn $self->{_columns_info_loaded}, "\n"; + if ( ! $self->_columns->{$column}{data_type} - and $self->column_info_from_storage and ! $self->{_columns_info_loaded} - and $self->schema and $self->storage ) + and $self->column_info_from_storage + and my $stor = try { $self->storage } ) { $self->{_columns_info_loaded}++; - my $info = {}; - my $lc_info = {}; - # eval for the case of storage without table - eval { $info = $self->storage->columns_info_for( $self->from ) }; - unless ($@) { - for my $realcol ( keys %{$info} ) { - $lc_info->{lc $realcol} = $info->{$realcol}; - } + + # try for the case of storage without table + try { + my $info = $stor->columns_info_for( $self->from ); + my $lc_info = { map + { (lc $_) => $info->{$_} } + ( keys %$info ) + }; + foreach my $col ( keys %{$self->_columns} ) { $self->_columns->{$col} = { %{ $self->_columns->{$col} }, %{ $info->{$col} || $lc_info->{lc $col} || {} } }; } - } + }; } + return $self->_columns->{$column}; } @@ -274,10 +430,84 @@ sub columns { my $self = shift; $self->throw_exception( "columns() is a read-only accessor, did you mean add_columns()?" - ) if (@_ > 1); + ) if @_; return @{$self->{_ordered_columns}||[]}; } +=head2 columns_info + +=over + +=item Arguments: \@colnames ? + +=item Return value: Hashref of column name/info pairs + +=back + + my $columns_info = $source->columns_info; + +Like L but returns information for the requested columns. If +the optional column-list arrayref is omitted it returns info on all columns +currently defined on the ResultSource via L. + +=cut + +sub columns_info { + my ($self, $columns) = @_; + + my $colinfo = $self->_columns; + + if ( + first { ! $_->{data_type} } values %$colinfo + and + ! $self->{_columns_info_loaded} + and + $self->column_info_from_storage + and + my $stor = try { $self->storage } + ) { + $self->{_columns_info_loaded}++; + + # try for the case of storage without table + try { + my $info = $stor->columns_info_for( $self->from ); + my $lc_info = { map + { (lc $_) => $info->{$_} } + ( keys %$info ) + }; + + foreach my $col ( keys %$colinfo ) { + $colinfo->{$col} = { + %{ $colinfo->{$col} }, + %{ $info->{$col} || $lc_info->{lc $col} || {} } + }; + } + }; + } + + my %ret; + + if ($columns) { + for (@$columns) { + if (my $inf = $colinfo->{$_}) { + $ret{$_} = $inf; + } + else { + $self->throw_exception( sprintf ( + "No such column '%s' on source %s", + $_, + $self->source_name, + )); + } + } + } + else { + %ret = %$colinfo; + } + + return \%ret; +} + =head2 remove_columns =over @@ -344,14 +574,17 @@ sub remove_column { shift->remove_columns(@_); } # DO NOT CHANGE THIS TO GLOB =back -Defines one or more columns as primary key for this source. Should be +Defines one or more columns as primary key for this source. Must be called after L. Additionally, defines a L named C. -The primary key columns are used by L to -retrieve automatically created values from the database. +Note: you normally do want to define a primary key on your sources +B. +See +L +for more info. =cut @@ -386,11 +619,52 @@ sub primary_columns { return @{shift->_primaries||[]}; } +# a helper method that will automatically die with a descriptive message if +# no pk is defined on the source in question. For internal use to save +# on if @pks... boilerplate +sub _pri_cols { + my $self = shift; + my @pcols = $self->primary_columns + or $self->throw_exception (sprintf( + "Operation requires a primary key to be declared on '%s' via set_primary_key", + # source_name is set only after schema-registration + $self->source_name || $self->result_class || $self->name || 'Unknown source...?', + )); + return @pcols; +} + +=head2 sequence + +Manually define the correct sequence for your table, to avoid the overhead +associated with looking up the sequence automatically. The supplied sequence +will be applied to the L of each L + +=over 4 + +=item Arguments: $sequence_name + +=item Return value: undefined + +=back + +=cut + +sub sequence { + my ($self,$seq) = @_; + + my @pks = $self->primary_columns + or return; + + $_->{sequence} = $seq + for values %{ $self->columns_info (\@pks) }; +} + + =head2 add_unique_constraint =over 4 -=item Arguments: [ $name ], \@colnames +=item Arguments: $name?, \@colnames =item Return value: undefined @@ -408,11 +682,13 @@ Alternatively, you can specify only the columns: __PACKAGE__->add_unique_constraint([ qw/column1 column2/ ]); -This will result in a unique constraint named C, where -C
is replaced with the table name. +This will result in a unique constraint named +C, where C
is replaced with the table +name. -Unique constraints are used, for example, when you call -L. Only columns in the constraint are searched. +Unique constraints are used, for example, when you pass the constraint +name as the C attribute to L. Then +only columns in the constraint are searched. Throws an error if any of the given column names do not yet exist on the result source. @@ -421,8 +697,22 @@ the result source. sub add_unique_constraint { my $self = shift; + + if (@_ > 2) { + $self->throw_exception( + 'add_unique_constraint() does not accept multiple constraints, use ' + . 'add_unique_constraints() instead' + ); + } + my $cols = pop @_; - my $name = shift; + if (ref $cols ne 'ARRAY') { + $self->throw_exception ( + 'Expecting an arrayref of constraint columns, got ' . ($cols||'NOTHING') + ); + } + + my $name = shift @_; $name ||= $self->name_unique_constraint($cols); @@ -436,18 +726,70 @@ sub add_unique_constraint { $self->_unique_constraints(\%unique_constraints); } +=head2 add_unique_constraints + +=over 4 + +=item Arguments: @constraints + +=item Return value: undefined + +=back + +Declare multiple unique constraints on this source. + + __PACKAGE__->add_unique_constraints( + constraint_name1 => [ qw/column1 column2/ ], + constraint_name2 => [ qw/column2 column3/ ], + ); + +Alternatively, you can specify only the columns: + + __PACKAGE__->add_unique_constraints( + [ qw/column1 column2/ ], + [ qw/column3 column4/ ] + ); + +This will result in unique constraints named C and +C, where C
is replaced with the table name. + +Throws an error if any of the given column names do not yet exist on +the result source. + +See also L. + +=cut + +sub add_unique_constraints { + my $self = shift; + my @constraints = @_; + + if ( !(@constraints % 2) && first { ref $_ ne 'ARRAY' } @constraints ) { + # with constraint name + while (my ($name, $constraint) = splice @constraints, 0, 2) { + $self->add_unique_constraint($name => $constraint); + } + } + else { + # no constraint name + foreach my $constraint (@constraints) { + $self->add_unique_constraint($constraint); + } + } +} + =head2 name_unique_constraint =over 4 -=item Arguments: @colnames +=item Arguments: \@colnames =item Return value: Constraint name =back $source->table('mytable'); - $source->name_unique_constraint('col1', 'col2'); + $source->name_unique_constraint(['col1', 'col2']); # returns 'mytable_col1_col2' @@ -466,7 +808,10 @@ optional constraint name. sub name_unique_constraint { my ($self, $cols) = @_; - return join '_', $self->name, @$cols; + my $name = $self->name; + $name = $$name if (ref $name eq 'SCALAR'); + + return join '_', $name, @$cols; } =head2 unique_constraints @@ -481,7 +826,8 @@ sub name_unique_constraint { $source->unique_constraints(); -Read-only accessor which returns a hash of unique constraints on this source. +Read-only accessor which returns a hash of unique constraints on this +source. The hash is keyed by constraint name, and contains an arrayref of column names as values. @@ -544,6 +890,79 @@ sub unique_constraint_columns { return @{ $unique_constraints{$constraint_name} }; } +=head2 sqlt_deploy_callback + +=over + +=item Arguments: $callback_name | \&callback_code + +=item Return value: $callback_name | \&callback_code + +=back + + __PACKAGE__->sqlt_deploy_callback('mycallbackmethod'); + + or + + __PACKAGE__->sqlt_deploy_callback(sub { + my ($source_instance, $sqlt_table) = @_; + ... + } ); + +An accessor to set a callback to be called during deployment of +the schema via L or +L. + +The callback can be set as either a code reference or the name of a +method in the current result class. + +Defaults to L. + +Your callback will be passed the $source object representing the +ResultSource instance being deployed, and the +L object being created from it. The +callback can be used to manipulate the table object or add your own +customised indexes. If you need to manipulate a non-table object, use +the L. + +See L for examples. + +This sqlt deployment callback can only be used to manipulate +SQL::Translator objects as they get turned into SQL. To execute +post-deploy statements which SQL::Translator does not currently +handle, override L in your Schema class +and call L. + +=head2 default_sqlt_deploy_hook + +This is the default deploy hook implementation which checks if your +current Result class has a C method, and if present +invokes it B. This is to preserve the +semantics of C which was originally designed to expect +the Result class name and the +L<$sqlt_table instance|SQL::Translator::Schema::Table> of the table being +deployed. + +=cut + +sub default_sqlt_deploy_hook { + my $self = shift; + + my $class = $self->result_class; + + if ($class and $class->can('sqlt_deploy_hook')) { + $class->sqlt_deploy_hook(@_); + } +} + +sub _invoke_sqlt_deploy_hook { + my $self = shift; + if ( my $hook = $self->sqlt_deploy_callback) { + $self->$hook(@_); + } +} + =head2 resultset =over 4 @@ -571,13 +990,17 @@ but is cached from then on unless resultset_class changes. =back - package My::ResultSetClass; + package My::Schema::ResultSet::Artist; use base 'DBIx::Class::ResultSet'; ... - $source->resultset_class('My::ResultSet::Class'); + # In the result class + __PACKAGE__->resultset_class('My::Schema::ResultSet::Artist'); -Set the class of the resultset, this is useful if you want to create your + # Or in code + $source->resultset_class('My::Schema::ResultSet::Artist'); + +Set the class of the resultset. This is useful if you want to create your own resultset methods. Create your own class derived from L, and set it here. If called with no arguments, this method returns the name of the existing resultset class, if one @@ -593,6 +1016,10 @@ exists. =back + # In the result class + __PACKAGE__->resultset_attributes({ order_by => [ 'id' ] }); + + # Or in code $source->resultset_attributes({ order_by => [ 'id' ] }); Store a collection of resultset attributes, that will be set on every @@ -608,15 +1035,29 @@ sub resultset { 'call it on the schema instead.' ) if scalar @_; - return $self->resultset_class->new( + $self->resultset_class->new( $self, { + try { %{$self->schema->default_resultset_attributes} }, %{$self->{resultset_attributes}}, - %{$self->schema->default_resultset_attributes} }, ); } +=head2 name + +=over 4 + +=item Arguments: None + +=item Result value: $name + +=back + +Returns the name of the result source, which will typically be the table +name. This may be a scalar reference if the result source has a non-standard +name. + =head2 source_name =over 4 @@ -655,11 +1096,15 @@ Returns an expression of the source to be supplied to storage to specify retrieval from this source. In the case of a database, the required FROM clause contents. +=cut + +sub from { die 'Virtual method!' } + =head2 schema =over 4 -=item Arguments: None +=item Arguments: $schema =item Return value: A schema object @@ -667,8 +1112,29 @@ clause contents. my $schema = $source->schema(); -Returns the L object that this result source -belongs to. +Sets and/or returns the L object to which this +result source instance has been attached to. + +=cut + +sub schema { + if (@_ > 1) { + $_[0]->{schema} = $_[1]; + } + else { + $_[0]->{schema} || do { + my $name = $_[0]->{source_name} || '_unnamed_'; + my $err = 'Unable to perform storage-dependent operations with a detached result source ' + . "(source '$name' is not associated with a schema)."; + + $err .= ' You need to use $schema->thaw() or manually set' + . ' $DBIx::Class::ResultSourceHandle::thaw_schema while thawing.' + if $_[0]->{_detached_thaw}; + + DBIx::Class::Exception->throw($err); + }; + } +} =head2 storage @@ -765,7 +1231,7 @@ relationship. =back Throws an exception if the condition is improperly supplied, or cannot -be resolved using L. +be resolved. =cut @@ -792,7 +1258,7 @@ sub add_relationship { return $self; - # XXX disabled. doesn't work properly currently. skip in tests. +# XXX disabled. doesn't work properly currently. skip in tests. my $f_source = $self->schema->source($f_source_name); unless ($f_source) { @@ -805,13 +1271,14 @@ sub add_relationship { } return unless $f_source; # Can't test rel without f_source - eval { $self->resolve_join($rel, 'me') }; - - if ($@) { # If the resolve failed, back out and re-throw the error - delete $rels{$rel}; # + try { $self->_resolve_join($rel, 'me', {}, []) } + catch { + # If the resolve failed, back out and re-throw the error + delete $rels{$rel}; $self->_relationships(\%rels); - $self->throw_exception("Error creating relationship $rel: $@"); - } + $self->throw_exception("Error creating relationship $rel: $_"); + }; + 1; } @@ -893,190 +1360,231 @@ opposing a C relation. For definition of these look in L. The returned hashref is keyed by the name of the opposing -relationship, and contains it's data in the same manner as +relationship, and contains its data in the same manner as L. =cut sub reverse_relationship_info { my ($self, $rel) = @_; - my $rel_info = $self->relationship_info($rel); + + my $rel_info = $self->relationship_info($rel) + or $self->throw_exception("No such relationship '$rel'"); + my $ret = {}; return $ret unless ((ref $rel_info->{cond}) eq 'HASH'); - my @cond = keys(%{$rel_info->{cond}}); - my @refkeys = map {/^\w+\.(\w+)$/} @cond; - my @keys = map {$rel_info->{cond}->{$_} =~ /^\w+\.(\w+)$/} @cond; + my $stripped_cond = $self->__strip_relcond ($rel_info->{cond}); - # Get the related result source for this relationship - my $othertable = $self->related_source($rel); + my $rsrc_schema_moniker = $self->source_name + if try { $self->schema }; + + # this may be a partial schema or something else equally esoteric + my $other_rsrc = try { $self->related_source($rel) } + or return $ret; # Get all the relationships for that source that related to this source # whose foreign column set are our self columns on $rel and whose self - # columns are our foreign columns on $rel. - my @otherrels = $othertable->relationships(); - my $otherrelationship; - foreach my $otherrel (@otherrels) { - my $otherrel_info = $othertable->relationship_info($otherrel); + # columns are our foreign columns on $rel + foreach my $other_rel ($other_rsrc->relationships) { - my $back = $othertable->related_source($otherrel); - next unless $back->source_name eq $self->source_name; + # only consider stuff that points back to us + # "us" here is tricky - if we are in a schema registration, we want + # to use the source_names, otherwise we will use the actual classes - my @othertestconds; + # the schema may be partial + my $roundtrip_rsrc = try { $other_rsrc->related_source($other_rel) } + or next; - if (ref $otherrel_info->{cond} eq 'HASH') { - @othertestconds = ($otherrel_info->{cond}); - } - elsif (ref $otherrel_info->{cond} eq 'ARRAY') { - @othertestconds = @{$otherrel_info->{cond}}; + if ($rsrc_schema_moniker and try { $roundtrip_rsrc->schema } ) { + next unless $rsrc_schema_moniker eq $roundtrip_rsrc->source_name; } else { - next; + next unless $self->result_class eq $roundtrip_rsrc->result_class; } - foreach my $othercond (@othertestconds) { - my @other_cond = keys(%$othercond); - my @other_refkeys = map {/^\w+\.(\w+)$/} @other_cond; - my @other_keys = map {$othercond->{$_} =~ /^\w+\.(\w+)$/} @other_cond; - next if (!$self->compare_relationship_keys(\@refkeys, \@other_keys) || - !$self->compare_relationship_keys(\@other_refkeys, \@keys)); - $ret->{$otherrel} = $otherrel_info; - } + my $other_rel_info = $other_rsrc->relationship_info($other_rel); + + # this can happen when we have a self-referential class + next if $other_rel_info eq $rel_info; + + next unless ref $other_rel_info->{cond} eq 'HASH'; + my $other_stripped_cond = $self->__strip_relcond($other_rel_info->{cond}); + + $ret->{$other_rel} = $other_rel_info if ( + $self->_compare_relationship_keys ( + [ keys %$stripped_cond ], [ values %$other_stripped_cond ] + ) + and + $self->_compare_relationship_keys ( + [ values %$stripped_cond ], [ keys %$other_stripped_cond ] + ) + ); } + return $ret; } -=head2 compare_relationship_keys - -=over 4 +# all this does is removes the foreign/self prefix from a condition +sub __strip_relcond { + +{ + map + { map { /^ (?:foreign|self) \. (\w+) $/x } ($_, $_[1]{$_}) } + keys %{$_[1]} + } +} -=item Arguments: \@keys1, \@keys2 +sub compare_relationship_keys { + carp 'compare_relationship_keys is a private method, stop calling it'; + my $self = shift; + $self->_compare_relationship_keys (@_); +} -=item Return value: 1/0 (true/false) +# Returns true if both sets of keynames are the same, false otherwise. +sub _compare_relationship_keys { +# my ($self, $keys1, $keys2) = @_; + return + join ("\x00", sort @{$_[1]}) + eq + join ("\x00", sort @{$_[2]}) + ; +} -=back +# optionally takes either an arrayref of column names, or a hashref of already +# retrieved colinfos +# returns an arrayref of column names of the shortest unique constraint +# (matching some of the input if any), giving preference to the PK +sub _identifying_column_set { + my ($self, $cols) = @_; -Returns true if both sets of keynames are the same, false otherwise. + my %unique = $self->unique_constraints; + my $colinfos = ref $cols eq 'HASH' ? $cols : $self->columns_info($cols||()); -=cut + # always prefer the PK first, and then shortest constraints first + USET: + for my $set (delete $unique{primary}, sort { @$a <=> @$b } (values %unique) ) { + next unless $set && @$set; -sub compare_relationship_keys { - my ($self, $keys1, $keys2) = @_; - - # Make sure every keys1 is in keys2 - my $found; - foreach my $key (@$keys1) { - $found = 0; - foreach my $prim (@$keys2) { - if ($prim eq $key) { - $found = 1; - last; - } + for (@$set) { + next USET unless ($colinfos->{$_} && !$colinfos->{$_}{is_nullable} ); } - last unless $found; - } - # Make sure every key2 is in key1 - if ($found) { - foreach my $prim (@$keys2) { - $found = 0; - foreach my $key (@$keys1) { - if ($prim eq $key) { - $found = 1; - last; - } - } - last unless $found; - } + # copy so we can mangle it at will + return [ @$set ]; } - return $found; + return undef; } -=head2 resolve_join +# Returns the {from} structure used to express JOIN conditions +sub _resolve_join { + my ($self, $join, $alias, $seen, $jpath, $parent_force_left) = @_; -=over 4 + # we need a supplied one, because we do in-place modifications, no returns + $self->throw_exception ('You must supply a seen hashref as the 3rd argument to _resolve_join') + unless ref $seen eq 'HASH'; -=item Arguments: $relation - -=item Return value: Join condition arrayref - -=back + $self->throw_exception ('You must supply a joinpath arrayref as the 4th argument to _resolve_join') + unless ref $jpath eq 'ARRAY'; -Returns the join structure required for the related result source. + $jpath = [@$jpath]; # copy -=cut - -sub resolve_join { - my ($self, $join, $alias, $seen, $force_left) = @_; - $seen ||= {}; - $force_left ||= { force => 0 }; - if (ref $join eq 'ARRAY') { - return map { $self->resolve_join($_, $alias, $seen) } @$join; - } elsif (ref $join eq 'HASH') { + if (not defined $join or not length $join) { + return (); + } + elsif (ref $join eq 'ARRAY') { return map { - my $as = ($seen->{$_} ? $_.'_'.($seen->{$_}+1) : $_); - local $force_left->{force}; - ( - $self->resolve_join($_, $alias, $seen, $force_left), - $self->related_source($_)->resolve_join( - $join->{$_}, $as, $seen, $force_left - ) - ); - } keys %$join; - } elsif (ref $join) { - $self->throw_exception("No idea how to resolve join reftype ".ref $join); - } else { - my $count = ++$seen->{$join}; - #use Data::Dumper; warn Dumper($seen); - my $as = ($count > 1 ? "${join}_${count}" : $join); - my $rel_info = $self->relationship_info($join); - $self->throw_exception("No such relationship ${join}") unless $rel_info; - my $type; - if ($force_left->{force}) { - $type = 'left'; - } else { - $type = $rel_info->{attrs}{join_type} || ''; - $force_left->{force} = 1 if lc($type) eq 'left'; - } - return [ { $as => $self->related_source($join)->from, - -join_type => $type }, - $self->resolve_condition($rel_info->{cond}, $as, $alias) ]; + $self->_resolve_join($_, $alias, $seen, $jpath, $parent_force_left); + } @$join; } -} + elsif (ref $join eq 'HASH') { -=head2 pk_depends_on + my @ret; + for my $rel (keys %$join) { -=over 4 - -=item Arguments: $relname, $rel_data + my $rel_info = $self->relationship_info($rel) + or $self->throw_exception("No such relationship '$rel' on " . $self->source_name); -=item Return value: 1/0 (true/false) + my $force_left = $parent_force_left; + $force_left ||= lc($rel_info->{attrs}{join_type}||'') eq 'left'; -=back + # the actual seen value will be incremented by the recursion + my $as = $self->storage->relname_to_table_alias( + $rel, ($seen->{$rel} && $seen->{$rel} + 1) + ); -Determines whether a relation is dependent on an object from this source -having already been inserted. Takes the name of the relationship and a -hashref of columns of the related object. + push @ret, ( + $self->_resolve_join($rel, $alias, $seen, [@$jpath], $force_left), + $self->related_source($rel)->_resolve_join( + $join->{$rel}, $as, $seen, [@$jpath, { $rel => $as }], $force_left + ) + ); + } + return @ret; -=cut + } + elsif (ref $join) { + $self->throw_exception("No idea how to resolve join reftype ".ref $join); + } + else { + my $count = ++$seen->{$join}; + my $as = $self->storage->relname_to_table_alias( + $join, ($count > 1 && $count) + ); + + my $rel_info = $self->relationship_info($join) + or $self->throw_exception("No such relationship $join on " . $self->source_name); + + my $rel_src = $self->related_source($join); + return [ { $as => $rel_src->from, + -rsrc => $rel_src, + -join_type => $parent_force_left + ? 'left' + : $rel_info->{attrs}{join_type} + , + -join_path => [@$jpath, { $join => $as } ], + -is_single => ( + (! $rel_info->{attrs}{accessor}) + or + first { $rel_info->{attrs}{accessor} eq $_ } (qw/single filter/) + ), + -alias => $as, + -relation_chain_depth => $seen->{-relation_chain_depth} || 0, + }, + scalar $self->_resolve_condition($rel_info->{cond}, $as, $alias, $join) + ]; + } +} sub pk_depends_on { + carp 'pk_depends_on is a private method, stop calling it'; + my $self = shift; + $self->_pk_depends_on (@_); +} + +# Determines whether a relation is dependent on an object from this source +# having already been inserted. Takes the name of the relationship and a +# hashref of columns of the related object. +sub _pk_depends_on { my ($self, $relname, $rel_data) = @_; - my $cond = $self->relationship_info($relname)->{cond}; + my $relinfo = $self->relationship_info($relname); + + # don't assume things if the relationship direction is specified + return $relinfo->{attrs}{is_foreign_key_constraint} + if exists ($relinfo->{attrs}{is_foreign_key_constraint}); + + my $cond = $relinfo->{cond}; return 0 unless ref($cond) eq 'HASH'; # map { foreign.foo => 'self.bar' } to { bar => 'foo' } - my $keyhash = { map { my $x = $_; $x =~ s/.*\.//; $x; } reverse %$cond }; # assume anything that references our PK probably is dependent on us # rather than vice versa, unless the far side is (a) defined or (b) # auto-increment - my $rel_source = $self->related_source($relname); foreach my $p ($self->primary_columns) { @@ -1092,26 +1600,95 @@ sub pk_depends_on { return 1; } -=head2 resolve_condition +sub resolve_condition { + carp 'resolve_condition is a private method, stop calling it'; + my $self = shift; + $self->_resolve_condition (@_); +} + +our $UNRESOLVABLE_CONDITION = \ '1 = 0'; -=over 4 +# Resolves the passed condition to a concrete query fragment and a flag +# indicating whether this is a cross-table condition. Also an optional +# list of non-triviail values (notmally conditions) returned as a part +# of a joinfree condition hash +sub _resolve_condition { + my ($self, $cond, $as, $for, $relname) = @_; -=item Arguments: $cond, $as, $alias|$object + my $obj_rel = !!blessed $for; -=back + if (ref $cond eq 'CODE') { + my $relalias = $obj_rel ? 'me' : $as; -Resolves the passed condition to a concrete query fragment. If given an alias, -returns a join condition; if given an object, inverts that object to produce -a related conditional from that object. + my ($crosstable_cond, $joinfree_cond) = $cond->({ + self_alias => $obj_rel ? $as : $for, + foreign_alias => $relalias, + self_resultsource => $self, + foreign_relname => $relname || ($obj_rel ? $as : $for), + self_rowobj => $obj_rel ? $for : undef + }); -=cut + my $cond_cols; + if ($joinfree_cond) { + + # FIXME sanity check until things stabilize, remove at some point + $self->throw_exception ( + "A join-free condition returned for relationship '$relname' without a row-object to chain from" + ) unless $obj_rel; + + # FIXME another sanity check + if ( + ref $joinfree_cond ne 'HASH' + or + first { $_ !~ /^\Q$relalias.\E.+/ } keys %$joinfree_cond + ) { + $self->throw_exception ( + "The join-free condition returned for relationship '$relname' must be a hash " + .'reference with all keys being valid columns on the related result source' + ); + } -our $UNRESOLVABLE_CONDITION = \'1 = 0'; + # normalize + for (values %$joinfree_cond) { + $_ = $_->{'='} if ( + ref $_ eq 'HASH' + and + keys %$_ == 1 + and + exists $_->{'='} + ); + } -sub resolve_condition { - my ($self, $cond, $as, $for) = @_; - #warn %$cond; - if (ref $cond eq 'HASH') { + # see which parts of the joinfree cond are conditionals + my $relcol_list = { map { $_ => 1 } $self->related_source($relname)->columns }; + + for my $c (keys %$joinfree_cond) { + my ($colname) = $c =~ /^ (?: \Q$relalias.\E )? (.+)/x; + + unless ($relcol_list->{$colname}) { + push @$cond_cols, $colname; + next; + } + + if ( + ref $joinfree_cond->{$c} + and + ref $joinfree_cond->{$c} ne 'SCALAR' + and + ref $joinfree_cond->{$c} ne 'REF' + ) { + push @$cond_cols, $colname; + next; + } + } + + return wantarray ? ($joinfree_cond, 0, $cond_cols) : $joinfree_cond; + } + else { + return wantarray ? ($crosstable_cond, 1) : $crosstable_cond; + } + } + elsif (ref $cond eq 'HASH') { my %ret; foreach my $k (keys %{$cond}) { my $v = $cond->{$k}; @@ -1124,7 +1701,15 @@ sub resolve_condition { #warn "$self $k $for $v"; unless ($for->has_column_loaded($v)) { if ($for->in_storage) { - $self->throw_exception("Column ${v} not loaded on ${for} trying to resolve relationship"); + $self->throw_exception(sprintf + "Unable to resolve relationship '%s' from object %s: column '%s' not " + . 'loaded from storage (or not passed to new() prior to insert()). You ' + . 'probably need to call ->discard_changes to get the server-side defaults ' + . 'from the database.', + $as, + $for, + $v, + ); } return $UNRESOLVABLE_CONDITION; } @@ -1140,84 +1725,52 @@ sub resolve_condition { } elsif (!defined $as) { # undef, i.e. "no reverse object" $ret{$v} = undef; } else { - $ret{"${as}.${k}"} = "${for}.${v}"; + $ret{"${as}.${k}"} = { -ident => "${for}.${v}" }; } } - return \%ret; - } elsif (ref $cond eq 'ARRAY') { - return [ map { $self->resolve_condition($_, $as, $for) } @$cond ]; - } else { - die("Can't handle this yet :("); + + return wantarray + ? ( \%ret, ($obj_rel || !defined $as || ref $as) ? 0 : 1 ) + : \%ret + ; + } + elsif (ref $cond eq 'ARRAY') { + my (@ret, $crosstable); + for (@$cond) { + my ($cond, $crosstab) = $self->_resolve_condition($_, $as, $for, $relname); + push @ret, $cond; + $crosstable ||= $crosstab; + } + return wantarray ? (\@ret, $crosstable) : \@ret; + } + else { + $self->throw_exception ("Can't handle condition $cond for relationship '$relname' yet :("); } } -=head2 resolve_prefetch - -=over 4 - -=item Arguments: hashref/arrayref/scalar - -=back - -Accepts one or more relationships for the current source and returns an -array of column names for each of those relationships. Column names are -prefixed relative to the current source, in accordance with where they appear -in the supplied relationships. Examples: - - my $source = $schema->resultset('Tag')->source; - @columns = $source->resolve_prefetch( { cd => 'artist' } ); - - # @columns = - #( - # 'cd.cdid', - # 'cd.artist', - # 'cd.title', - # 'cd.year', - # 'cd.artist.artistid', - # 'cd.artist.name' - #) - - @columns = $source->resolve_prefetch( qw[/ cd /] ); - - # @columns = - #( - # 'cd.cdid', - # 'cd.artist', - # 'cd.title', - # 'cd.year' - #) - - $source = $schema->resultset('CD')->source; - @columns = $source->resolve_prefetch( qw[/ artist producer /] ); - - # @columns = - #( - # 'artist.artistid', - # 'artist.name', - # 'producer.producerid', - # 'producer.name' - #) - -=cut +# Accepts one or more relationships for the current source and returns an +# array of column names for each of those relationships. Column names are +# prefixed relative to the current source, in accordance with where they appear +# in the supplied relationships. +sub _resolve_prefetch { + my ($self, $pre, $alias, $alias_map, $order, $pref_path) = @_; + $pref_path ||= []; -sub resolve_prefetch { - my ($self, $pre, $alias, $seen, $order, $collapse) = @_; - $seen ||= {}; - #$alias ||= $self->name; - #warn $alias, Dumper $pre; - if( ref $pre eq 'ARRAY' ) { + if (not defined $pre or not length $pre) { + return (); + } + elsif( ref $pre eq 'ARRAY' ) { return - map { $self->resolve_prefetch( $_, $alias, $seen, $order, $collapse ) } + map { $self->_resolve_prefetch( $_, $alias, $alias_map, $order, [ @$pref_path ] ) } @$pre; } elsif( ref $pre eq 'HASH' ) { my @ret = map { - $self->resolve_prefetch($_, $alias, $seen, $order, $collapse), - $self->related_source($_)->resolve_prefetch( - $pre->{$_}, "${alias}.$_", $seen, $order, $collapse) + $self->_resolve_prefetch($_, $alias, $alias_map, $order, [ @$pref_path ] ), + $self->related_source($_)->_resolve_prefetch( + $pre->{$_}, "${alias}.$_", $alias_map, $order, [ @$pref_path, $_] ) } keys %$pre; - #die Dumper \@ret; return @ret; } elsif( ref $pre ) { @@ -1225,53 +1778,626 @@ sub resolve_prefetch { "don't know how to resolve prefetch reftype ".ref($pre)); } else { - my $count = ++$seen->{$pre}; - my $as = ($count > 1 ? "${pre}_${count}" : $pre); + my $p = $alias_map; + $p = $p->{$_} for (@$pref_path, $pre); + + $self->throw_exception ( + "Unable to resolve prefetch '$pre' - join alias map does not contain an entry for path: " + . join (' -> ', @$pref_path, $pre) + ) if (ref $p->{-join_aliases} ne 'ARRAY' or not @{$p->{-join_aliases}} ); + + my $as = shift @{$p->{-join_aliases}}; + my $rel_info = $self->relationship_info( $pre ); - $self->throw_exception( $self->name . " has no such relationship '$pre'" ) + $self->throw_exception( $self->source_name . " has no such relationship '$pre'" ) unless $rel_info; + my $as_prefix = ($alias =~ /^.*?\.(.+)$/ ? $1.'.' : ''); my $rel_source = $self->related_source($pre); - if (exists $rel_info->{attrs}{accessor} - && $rel_info->{attrs}{accessor} eq 'multi') { + if ($rel_info->{attrs}{accessor} && $rel_info->{attrs}{accessor} eq 'multi') { $self->throw_exception( "Can't prefetch has_many ${pre} (join cond too complex)") unless ref($rel_info->{cond}) eq 'HASH'; my $dots = @{[$as_prefix =~ m/\./g]} + 1; # +1 to match the ".${as_prefix}" - if (my ($fail) = grep { @{[$_ =~ m/\./g]} == $dots } - keys %{$collapse}) { - my ($last) = ($fail =~ /([^\.]+)$/); - carp ( - "Prefetching multiple has_many rels ${last} and ${pre} " - .(length($as_prefix) - ? "at the same level (${as_prefix}) " - : "at top level " - ) - . 'will currently disrupt both the functionality of $rs->count(), ' - . 'and the amount of objects retrievable via $rs->next(). ' - . 'Use at your own risk.' - ); - } + #my @col = map { (/^self\.(.+)$/ ? ("${as_prefix}.$1") : ()); } # values %{$rel_info->{cond}}; - $collapse->{".${as_prefix}${pre}"} = [ $rel_source->primary_columns ]; - # action at a distance. prepending the '.' allows simpler code - # in ResultSet->_collapse_result my @key = map { (/^foreign\.(.+)$/ ? ($1) : ()); } keys %{$rel_info->{cond}}; - my @ord = (ref($rel_info->{attrs}{order_by}) eq 'ARRAY' - ? @{$rel_info->{attrs}{order_by}} - : (defined $rel_info->{attrs}{order_by} - ? ($rel_info->{attrs}{order_by}) - : ())); - push(@$order, map { "${as}.$_" } (@key, @ord)); + + push @$order, map { "${as}.$_" } @key; + + if (my $rel_order = $rel_info->{attrs}{order_by}) { + # this is kludgy and incomplete, I am well aware + # but the parent method is going away entirely anyway + # so sod it + my $sql_maker = $self->storage->sql_maker; + my ($orig_ql, $orig_qr) = $sql_maker->_quote_chars; + my $sep = $sql_maker->name_sep; + + # install our own quoter, so we can catch unqualified stuff + local $sql_maker->{quote_char} = ["\x00", "\xFF"]; + + my $quoted_prefix = "\x00${as}\xFF"; + + for my $chunk ( $sql_maker->_order_by_chunks ($rel_order) ) { + my @bind; + ($chunk, @bind) = @$chunk if ref $chunk; + + $chunk = "${quoted_prefix}${sep}${chunk}" + unless $chunk =~ /\Q$sep/; + + $chunk =~ s/\x00/$orig_ql/g; + $chunk =~ s/\xFF/$orig_qr/g; + push @$order, \[$chunk, @bind]; + } + } } return map { [ "${as}.$_", "${as_prefix}${pre}.$_", ] } $rel_source->columns; - #warn $alias, Dumper (\@ret); - #return @ret; + } +} + +# adding a dep on MoreUtils *just* for this is retarded +my $unique_numlist = sub { [ sort { $a <=> $b } keys %{ {map { $_ => 1 } @_ }} ] }; + +# This error must be thrown from two distinct codepaths, joining them is +# rather hard. Go for this hack instead. +my $get_related_source = sub { + my ($rsrc, $rel, $relcols) = @_; + try { + $rsrc->related_source ($rel) + } catch { + $rsrc->throw_exception(sprintf( + "Can't inflate prefetch into non-existent relationship '%s' from '%s', " + . "check the inflation specification (columns/as) ending in '...%s.%s'.", + $rel, + $rsrc->source_name, + $rel, + (sort { length($a) <=> length ($b) } keys %$relcols)[0], + ))}; +}; + +# Takes a selection list and generates a collapse-map representing +# row-object fold-points. Every relationship is assigned a set of unique, +# non-nullable columns (which may *not even be* from the same resultset) +# and the collapser will use this information to correctly distinguish +# data of individual to-be-row-objects. +sub _resolve_collapse { + my ($self, $as, $as_fq_idx, $rel_chain, $parent_info, $node_idx_ref) = @_; + + # for comprehensible error messages put ourselves at the head of the relationship chain + $rel_chain ||= [ $self->source_name ]; + + # record top-level fully-qualified column index + $as_fq_idx ||= { %$as }; + + my ($my_cols, $rel_cols); + for (keys %$as) { + if ($_ =~ /^ ([^\.]+) \. (.+) /x) { + $rel_cols->{$1}{$2} = 1; + } + else { + $my_cols->{$_} = {}; # important for ||= below + } + } + + my $relinfo; + # run through relationships, collect metadata, inject non-left fk-bridges from + # *INNER-JOINED* children (if any) + for my $rel (keys %$rel_cols) { + my $rel_src = $get_related_source->($self, $rel, $rel_cols->{$rel}); + + my $inf = $self->relationship_info ($rel); + + $relinfo->{$rel}{is_single} = $inf->{attrs}{accessor} && $inf->{attrs}{accessor} ne 'multi'; + $relinfo->{$rel}{is_inner} = ( $inf->{attrs}{join_type} || '' ) !~ /^left/i; + $relinfo->{$rel}{rsrc} = $rel_src; + + my $cond = $inf->{cond}; + + if ( + ref $cond eq 'HASH' + and + keys %$cond + and + ! first { $_ !~ /^foreign\./ } (keys %$cond) + and + ! first { $_ !~ /^self\./ } (values %$cond) + ) { + for my $f (keys %$cond) { + my $s = $cond->{$f}; + $_ =~ s/^ (?: foreign | self ) \.//x for ($f, $s); + $relinfo->{$rel}{fk_map}{$s} = $f; + + # need to know source from *our* pov, hnce $rel. + $my_cols->{$s} ||= { via_fk => "$rel.$f" } if ( + defined $rel_cols->{$rel}{$f} # in fact selected + and + (! $node_idx_ref or $relinfo->{$rel}{is_inner}) # either top-level or an inner join + ); + } + } + } + + # if the parent is already defined, assume all of its related FKs are selected + # (even if they in fact are NOT in the select list). Keep a record of what we + # assumed, and if any such phantom-column becomes part of our own collapser, + # throw everything assumed-from-parent away and replace with the collapser of + # the parent (whatever it may be) + my $assumed_from_parent; + unless ($parent_info->{underdefined}) { + $assumed_from_parent->{columns} = { map + # only add to the list if we do not already select said columns + { ! exists $my_cols->{$_} ? ( $_ => 1 ) : () } + values %{$parent_info->{rel_condition} || {}} + }; + + $my_cols->{$_} = { via_collapse => $parent_info->{collapse_on} } + for keys %{$assumed_from_parent->{columns}}; + } + + # get colinfo for everything + if ($my_cols) { + my $ci = $self->columns_info; + $my_cols->{$_}{colinfo} = $ci->{$_} for keys %$my_cols; + } + + my $collapse_map; + + # try to resolve based on our columns (plus already inserted FK bridges) + if ( + $my_cols + and + my $uset = $self->_unique_column_set ($my_cols) + ) { + # see if the resulting collapser relies on any implied columns, + # and fix stuff up if this is the case + + my $parent_collapser_used = defined delete @{$uset}{keys %{$assumed_from_parent->{columns}}}; + $collapse_map->{-node_id} = $unique_numlist->( + $parent_collapser_used ? @{$parent_info->{collapse_on}} : (), + (map + { + my $fqc = join ('.', + @{$rel_chain}[1 .. $#$rel_chain], + ( $my_cols->{$_}{via_fk} || $_ ), + ); + + $as_fq_idx->{$fqc}; + } + keys %$uset + ), + ); + } + + # Stil don't know how to collapse - keep descending down 1:1 chains - if + # a related non-LEFT 1:1 is resolvable - its condition will collapse us + # too + unless ($collapse_map->{-node_id}) { + my @candidates; + + for my $rel (keys %$relinfo) { + next unless ($relinfo->{$rel}{is_single} && $relinfo->{$rel}{is_inner}); + + if ( my $rel_collapse = $relinfo->{$rel}{rsrc}->_resolve_collapse ( + $rel_cols->{$rel}, + $as_fq_idx, + [ @$rel_chain, $rel ], + { underdefined => 1 } + )) { + push @candidates, $rel_collapse->{-node_id}; + } + } + + # get the set with least amount of columns + # FIXME - maybe need to implement a data type order as well (i.e. prefer several ints + # to a single varchar) + if (@candidates) { + ($collapse_map->{-node_id}) = sort { scalar @$a <=> scalar @$b } (@candidates); + } + } + + # Still dont know how to collapse - see if the parent passed us anything + # (i.e. reuse collapser over 1:1) + unless ($collapse_map->{-node_id}) { + $collapse_map->{-node_id} = $parent_info->{collapse_on} + if $parent_info->{collapser_reusable}; + } + + # stop descending into children if we were called by a parent for first-pass + # and don't despair if nothing was found (there may be other parallel branches + # to dive into) + if ($parent_info->{underdefined}) { + return $collapse_map->{-node_id} ? $collapse_map : undef + } + # nothing down the chain resolved - can't calculate a collapse-map + elsif (! $collapse_map->{-node_id}) { + $self->throw_exception ( sprintf + "Unable to calculate a definitive collapse column set for %s%s: fetch more unique non-nullable columns", + $self->source_name, + @$rel_chain > 1 + ? sprintf (' (last member of the %s chain)', join ' -> ', @$rel_chain ) + : '' + , + ); + } + + # If we got that far - we are collapsable - GREAT! Now go down all children + # a second time, and fill in the rest + + $collapse_map->{-is_optional} = 1 if $parent_info->{is_optional}; + $collapse_map->{-node_index} = ${ $node_idx_ref ||= \do { my $x = 1 } }++; # this is *deliberately* not 0-based + + my (@id_sets, $multis_in_chain); + for my $rel (sort keys %$relinfo) { + + $collapse_map->{$rel} = $relinfo->{$rel}{rsrc}->_resolve_collapse ( + { map { $_ => 1 } ( keys %{$rel_cols->{$rel}} ) }, + + $as_fq_idx, + + [ @$rel_chain, $rel], + + { + collapse_on => [ @{$collapse_map->{-node_id}} ], + + rel_condition => $relinfo->{$rel}{fk_map}, + + is_optional => $collapse_map->{-is_optional}, + + # if this is a 1:1 our own collapser can be used as a collapse-map + # (regardless of left or not) + collapser_reusable => $relinfo->{$rel}{is_single}, + }, + + $node_idx_ref, + ); + + $collapse_map->{$rel}{-is_single} = 1 if $relinfo->{$rel}{is_single}; + $collapse_map->{$rel}{-is_optional} ||= 1 unless $relinfo->{$rel}{is_inner}; + push @id_sets, @{ $collapse_map->{$rel}{-branch_id} }; + } + + $collapse_map->{-branch_id} = $unique_numlist->( @id_sets, @{$collapse_map->{-node_id}} ); + + return $collapse_map; +} + +sub _unique_column_set { + my ($self, $cols) = @_; + + my %unique = $self->unique_constraints; + + # always prefer the PK first, and then shortest constraints first + USET: + for my $set (delete $unique{primary}, sort { @$a <=> @$b } (values %unique) ) { + next unless $set && @$set; + + for (@$set) { + next USET unless ($cols->{$_} && $cols->{$_}{colinfo} && !$cols->{$_}{colinfo}{is_nullable} ); + } + + return { map { $_ => 1 } @$set }; + } + + return undef; +} + +# Takes an arrayref of {as} dbic column aliases and the collapse and select +# attributes from the same $rs (the slector requirement is a temporary +# workaround), and returns a coderef capable of: +# my $me_pref_clps = $coderef->([$rs->cursor->next]) +# Where the $me_pref_clps arrayref is the future argument to +# ::ResultSet::_collapse_result. +# +# $me_pref_clps->[0] is always returned (even if as an empty hash with no +# rowdata), however branches of related data in $me_pref_clps->[1] may be +# pruned short of what was originally requested based on {as}, depending +# on: +# +# * If collapse is requested, a definitive collapse map is calculated for +# every relationship "fold-point", consisting of a set of values (which +# may not even be contained in the future 'me' of said relationship +# (for example a cd.artist_id defines the related inner-joined artist)). +# Thus a definedness check is carried on all collapse-condition values +# and if at least one is undef it is assumed that we are dealing with a +# NULLed right-side of a left-join, so we don't return a related data +# container at all, which implies no related objects +# +# * If we are not collapsing, there is no constraint on having a selector +# uniquely identifying all possible objects, and the user might have very +# well requested a column that just *happens* to be all NULLs. What we do +# in this case is fallback to the old behavior (which is a potential FIXME) +# by always returning a data container, but only filling it with columns +# IFF at least one of them is defined. This way we do not get an object +# with a bunch of has_column_loaded to undef, but at the same time do not +# further relationships based off this "null" object (e.g. in case the user +# deliberately skipped link-table values). I am pretty sure there are some +# tests that codify this behavior, need to find the exact testname. +# +# For an example of this coderef in action (and to see its guts) look at +# t/prefetch/_internals.t +# +# This is a huge performance win, as we call the same code for +# every row returned from the db, thus avoiding repeated method +# lookups when traversing relationships +# +# Also since the coderef is completely stateless (the returned structure is +# always fresh on every new invocation) this is a very good opportunity for +# memoization if further speed improvements are needed +# +# The way we construct this coderef is somewhat fugly, although I am not +# sure if the string eval is *that* bad of an idea. The alternative is to +# have a *very* large number of anon coderefs calling each other in a twisty +# maze, whereas the current result is a nice, smooth, single-pass function. +# In any case - the output of this thing is meticulously micro-tested, so +# any sort of rewrite should be relatively easy +# +sub _mk_row_parser { + my ($self, $args) = @_; + + my $inflate_index = { map + { $args->{inflate_map}[$_] => $_ } + ( 0 .. $#{$args->{inflate_map}} ) + }; + + my ($parser_src); + if ($args->{collapse}) { + # FIXME - deal with unorderedness + # unordered => $unordered + + my $collapse_map = $self->_resolve_collapse ( + # FIXME + # only consider real columns (not functions) during collapse resolution + # this check shouldn't really be here, as fucktards are not supposed to + # alias random crap to existing column names anyway, but still - just in + # case + # FIXME !!!! - this does not yet deal with unbalanced selectors correctly + # (it is now trivial as the attrs specify where things go out of sync) + { map + { ref $args->{selection}[$inflate_index->{$_}] ? () : ( $_ => $inflate_index->{$_} ) } + keys %$inflate_index + } + ); + + my $unrolled_top_branch_id_indexes = join (', ', @{$collapse_map->{-branch_id}}); + + my ($sequenced_top_branch_id, $sequenced_top_node_id) = map + { join ('', map { "{'\xFF__IDVALPOS__${_}__\xFF'}" } @$_ ) } + $collapse_map->{-branch_id}, $collapse_map->{-node_id} + ; + + my $rolled_out_assemblers = __visit_infmap_collapse ( + $inflate_index, $collapse_map + ); + + my @sprintf_args = ( + $unrolled_top_branch_id_indexes, + $sequenced_top_branch_id, + $sequenced_top_node_id, + $rolled_out_assemblers, + $sequenced_top_node_id, + ); + $parser_src = sprintf (<<'EOS', @sprintf_args); + +### BEGIN STRING EVAL + my ($rows_pos, $result_pos, $cur_row, @cur_row_id_values, $is_new_res, @collapse_idx) = (0,0); + + # this loop is a bit arcane - the rationale is that the passed in + # $_[0] will either have only one row (->next) or will have all + # rows already pulled in (->all and/or unordered). Given that the + # result can be rather large - we reuse the same already allocated + # array, since the collapsed prefetch is smaller by definition. + # At the end we cut the leftovers away and move on. + while ($cur_row = + ($rows_pos >= 0 and $_[0][$rows_pos++] or do { $rows_pos = -1; 0 } ) + || + ($_[1] and $_[1]->()) + ) { + + # FIXME + # optimize this away when we know we have no undefs in the collapse map + $cur_row_id_values[$_] = defined $cur_row->[$_] ? $cur_row->[$_] : "\xFF\xFFN\xFFU\xFFL\xFFL\xFF\xFF" + for (%s); # the top branch_id includes all id values + + # check top branch for doubling via a has_many non-selecting join or something + # 0 is reserved for this (node indexes start from 1) + next if $collapse_idx[0]%s++; + + $is_new_res = ! $collapse_idx[1]%s; + + # lazify + # fire on ordered only +# if ($is_new_res = ! $collapse_idx[1]{$cur_row_id_values[2]}) { +# } + + %s + + $_[0][$result_pos++] = $collapse_idx[1]%s + if $is_new_res; + } + + splice @{$_[0]}, $result_pos; # truncate the passed in array for cases of collapsing ->all() + +### END STRING EVAL +EOS + + # change the quoted placeholders to unquoted alias-references + $parser_src =~ s/ \' \xFF__VALPOS__(\d+)__\xFF \' /sprintf ('$cur_row->[%d]', $1)/gex; + $parser_src =~ s/ \' \xFF__IDVALPOS__(\d+)__\xFF \' /sprintf ('$cur_row_id_values[%d]', $1)/gex; + } + + else { + $parser_src = sprintf( + '$_ = %s for @{$_[0]}', + __visit_infmap_simple($inflate_index, { rsrc => $self }), # need the $rsrc to determine left-ness + ); + + # change the quoted placeholders to unquoted alias-references + $parser_src =~ s/ \' \xFF__VALPOS__(\d+)__\xFF \' /sprintf ('$_->[%d]', $1)/gex; + } + + eval "sub { no strict; no warnings; $parser_src }" or die "$@\n\n$parser_src"; +} + +{ + # keep our own DD object around so we don't have to fitz with quoting + my $dumper_obj; + my $visit_dump = sub { + # we actually will be producing functional perl code here, + # thus no second-guessing of what these globals might have + # been set to. DO NOT CHANGE! + ($dumper_obj ||= do { + require Data::Dumper; + Data::Dumper->new([]) + ->Purity (1) + ->Pad ('') + ->Useqq (0) + ->Terse (1) + ->Quotekeys (1) + ->Deepcopy (1) + ->Deparse (0) + ->Maxdepth (0) + ->Indent (0) + })->Values ([shift])->Dump, + }; + + sub __visit_infmap_simple { + my ($val_idx, $args) = @_; + + my $my_cols = {}; + my $rel_cols; + for (keys %$val_idx) { + if ($_ =~ /^ ([^\.]+) \. (.+) /x) { + $rel_cols->{$1}{$2} = $val_idx->{$_}; + } + else { + $my_cols->{$_} = $val_idx->{$_}; + } + } + my @relperl; + for my $rel (sort keys %$rel_cols) { + + my $rel_rsrc = $get_related_source->($args->{rsrc}, $rel, $rel_cols->{$rel}); + + #my $optional = $args->{is_optional}; + #$optional ||= ($args->{rsrc}->relationship_info($rel)->{attrs}{join_type} || '') =~ /^left/i; + + push @relperl, join ' => ', perlstring($rel), __visit_infmap_simple($rel_cols->{$rel}, { + non_top => 1, + #is_optional => $optional, + rsrc => $rel_rsrc, + }); + + # FIXME SUBOPTIMAL - disabled to satisfy t/resultset/inflate_result_api.t + #if ($optional and my @branch_null_checks = map + # { "(! defined '\xFF__VALPOS__${_}__\xFF')" } + # sort { $a <=> $b } values %{$rel_cols->{$rel}} + #) { + # $relperl[-1] = sprintf ( '(%s) ? ( %s => [] ) : ( %s )', + # join (' && ', @branch_null_checks ), + # perlstring($rel), + # $relperl[-1], + # ); + #} + } + + my $me_struct = keys %$my_cols + ? $visit_dump->({ map { $_ => "\xFF__VALPOS__$my_cols->{$_}__\xFF" } (keys %$my_cols) }) + : 'undef' + ; + + return sprintf '[%s]', join (',', + $me_struct, + @relperl ? sprintf ('{ %s }', join (',', @relperl)) : (), + ); + } + + sub __visit_infmap_collapse { + my ($val_idx, $collapse_map, $parent_info) = @_; + + my $my_cols = {}; + my $rel_cols; + for (keys %$val_idx) { + if ($_ =~ /^ ([^\.]+) \. (.+) /x) { + $rel_cols->{$1}{$2} = $val_idx->{$_}; + } + else { + $my_cols->{$_} = $val_idx->{$_}; + } + } + + my $sequenced_node_id = join ('', map + { "{'\xFF__IDVALPOS__${_}__\xFF'}" } + @{$collapse_map->{-node_id}} + ); + + my $me_struct = keys %$my_cols + ? $visit_dump->([{ map { $_ => "\xFF__VALPOS__$my_cols->{$_}__\xFF" } (keys %$my_cols) }]) + : 'undef' + ; + my $node_idx_ref = sprintf '$collapse_idx[%d]%s', $collapse_map->{-node_index}, $sequenced_node_id; + + my $parent_idx_ref = sprintf( '$collapse_idx[%d]%s[1]{%s}', + @{$parent_info}{qw/node_idx sequenced_node_id/}, + perlstring($parent_info->{relname}), + ) if $parent_info; + + my @src; + if ($collapse_map->{-node_index} == 1) { + push @src, sprintf( '%s ||= %s;', + $node_idx_ref, + $me_struct, + ); + } + elsif ($collapse_map->{-is_single}) { + push @src, sprintf ( '%s = %s ||= %s;', + $parent_idx_ref, + $node_idx_ref, + $me_struct, + ); + } + else { + push @src, sprintf('push @{%s}, %s = %s if !%s;', + $parent_idx_ref, + $node_idx_ref, + $me_struct, + $node_idx_ref, + ); + } + + #my $known_defined = { %{ $parent_info->{known_defined} || {} } }; + #$known_defined->{$_}++ for @{$collapse_map->{-node_id}}; + + for my $rel (sort keys %$rel_cols) { + + push @src, sprintf( '%s[1]{%s} ||= [];', $node_idx_ref, perlstring($rel) ); + + push @src, __visit_infmap_collapse($rel_cols->{$rel}, $collapse_map->{$rel}, { + node_idx => $collapse_map->{-node_index}, + sequenced_node_id => $sequenced_node_id, + relname => $rel, + #known_defined => $known_defined, + }); + + # FIXME SUBOPTIMAL - disabled to satisfy t/resultset/inflate_result_api.t + #if ($collapse_map->{$rel}{-is_optional} and my @null_checks = map + # { "(! defined '\xFF__VALPOS__${_}__\xFF')" } + # sort { $a <=> $b } grep + # { ! $known_defined->{$_} } + # @{$collapse_map->{$rel}{-node_id}} + #) { + # $src[-1] = sprintf( '(%s) or %s', + # join (' || ', @null_checks ), + # $src[-1], + # ); + #} + } + + join "\n", @src; } } @@ -1292,9 +2418,20 @@ Returns the result source object for the given relationship. sub related_source { my ($self, $rel) = @_; if( !$self->has_relationship( $rel ) ) { - $self->throw_exception("No such relationship '$rel'"); + $self->throw_exception("No such relationship '$rel' on " . $self->source_name); + } + + # if we are not registered with a schema - just use the prototype + # however if we do have a schema - ask for the source by name (and + # throw in the process if all fails) + if (my $schema = try { $self->schema }) { + $schema->source($self->relationship_info($rel)->{source}); + } + else { + my $class = $self->relationship_info($rel)->{class}; + $self->ensure_class_loaded($class); + $class->result_source_instance; } - return $self->schema->source($self->relationship_info($rel)->{source}); } =head2 related_class @@ -1314,23 +2451,92 @@ Returns the class name for objects in the given relationship. sub related_class { my ($self, $rel) = @_; if( !$self->has_relationship( $rel ) ) { - $self->throw_exception("No such relationship '$rel'"); + $self->throw_exception("No such relationship '$rel' on " . $self->source_name); } return $self->schema->class($self->relationship_info($rel)->{source}); } =head2 handle -Obtain a new handle to this source. Returns an instance of a -L. +=over 4 + +=item Arguments: None + +=item Return value: $source_handle + +=back + +Obtain a new L +for this source. Used as a serializable pointer to this resultsource, as it is not +easy (nor advisable) to serialize CODErefs which may very well be present in e.g. +relationship definitions. =cut sub handle { - return new DBIx::Class::ResultSourceHandle({ - schema => $_[0]->schema, - source_moniker => $_[0]->source_name - }); + return DBIx::Class::ResultSourceHandle->new({ + source_moniker => $_[0]->source_name, + + # so that a detached thaw can be re-frozen + $_[0]->{_detached_thaw} + ? ( _detached_source => $_[0] ) + : ( schema => $_[0]->schema ) + , + }); +} + +my $global_phase_destroy; +sub DESTROY { + return if $global_phase_destroy ||= in_global_destruction; + +###### +# !!! ACHTUNG !!!! +###### +# +# Under no circumstances shall $_[0] be stored anywhere else (like copied to +# a lexical variable, or shifted, or anything else). Doing so will mess up +# the refcount of this particular result source, and will allow the $schema +# we are trying to save to reattach back to the source we are destroying. +# The relevant code checking refcounts is in ::Schema::DESTROY() + + # if we are not a schema instance holder - we don't matter + return if( + ! ref $_[0]->{schema} + or + isweak $_[0]->{schema} + ); + + # weaken our schema hold forcing the schema to find somewhere else to live + # during global destruction (if we have not yet bailed out) this will throw + # which will serve as a signal to not try doing anything else + # however beware - on older perls the exception seems randomly untrappable + # due to some weird race condition during thread joining :((( + local $@; + eval { + weaken $_[0]->{schema}; + + # if schema is still there reintroduce ourselves with strong refs back to us + if ($_[0]->{schema}) { + my $srcregs = $_[0]->{schema}->source_registrations; + for (keys %$srcregs) { + next unless $srcregs->{$_}; + $srcregs->{$_} = $_[0] if $srcregs->{$_} == $_[0]; + } + } + + 1; + } or do { + $global_phase_destroy = 1; + }; + + return; +} + +sub STORABLE_freeze { Storable::nfreeze($_[0]->handle) } + +sub STORABLE_thaw { + my ($self, $cloning, $ice) = @_; + %$self = %{ (Storable::thaw($ice))->resolve }; } =head2 throw_exception @@ -1341,11 +2547,11 @@ See L. sub throw_exception { my $self = shift; - if (defined $self->schema) { - $self->schema->throw_exception(@_); - } else { - croak(@_); - } + + $self->{schema} + ? $self->{schema}->throw_exception(@_) + : DBIx::Class::Exception->throw(@_) + ; } =head2 source_info @@ -1367,8 +2573,6 @@ and don't actually accomplish anything on their own: Creates a new ResultSource object. Not normally called directly by end users. -=cut - =head2 column_info_from_storage =over @@ -1379,81 +2583,12 @@ Creates a new ResultSource object. Not normally called directly by end users. =back -Enables the on-demand automatic loading of the above column -metadata from storage as neccesary. This is *deprecated*, and -should not be used. It will be removed before 1.0. - __PACKAGE__->column_info_from_storage(1); -=head2 sqlt_deploy_callback - -An attribute which contains the callback to trigger on L. -Defaults to L. Can be a code reference or the name -of a method in the current result class. You would change the default value -in case you want to share a hook between several result sources, or if you -want to use a result source without a declared result class. - -=head2 default_sqlt_deploy_hook - -=over - -=item Arguments: $source, $sqlt_table - -=item Return value: undefined - -=back - -Proxies its arguments to a C method on the C -if such a method exists. This is useful to make L create -non-unique indexes, or set table options such as C. For -examples of what you can do with this, see -L. - -=cut - -sub default_sqlt_deploy_hook { - my $self = shift; - - my $class = $self->result_class; - - if ($class and $class->can('sqlt_deploy_hook')) { - $class->sqlt_deploy_hook(@_); - } -} - - -=head2 sqlt_deploy_hook - -=over 4 - -=item Arguments: $source, $sqlt_table - -=item Return value: undefined - -=back - -This is the entry point invoked by L -during the execution of L. -Delegates to the method name or code reference specified in -L. - -Note that the code is called by -L, which in turn is called -before L. Therefore the hook can be used only -to manipulate the L object before it is turned into -SQL fed to the database. If you want to execute post-deploy statements which -currently can can not be generated by L, the suggested -method is to overload L and use -L. - -=cut +Enables the on-demand automatic loading of the above column +metadata from storage as necessary. This is *deprecated*, and +should not be used. It will be removed before 1.0. -sub sqlt_deploy_hook { - my $self = shift; - if ( my $hook = $self->sqlt_deploy_callback) { - $self->$hook(@_); - } -} =head1 AUTHORS