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=dbbe1adfd8f1bacaca1cdc20799a008d168d537a;hpb=d84c7d7885c9f2c04ab7f6ea095bcfd85a600a2e;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSource.pm b/lib/DBIx/Class/ResultSource.pm index dbbe1ad..b4dc288 100644 --- a/lib/DBIx/Class/ResultSource.pm +++ b/lib/DBIx/Class/ResultSource.pm @@ -3,19 +3,34 @@ package DBIx::Class::ResultSource; use strict; use warnings; +use base 'DBIx::Class'; + use DBIx::Class::ResultSet; -use Carp::Clan qw/^DBIx::Class/; -use Storable; +use DBIx::Class::ResultSourceHandle; + +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 base qw/DBIx::Class/; -__PACKAGE__->load_components(qw/AccessorGroup/); +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_name/); +__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 @@ -23,22 +38,77 @@ 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. -=head1 METHODS +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. -=pod +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. -=head2 new +Result sources representing select queries, or views, can also be +created, see L for full details. - $class->new(); +=head2 Finding result source objects - $class->new({attribute_name => value}); +As mentioned above, a result source instance is created and stored for +you when you define a L. -Creates a new ResultSource object. Not normally called directly by end users. +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 + +=pod =cut @@ -46,9 +116,7 @@ sub new { my ($class, $attrs) = @_; $class = ref $class if ref $class; - my $new = { %{$attrs || {}}, _resultset => undef }; - bless $new, $class; - + my $new = bless { %{$attrs || {}} }, $class; $new->{resultset_class} ||= 'DBIx::Class::ResultSet'; $new->{resultset_attributes} = { %{$new->{resultset_attributes} || {}} }; $new->{_ordered_columns} = [ @{$new->{_ordered_columns}||[]}]; @@ -56,9 +124,6 @@ sub new { $new->{_relationships} = { %{$new->{_relationships}||{}} }; $new->{name} ||= "!!NAME NOT SET!!"; $new->{_columns_info_loaded} ||= 0; - if(!defined $new->column_info_from_storage) { - $new->{column_info_from_storage} = 1 - } return $new; } @@ -66,13 +131,32 @@ sub new { =head2 add_columns - $table->add_columns(qw/col1 col2 col3/); +=over + +=item Arguments: @columns + +=item Return value: The ResultSource object + +=back + + $source->add_columns(qw/col1 col2 col3/); + + $source->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...); + +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. - $table->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...); +The column names given will be created as accessor methods on your +L objects. You can change the name of the accessor +by supplying an L in the column_info hash. -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. +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: @@ -81,61 +165,130 @@ keys are currently recognised/used by DBIx::Class: =item accessor -Use this to set the name of the accessor for this column. If unset, + { 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 extras +=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 + + { 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 This is used by L and L -to add extra non-generic data to the column. For example: C<< extras +to add extra non-generic data to the column. For example: C<< extra => { unsigned => 1} >> is used by the MySQL producer to set an integer column to unsigned. For more details, see L. @@ -144,9 +297,18 @@ L. =head2 add_column - $table->add_column('col' => \%info?); +=over + +=item Arguments: $colname, \%columninfo? + +=item Return value: 1/0 (true/false) + +=back + + $source->add_column('col' => \%info); -Convenience alias to add_columns. +Add a single column and optional column info. Uses the same column +info keys as L. =cut @@ -157,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; } @@ -167,11 +337,19 @@ sub add_columns { return $self; } -*add_column = \&add_columns; +sub add_column { shift->add_columns(@_); } # DO NOT CHANGE THIS TO GLOB =head2 has_column - if ($obj->has_column($col)) { ... } +=over + +=item Arguments: $colname + +=item Return value: 1/0 (true/false) + +=back + + if ($source->has_column($colname)) { ... } Returns true if the source has a column of this name, false otherwise. @@ -184,10 +362,19 @@ sub has_column { =head2 column_info - my $info = $obj->column_info($col); +=over + +=item Arguments: $colname + +=item Return value: Hashref of info -Returns the column metadata hashref for a column. See the description -of add_column for information on the contents of the hashref. +=back + + my $info = $source->column_info($col); + +Returns the column metadata hashref for a column, as originally passed +to L. See L above for information on the +contents of the hashref. =cut @@ -195,48 +382,47 @@ 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}; } -=head2 column_info_from_storage +=head2 columns -Enables or disables the on-demand automatic loading of the above -column metadata from storage as neccesary. Defaults to true in the -current release, but will default to false in future releases starting -with 0.08000. This is *deprecated*, and should not be used. It will -be removed before 1.0. +=over - __PACKAGE__->column_info_from_storage(0); - __PACKAGE__->column_info_from_storage(1); +=item Arguments: None -=head2 columns +=item Return value: Ordered list of column names + +=back - my @column_names = $obj->columns; + my @column_names = $source->columns; -Returns all column names in the order they were declared to add_columns. +Returns all column names in the order they were declared to L. =cut @@ -244,44 +430,139 @@ 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 remove_columns +=head2 columns_info - $table->remove_columns(qw/col1 col2 col3/); +=over -Removes columns from the result source. +=item Arguments: \@colnames ? -=head2 remove_column +=item Return value: Hashref of column name/info pairs + +=back - $table->remove_column('col'); + my $columns_info = $source->columns_info; -Convenience alias to remove_columns. +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 remove_columns { - my ($self, @cols) = @_; +sub columns_info { + my ($self, $columns) = @_; - return unless $self->_ordered_columns; + my $colinfo = $self->_columns; - my $columns = $self->_columns; - my @remaining; + 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} || {} } + }; + } + }; + } - foreach my $col (@{$self->_ordered_columns}) { - push @remaining, $col unless grep(/$col/, @cols); + 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; } - foreach (@cols) { + return \%ret; +} + +=head2 remove_columns + +=over + +=item Arguments: @colnames + +=item Return value: undefined + +=back + + $source->remove_columns(qw/col1 col2 col3/); + +Removes the given list of columns by name, from the result source. + +B: Removing a column that is also used in the sources primary +key, or in one of the sources unique constraints, B result in a +broken result source. + +=head2 remove_column + +=over + +=item Arguments: $colname + +=item Return value: undefined + +=back + + $source->remove_column('col'); + +Remove a single column by name from the result source, similar to +L. + +B: Removing a column that is also used in the sources primary +key, or in one of the sources unique constraints, B result in a +broken result source. + +=cut + +sub remove_columns { + my ($self, @to_remove) = @_; + + my $columns = $self->_columns + or return; + + my %to_remove; + for (@to_remove) { delete $columns->{$_}; - }; + ++$to_remove{$_}; + } - $self->_ordered_columns(\@remaining); + $self->_ordered_columns([ grep { not $to_remove{$_} } @{$self->_ordered_columns} ]); } -*remove_column = \&remove_columns; +sub remove_column { shift->remove_columns(@_); } # DO NOT CHANGE THIS TO GLOB =head2 set_primary_key @@ -289,15 +570,21 @@ sub remove_columns { =item Arguments: @cols +=item Return value: undefined + =back -Defines one or more columns as primary key for this source. Should be -called after C. +Defines one or more columns as primary key for this source. Must be +called after L. -Additionally, defines a unique constraint named C. +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 @@ -315,7 +602,16 @@ sub set_primary_key { =head2 primary_columns -Read-only accessor which returns the list of primary keys. +=over 4 + +=item Arguments: None + +=item Return value: Ordered list of primary column names + +=back + +Read-only accessor which returns the list of primary keys, supplied by +L. =cut @@ -323,8 +619,57 @@ 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 Return value: undefined + +=back + Declare a unique constraint on this source. Call once for each unique constraint. @@ -337,18 +682,37 @@ 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 pass the constraint +name as the C attribute to L. Then +only columns in the constraint are searched. -Unique constraints are used, for example, when you call -L. 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. =cut 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); @@ -362,25 +726,111 @@ 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 -Return a name for a unique constraint containing the specified columns. These -names consist of the table name and each column name, separated by underscores. +=over 4 + +=item Arguments: \@colnames + +=item Return value: Constraint name + +=back + + $source->table('mytable'); + $source->name_unique_constraint(['col1', 'col2']); + # returns + 'mytable_col1_col2' + +Return a name for a unique constraint containing the specified +columns. The name is created by joining the table name and each column +name, using an underscore character. For example, a constraint on a table named C containing the columns C and C would result in a constraint name of C<cd_artist_title>. +This is used by L</add_unique_constraint> if you do not specify the +optional constraint name. + =cut 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 -Read-only accessor which returns the list of unique constraints on this source. +=over 4 + +=item Arguments: None + +=item Return value: Hash of unique constraint data + +=back + + $source->unique_constraints(); + +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. =cut @@ -390,6 +840,16 @@ sub unique_constraints { =head2 unique_constraint_names +=over 4 + +=item Arguments: None + +=item Return value: Unique constraint names + +=back + + $source->unique_constraint_names(); + Returns the list of unique constraint names defined on this source. =cut @@ -404,6 +864,16 @@ sub unique_constraint_names { =head2 unique_constraint_columns +=over 4 + +=item Arguments: $constraintname + +=item Return value: List of constraint columns + +=back + + $source->unique_constraint_columns('myconstraint'); + Returns the list of columns that make up the specified unique constraint. =cut @@ -420,31 +890,288 @@ sub unique_constraint_columns { return @{ $unique_constraints{$constraint_name} }; } -=head2 from +=head2 sqlt_deploy_callback -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. +=over -=head2 schema +=item Arguments: $callback_name | \&callback_code -Returns the L<DBIx::Class::Schema> object that this result source -belongs too. +=item Return value: $callback_name | \&callback_code -=head2 storage +=back -Returns the storage handle for the current schema. + __PACKAGE__->sqlt_deploy_callback('mycallbackmethod'); -See also: L<DBIx::Class::Storage> + or -=cut + __PACKAGE__->sqlt_deploy_callback(sub { + my ($source_instance, $sqlt_table) = @_; + ... + } ); -sub storage { shift->schema->storage; } +An accessor to set a callback to be called during deployment of +the schema via L<DBIx::Class::Schema/create_ddl_dir> or +L<DBIx::Class::Schema/deploy>. -=head2 add_relationship +The callback can be set as either a code reference or the name of a +method in the current result class. + +Defaults to L</default_sqlt_deploy_hook>. + +Your callback will be passed the $source object representing the +ResultSource instance being deployed, and the +L<SQL::Translator::Schema::Table> 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<DBIx::Class::Schema/sqlt_deploy_hook>. + +See L<DBIx::Class::Manual::Cookbook/Adding Indexes And Functions To +Your SQL> 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<DBIx::Class::Schema/deploy> in your Schema class +and call L<dbh_do|DBIx::Class::Storage::DBI/dbh_do>. + +=head2 default_sqlt_deploy_hook + +This is the default deploy hook implementation which checks if your +current Result class has a C<sqlt_deploy_hook> method, and if present +invokes it B<on the Result class directly>. This is to preserve the +semantics of C<sqlt_deploy_hook> 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 + +=item Arguments: None + +=item Return value: $resultset + +=back + +Returns a resultset for the given source. This will initially be created +on demand by calling + + $self->resultset_class->new($self, $self->resultset_attributes) + +but is cached from then on unless resultset_class changes. + +=head2 resultset_class + +=over 4 + +=item Arguments: $classname + +=item Return value: $classname + +=back + + package My::Schema::ResultSet::Artist; + use base 'DBIx::Class::ResultSet'; + ... + + # In the result class + __PACKAGE__->resultset_class('My::Schema::ResultSet::Artist'); + + # 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<DBIx::Class::ResultSet>, and set it here. If called with no arguments, +this method returns the name of the existing resultset class, if one +exists. + +=head2 resultset_attributes + +=over 4 + +=item Arguments: \%attrs + +=item Return value: \%attrs + +=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 +L<DBIx::Class::ResultSet> produced from this result source. For a full +list see L<DBIx::Class::ResultSet/ATTRIBUTES>. + +=cut + +sub resultset { + my $self = shift; + $self->throw_exception( + 'resultset does not take any arguments. If you want another resultset, '. + 'call it on the schema instead.' + ) if scalar @_; + + $self->resultset_class->new( + $self, + { + try { %{$self->schema->default_resultset_attributes} }, + %{$self->{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 + +=item Arguments: $source_name + +=item Result value: $source_name + +=back + +Set an alternate name for the result source when it is loaded into a schema. +This is useful if you want to refer to a result source by a name other than +its class name. + + package ArchivedBooks; + use base qw/DBIx::Class/; + __PACKAGE__->table('books_archive'); + __PACKAGE__->source_name('Books'); + + # from your schema... + $schema->resultset('Books')->find(1); + +=head2 from + +=over 4 + +=item Arguments: None + +=item Return value: FROM clause + +=back + + my $from_clause = $source->from(); + +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: $schema + +=item Return value: A schema object + +=back + + my $schema = $source->schema(); + +Sets and/or returns the L<DBIx::Class::Schema> 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 + +=over 4 + +=item Arguments: None + +=item Return value: A Storage object + +=back + + $source->storage->debug(1); + +Returns the storage handle for the current schema. + +See also: L<DBIx::Class::Storage> + +=cut + +sub storage { shift->schema->storage; } + +=head2 add_relationship + +=over 4 + +=item Arguments: $relname, $related_source_name, \%cond, [ \%attrs ] + +=item Return value: 1/true if it succeeded + +=back $source->add_relationship('relname', 'related_source', $cond, $attrs); +L<DBIx::Class::Relationship> describes a series of methods which +create pre-defined useful types of relationships. Look there first +before using this method directly. + The relationship name can be arbitrary, but must be unique for each relationship attached to this result source. 'related_source' should be the name with which the related result source was registered with @@ -456,7 +1183,7 @@ the current schema. For example: The condition C<$cond> needs to be an L<SQL::Abstract>-style representation of the join between the tables. For example, if you're -creating a rel from Author to Book, +creating a relation from Author to Book, { 'foreign.author_id' => 'self.id' } @@ -503,6 +1230,9 @@ relationship. =back +Throws an exception if the condition is improperly supplied, or cannot +be resolved. + =cut sub add_relationship { @@ -511,6 +1241,14 @@ sub add_relationship { unless $cond; $attrs ||= {}; + # Check foreign and self are right in cond + if ( (ref $cond ||'') eq 'HASH') { + for (keys %$cond) { + $self->throw_exception("Keys of condition should be of form 'foreign.col', not '$_'") + if /\./ && !/^foreign\./; + } + } + my %rels = %{ $self->_relationships }; $rels{$rel} = { class => $f_source_name, source => $f_source_name, @@ -520,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) { @@ -533,18 +1271,29 @@ 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; } =head2 relationships +=over 4 + +=item Arguments: None + +=item Return value: List of relationship names + +=back + + my @relnames = $source->relationships(); + Returns all relationship names for this source. =cut @@ -559,10 +1308,12 @@ sub relationships { =item Arguments: $relname +=item Return value: Hashref of relation data, + =back Returns a hash of relationship information for the specified relationship -name. +name. The keys/values are as specified for L</add_relationship>. =cut @@ -577,6 +1328,8 @@ sub relationship_info { =item Arguments: $rel +=item Return value: 1/0 (true/false) + =back Returns true if the source has a relationship of this name, false otherwise. @@ -594,164 +1347,348 @@ sub has_relationship { =item Arguments: $relname +=item Return value: Hashref of relationship data + =back -Returns an array of hash references of relationship information for -the other side of the specified relationship name. +Looks through all the relationships on the source this relationship +points to, looking for one whose condition is the reverse of the +condition on this relationship. + +A common use of this is to find the name of the C<belongs_to> relation +opposing a C<has_many> relation. For definition of these look in +L<DBIx::Class::Relationship>. + +The returned hashref is keyed by the name of the opposing +relationship, and contains its data in the same manner as +L</relationship_info>. =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->name eq $self->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 +# all this does is removes the foreign/self prefix from a condition +sub __strip_relcond { + +{ + map + { map { /^ (?:foreign|self) \. (\w+) $/x } ($_, $_[1]{$_}) } + keys %{$_[1]} + } +} -=over 4 +sub compare_relationship_keys { + carp 'compare_relationship_keys is a private method, stop calling it'; + my $self = shift; + $self->_compare_relationship_keys (@_); +} -=item Arguments: $keys1, $keys2 +# 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 - -=over 4 - -=item Arguments: $relation +# Returns the {from} structure used to express JOIN conditions +sub _resolve_join { + my ($self, $join, $alias, $seen, $jpath, $parent_force_left) = @_; -=back + # 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'; -Returns the join structure required for the related result source. + $self->throw_exception ('You must supply a joinpath arrayref as the 4th argument to _resolve_join') + unless ref $jpath eq 'ARRAY'; -=cut + $jpath = [@$jpath]; # copy -sub resolve_join { - my ($self, $join, $alias, $seen) = @_; - $seen ||= {}; - 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) : $_); - ($self->resolve_join($_, $alias, $seen), - $self->related_source($_)->resolve_join($join->{$_}, $as, $seen)); - } keys %$join; - } elsif (ref $join) { + $self->_resolve_join($_, $alias, $seen, $jpath, $parent_force_left); + } @$join; + } + elsif (ref $join eq 'HASH') { + + my @ret; + for my $rel (keys %$join) { + + my $rel_info = $self->relationship_info($rel) + or $self->throw_exception("No such relationship '$rel' on " . $self->source_name); + + my $force_left = $parent_force_left; + $force_left ||= lc($rel_info->{attrs}{join_type}||'') eq 'left'; + + # the actual seen value will be incremented by the recursion + my $as = $self->storage->relname_to_table_alias( + $rel, ($seen->{$rel} && $seen->{$rel} + 1) + ); + + 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; + + } + elsif (ref $join) { $self->throw_exception("No idea how to resolve join reftype ".ref $join); - } else { + } + 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 = $rel_info->{attrs}{join_type} || ''; - return [ { $as => $self->related_source($join)->from, - -join_type => $type }, - $self->resolve_condition($rel_info->{cond}, $as, $alias) ]; + 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) + ]; } } -=head2 resolve_condition +sub pk_depends_on { + carp 'pk_depends_on is a private method, stop calling it'; + my $self = shift; + $self->_pk_depends_on (@_); +} -=over 4 +# 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) = @_; -=item Arguments: $cond, $as, $alias|$object + my $relinfo = $self->relationship_info($relname); -=back + # 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}); -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 $cond = $relinfo->{cond}; + return 0 unless ref($cond) eq 'HASH'; -=cut + # 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) { + if (exists $keyhash->{$p}) { + unless (defined($rel_data->{$keyhash->{$p}}) + || $rel_source->column_info($keyhash->{$p}) + ->{is_auto_increment}) { + return 0; + } + } + } + + return 1; +} sub resolve_condition { - my ($self, $cond, $as, $for) = @_; - #warn %$cond; - if (ref $cond eq 'HASH') { + carp 'resolve_condition is a private method, stop calling it'; + my $self = shift; + $self->_resolve_condition (@_); +} + +our $UNRESOLVABLE_CONDITION = \ '1 = 0'; + +# 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) = @_; + + my $obj_rel = !!blessed $for; + + if (ref $cond eq 'CODE') { + my $relalias = $obj_rel ? 'me' : $as; + + 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 + }); + + 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' + ); + } + + # normalize + for (values %$joinfree_cond) { + $_ = $_->{'='} if ( + ref $_ eq 'HASH' + and + keys %$_ == 1 + and + exists $_->{'='} + ); + } + + # 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}; @@ -762,93 +1699,78 @@ sub resolve_condition { $self->throw_exception("Invalid rel cond val ${v}"); if (ref $for) { # Object #warn "$self $k $for $v"; + unless ($for->has_column_loaded($v)) { + if ($for->in_storage) { + $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; + } $ret{$k} = $for->get_column($v); + #$ret{$k} = $for->get_column($v) if $for->has_column_loaded($v); #warn %ret; } elsif (!defined $for) { # undef, i.e. "no object" $ret{$k} = undef; + } elsif (ref $as eq 'HASH') { # reverse hashref + $ret{$v} = $as->{$k}; } elsif (ref $as) { # reverse object $ret{$v} = $as->get_column($k); } 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' - #) +# 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 ||= []; -=cut - -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 ) { @@ -856,34 +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}" + + #my @col = map { (/^self\.(.+)$/ ? ("${as_prefix}.$1") : ()); } + # values %{$rel_info->{cond}}; my @key = map { (/^foreign\.(.+)$/ ? ($1) : ()); } keys %{$rel_info->{cond}}; - $collapse->{"${as_prefix}${pre}"} = \@key; - 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; } } @@ -893,6 +2407,8 @@ sub resolve_prefetch { =item Arguments: $relname +=item Return value: $source + =back Returns the result source object for the given relationship. @@ -902,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 @@ -913,6 +2440,8 @@ sub related_source { =item Arguments: $relname +=item Return value: $classname + =back Returns the class name for objects in the given relationship. @@ -922,94 +2451,144 @@ 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 resultset +=head2 handle -Returns a resultset for the given source. This will initially be created -on demand by calling +=over 4 - $self->resultset_class->new($self, $self->resultset_attributes) +=item Arguments: None -but is cached from then on unless resultset_class changes. +=item Return value: $source_handle -=head2 resultset_class +=back -` package My::ResultSetClass; - use base 'DBIx::Class::ResultSet'; - ... +Obtain a new L<result source handle instance|DBIx::Class::ResultSourceHandle> +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 - $source->resultset_class('My::ResultSet::Class'); +sub handle { + return DBIx::Class::ResultSourceHandle->new({ + source_moniker => $_[0]->source_name, -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<DBIx::Class::ResultSet>, and set it here. + # so that a detached thaw can be re-frozen + $_[0]->{_detached_thaw} + ? ( _detached_source => $_[0] ) + : ( schema => $_[0]->schema ) + , + }); +} -=head2 resultset_attributes +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} + ); - $source->resultset_attributes({ order_by => [ 'id' ] }); + # 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; + }; -Specify here any attributes you wish to pass to your specialised -resultset. For a full list of these, please see -L<DBIx::Class::ResultSet/ATTRIBUTES>. + return; +} + +sub STORABLE_freeze { Storable::nfreeze($_[0]->handle) } + +sub STORABLE_thaw { + my ($self, $cloning, $ice) = @_; + %$self = %{ (Storable::thaw($ice))->resolve }; +} + +=head2 throw_exception + +See L<DBIx::Class::Schema/"throw_exception">. =cut -sub resultset { +sub throw_exception { my $self = shift; - $self->throw_exception( - 'resultset does not take any arguments. If you want another resultset, '. - 'call it on the schema instead.' - ) if scalar @_; - - # disabled until we can figure out a way to do it without consistency issues - # - #return $self->{_resultset} - # if ref $self->{_resultset} eq $self->resultset_class; - #return $self->{_resultset} = - return $self->resultset_class->new( - $self, $self->{resultset_attributes} - ); + $self->{schema} + ? $self->{schema}->throw_exception(@_) + : DBIx::Class::Exception->throw(@_) + ; } -=head2 source_name +=head2 source_info -=over 4 +Stores a hashref of per-source metadata. No specific key names +have yet been standardized, the examples below are purely hypothetical +and don't actually accomplish anything on their own: -=item Arguments: $source_name + __PACKAGE__->source_info({ + "_tablespace" => 'fast_disk_array_3', + "_engine" => 'InnoDB', + }); -=back +=head2 new -Set the name of the result source when it is loaded into a schema. -This is usefull if you want to refer to a result source by a name other than -its class name. + $class->new(); - package ArchivedBooks; - use base qw/DBIx::Class/; - __PACKAGE__->table('books_archive'); - __PACKAGE__->source_name('Books'); + $class->new({attribute_name => value}); - # from your schema... - $schema->resultset('Books')->find(1); +Creates a new ResultSource object. Not normally called directly by end users. -=head2 throw_exception +=head2 column_info_from_storage -See L<DBIx::Class::Schema/"throw_exception">. +=over -=cut +=item Arguments: 1/0 (default: 0) + +=item Return value: 1/0 + +=back + + __PACKAGE__->column_info_from_storage(1); + +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 throw_exception { - my $self = shift; - if (defined $self->schema) { - $self->schema->throw_exception(@_); - } else { - croak(@_); - } -} =head1 AUTHORS @@ -1021,3 +2600,4 @@ You may distribute this code under the same terms as Perl itself. =cut +1;