X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSource.pm;h=c3a318ff3408dbc4a23cc153ebb25b0c5b58763d;hb=c19ca6e80;hp=c577684b2ddeb6ce53fd1e480ddf5fb707190c12;hpb=3441fd57f3e40b9862f41618b49178f66de1a4d8;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSource.pm b/lib/DBIx/Class/ResultSource.pm index c577684..c3a318f 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 qw/DBIx::Class::ResultSource::RowParser 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::Carp; +use DBIx::Class::_Util 'UNRESOLVABLE_CONDITION'; +use SQL::Abstract 'is_literal_value'; +use Devel::GlobalDestruction; +use Try::Tiny; +use List::Util 'first'; +use Scalar::Util qw/blessed weaken isweak/; + +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 - source_info/); +__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('inherited' => 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,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 Result object: + + $result->result_source; + +=item From a ResultSet object: + + $rs->result_source; + +=back + +=head1 METHODS + +=pod =cut @@ -47,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}||[]}]; @@ -62,26 +129,34 @@ sub new { =pod -=head2 source_info +=head2 add_columns -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: +=over - __PACKAGE__->source_info({ - "_tablespace" => 'fast_disk_array_3', - "_engine" => 'InnoDB', - }); +=item Arguments: @columns -=head2 add_columns +=item Return Value: L<$result_source|/new> + +=back - $table->add_columns(qw/col1 col2 col3/); + $source->add_columns(qw/col1 col2 col3/); - $table->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...); + $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 +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: @@ -90,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 column 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. @@ -153,9 +297,18 @@ L. =head2 add_column - $table->add_column('col' => \%info?); +=over + +=item Arguments: $colname, \%columninfo? -Convenience alias to add_columns. +=item Return Value: 1/0 (true/false) + +=back + + $source->add_column('col' => \%info); + +Add a single column and optional column info. Uses the same column +info keys as L. =cut @@ -166,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; } @@ -176,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. @@ -193,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 + +=back + + my $info = $source->column_info($col); -Returns the column metadata hashref for a column. See the description -of add_column for information on the contents of the hashref. +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 @@ -204,45 +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 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. +=over - __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 @@ -250,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 || $self->name || 'Unknown source...?', + )); + } + } + } + else { + %ret = %$colinfo; } - foreach (@cols) { + return \%ret; +} + +=head2 remove_columns + +=over + +=item Arguments: @colnames + +=item Return Value: not defined + +=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: not defined + +=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 @@ -295,25 +570,38 @@ sub remove_columns { =item Arguments: @cols +=item Return Value: not defined + =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 sub set_primary_key { my ($self, @cols) = @_; - # check if primary key columns are valid columns - foreach my $col (@cols) { - $self->throw_exception("No such column $col on table " . $self->name) - unless $self->has_column($col); + + my $colinfo = $self->columns_info(\@cols); + for my $col (@cols) { + carp_unique(sprintf ( + "Primary key of source '%s' includes the column '%s' which has its " + . "'is_nullable' attribute set to true. This is a mistake and will cause " + . 'various Result-object operations to fail', + $self->source_name || $self->name || 'Unknown source...?', + $col, + )) if $colinfo->{$col}{is_nullable}; } + $self->_primaries(\@cols); $self->add_unique_constraint(primary => \@cols); @@ -321,7 +609,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 @@ -329,8 +626,71 @@ 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_or_die { + 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; +} + +# same as above but mandating single-column PK (used by relationship condition +# inference) +sub _single_pri_col_or_die { + my $self = shift; + my ($pri, @too_many) = $self->_pri_cols_or_die; + + $self->throw_exception( sprintf( + "Operation requires a single-column primary key declared on '%s'", + $self->source_name || $self->result_class || $self->name || 'Unknown source...?', + )) if @too_many; + return $pri; +} + + +=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: not defined + +=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: not defined + +=back + Declare a unique constraint on this source. Call once for each unique constraint. @@ -343,18 +703,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); @@ -368,25 +747,111 @@ sub add_unique_constraint { $self->_unique_constraints(\%unique_constraints); } +=head2 add_unique_constraints + +=over 4 + +=item Arguments: @constraints + +=item Return Value: not defined + +=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 @@ -396,6 +861,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 @@ -410,9 +885,19 @@ sub unique_constraint_names { =head2 unique_constraint_columns -Returns the list of columns that make up the specified unique constraint. +=over 4 -=cut +=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 sub unique_constraint_columns { my ($self, $constraint_name) = @_; @@ -426,22 +911,318 @@ 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<DBIx::Class::Schema/create_ddl_dir> or +L<DBIx::Class::Schema/deploy>. + +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 result_class + +=over 4 + +=item Arguments: $classname + +=item Return Value: $classname + +=back + + use My::Schema::ResultClass::Inflator; + ... + + use My::Schema::Artist; + ... + __PACKAGE__->result_class('My::Schema::ResultClass::Inflator'); + +Set the default result class for this source. You can use this to create +and use your own result inflator. See L<DBIx::Class::ResultSet/result_class> +for more details. + +Please note that setting this to something like +L<DBIx::Class::ResultClass::HashRefInflator> will make every result unblessed +and make life more difficult. Inflators like those are better suited to +temporary usage via L<DBIx::Class::ResultSet/result_class>. + +=head2 resultset + +=over 4 + +=item Arguments: none + +=item Return Value: L<$resultset|DBIx::Class::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: L<\%attrs|DBIx::Class::ResultSet/ATTRIBUTES> + +=item Return Value: L<\%attrs|DBIx::Class::ResultSet/ATTRIBUTES> + +=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. + +B<CAVEAT>: C<resultset_attributes> comes with its own set of issues and +bugs! While C<resultset_attributes> isn't deprecated per se, its usage is +not recommended! + +Since relationships use attributes to link tables together, the "default" +attributes you set may cause unpredictable and undesired behavior. Furthermore, +the defaults cannot be turned off, so you are stuck with them. + +In most cases, what you should actually be using are project-specific methods: + + package My::Schema::ResultSet::Artist; + use base 'DBIx::Class::ResultSet'; + ... + + # BAD IDEA! + #__PACKAGE__->resultset_attributes({ prefetch => 'tracks' }); + + # GOOD IDEA! + sub with_tracks { shift->search({}, { prefetch => 'tracks' }) } + + # in your code + $schema->resultset('Artist')->with_tracks->... + +This gives you the flexibility of not using it when you don't need it. + +For more complex situations, another solution would be to use a virtual view +via L<DBIx::Class::ResultSource::View>. + +=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 -Returns the L<DBIx::Class::Schema> object that this result source -belongs too. +=over 4 + +=item Arguments: L<$schema?|DBIx::Class::Schema> + +=item Return Value: L<$schema|DBIx::Class::Schema> + +=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 -Returns the storage handle for the current schema. +=over 4 + +=item Arguments: none + +=item Return Value: L<$storage|DBIx::Class::Storage> + +=back + + $source->storage->debug(1); -See also: L<DBIx::Class::Storage> +Returns the L<storage handle|DBIx::Class::Storage> for the current schema. =cut @@ -449,7 +1230,19 @@ sub storage { shift->schema->storage; } =head2 add_relationship - $source->add_relationship('relname', 'related_source', $cond, $attrs); +=over 4 + +=item Arguments: $rel_name, $related_source_name, \%cond, \%attrs? + +=item Return Value: 1/true if it succeeded + +=back + + $source->add_relationship('rel_name', '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 @@ -462,7 +1255,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' } @@ -509,6 +1302,9 @@ relationship. =back +Throws an exception if the condition is improperly supplied, or cannot +be resolved. + =cut sub add_relationship { @@ -517,6 +1313,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, @@ -526,7 +1330,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) { @@ -539,18 +1343,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: L<@rel_names|DBIx::Class::Relationship> + +=back + + my @rel_names = $source->relationships(); + Returns all relationship names for this source. =cut @@ -563,25 +1378,29 @@ sub relationships { =over 4 -=item Arguments: $relname +=item Arguments: L<$rel_name|DBIx::Class::Relationship> + +=item Return Value: L<\%rel_data|DBIx::Class::Relationship::Base/add_relationship> =back Returns a hash of relationship information for the specified relationship -name. +name. The keys/values are as specified for L<DBIx::Class::Relationship::Base/add_relationship>. =cut sub relationship_info { - my ($self, $rel) = @_; - return $self->_relationships->{$rel}; + #my ($self, $rel) = @_; + return shift->_relationships->{+shift}; } =head2 has_relationship =over 4 -=item Arguments: $rel +=item Arguments: L<$rel_name|DBIx::Class::Relationship> + +=item Return Value: 1/0 (true/false) =back @@ -590,314 +1409,673 @@ Returns true if the source has a relationship of this name, false otherwise. =cut sub has_relationship { - my ($self, $rel) = @_; - return exists $self->_relationships->{$rel}; + #my ($self, $rel) = @_; + return exists shift->_relationships->{+shift}; } =head2 reverse_relationship_info =over 4 -=item Arguments: $relname +=item Arguments: L<$rel_name|DBIx::Class::Relationship> + +=item Return Value: L<\%rel_data|DBIx::Class::Relationship::Base/add_relationship> =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}); + + my $registered_source_name = $self->source_name; - # Get the related result source for this relationship - my $othertable = $self->related_source($rel); + # this may be a partial schema or something else equally esoteric + my $other_rsrc = $self->related_source($rel); # 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 ($registered_source_name) { + next if $registered_source_name ne ($roundtrip_rsrc->source_name || '') } else { - next; + next if $self->result_class ne $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 +# Returns the {from} structure used to express JOIN conditions +sub _resolve_join { + my ($self, $join, $alias, $seen, $jpath, $parent_force_left) = @_; -=over 4 - -=item Arguments: $relation - -=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 ) + 1, + }, + 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, $rel_name, $rel_data) = @_; -=item Arguments: $cond, $as, $alias|$object + my $relinfo = $self->relationship_info($rel_name); -=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($rel_name); -sub resolve_condition { - my ($self, $cond, $as, $for) = @_; - #warn %$cond; - if (ref $cond eq 'HASH') { - my %ret; - foreach my $k (keys %{$cond}) { - my $v = $cond->{$k}; - # XXX should probably check these are valid columns - $k =~ s/^foreign\.// || - $self->throw_exception("Invalid rel cond key ${k}"); - $v =~ s/^self\.// || - $self->throw_exception("Invalid rel cond val ${v}"); - if (ref $for) { # Object - #warn "$self $k $for $v"; - $ret{$k} = $for->get_column($v); - #warn %ret; - } elsif (!defined $for) { # undef, i.e. "no object" - $ret{$k} = undef; - } 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}"; + 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 \%ret; - } elsif (ref $cond eq 'ARRAY') { - return [ map { $self->resolve_condition($_, $as, $for) } @$cond ]; - } else { - die("Can't handle this yet :("); } + + return 1; } -=head2 resolve_prefetch +sub _resolve_condition { +# carp_unique sprintf +# '_resolve_condition is a private method, and moreover is about to go ' +# . 'away. Please contact the development team at %s if you believe you ' +# . 'have a genuine use for this method, in order to discuss alternatives.', +# DBIx::Class::_ENV_::HELP_URL, +# ; -=over 4 +####################### +### API Design? What's that...? (a backwards compatible shim, kill me now) -=item Arguments: hashref/arrayref/scalar + my ($self, $cond, @res_args, $rel_name); -=back + # we *SIMPLY DON'T KNOW YET* which arg is which, yay + ($self, $cond, $res_args[0], $res_args[1], $rel_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. 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' - #) + # assume that an undef is an object-like unset (set_from_related(undef)) + my @is_objlike = map { ! defined $_ or length ref $_ } (@res_args); -=cut + # turn objlike into proper objects for saner code further down + for (0,1) { + next unless $is_objlike[$_]; -sub resolve_prefetch { - my ($self, $pre, $alias, $seen, $order, $collapse) = @_; - $seen ||= {}; - #$alias ||= $self->name; - #warn $alias, Dumper $pre; - if( ref $pre eq 'ARRAY' ) { - return - map { $self->resolve_prefetch( $_, $alias, $seen, $order, $collapse ) } - @$pre; + if ( defined blessed $res_args[$_] ) { + + # but wait - there is more!!! WHAT THE FUCK?!?!?!?! + if ($res_args[$_]->isa('DBIx::Class::ResultSet')) { + carp('Passing a resultset for relationship resolution makes no sense - invoking __gremlins__'); + $is_objlike[$_] = 0; + $res_args[$_] = '__gremlins__'; + } + } + else { + $res_args[$_] ||= {}; + + # hate everywhere - have to pass in as a plain hash + # pretending to be an object at least for now + $self->throw_exception("Unsupported object-like structure encountered: $res_args[$_]") + unless ref $res_args[$_] eq 'HASH'; + } } - 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) - } keys %$pre; - #die Dumper \@ret; - return @ret; + + $self->throw_exception('No practical way to resolve a relationship between two structures') + if $is_objlike[0] and $is_objlike[1]; + + my $args = { + condition => $cond, + + # where-is-waldo block guesses relname, then further down we override it if available + ( + $is_objlike[1] ? ( rel_name => $res_args[0], self_alias => $res_args[0], foreign_alias => 'me', self_resultobj => $res_args[1] ) + : $is_objlike[0] ? ( rel_name => $res_args[1], self_alias => 'me', foreign_alias => $res_args[1], foreign_resultobj => $res_args[0] ) + : ( rel_name => $res_args[0], self_alias => $res_args[1], foreign_alias => $res_args[0] ) + ), + + ( $rel_name ? ( rel_name => $rel_name ) : () ), + }; +####################### + + # now it's fucking easy isn't it?! + my $rc = $self->_resolve_relationship_condition( $args ); + + my @res = ( + ( $rc->{join_free_condition} || $rc->{condition} ), + ! $rc->{join_free_condition}, + ); + + # _resolve_relationship_condition always returns qualified cols even in the + # case of join_free_condition, but nothing downstream expects this + if (ref $res[0] eq 'HASH' and ($is_objlike[0] or $is_objlike[1]) ) { + $res[0] = { map + { ($_ =~ /\.(.+)/) => $res[0]{$_} } + keys %{$res[0]} + }; } - elsif( ref $pre ) { - $self->throw_exception( - "don't know how to resolve prefetch reftype ".ref($pre)); + + # and more legacy + return wantarray ? @res : $res[0]; +} + +# Keep this indefinitely. There is evidence of both CPAN and +# darkpan using it, and there isn't much harm in an extra var +# anyway. +our $UNRESOLVABLE_CONDITION = UNRESOLVABLE_CONDITION; +# YES I KNOW THIS IS EVIL +# it is there to save darkpan from themselves, since internally +# we are moving to a constant +Internals::SvREADONLY($UNRESOLVABLE_CONDITION => 1); + +# Resolves the passed condition to a concrete query fragment and extra +# metadata +# +## self-explanatory API, modeled on the custom cond coderef: +# rel_name +# foreign_alias +# foreign_resultobj +# self_alias +# self_resultobj +# require_join_free_condition +# infer_values_based_on (optional, mandatory hashref argument) +# condition (optional, derived from $self->rel_info(rel_name)) +# +## returns a hash +# condition +# identity_map +# join_free_condition (maybe unset) +# inferred_values (always either complete or unset) +# +sub _resolve_relationship_condition { + my $self = shift; + + my $args = { ref $_[0] eq 'HASH' ? %{ $_[0] } : @_ }; + + for ( qw( rel_name self_alias foreign_alias ) ) { + $self->throw_exception("Mandatory argument '$_' to _resolve_relationship_condition() is not a plain string") + if !defined $args->{$_} or length ref $args->{$_}; + } + + my $exception_rel_id = "relationship '$args->{rel_name}' on source '@{[ $self->source_name ]}'"; + + my $rel_info = $self->relationship_info($args->{rel_name}); + # or $self->throw_exception( "No such $exception_rel_id" ); + + $self->throw_exception("No practical way to resolve $exception_rel_id between two objects") + if defined $args->{self_resultobj} and defined $args->{foreign_resultobj}; + + $self->throw_exception( "Argument to infer_values_based_on must be a hash" ) + if exists $args->{infer_values_based_on} and ref $args->{infer_values_based_on} ne 'HASH'; + + $args->{require_join_free_condition} ||= !!$args->{infer_values_based_on}; + + $args->{condition} ||= $rel_info->{cond}; + + if (exists $args->{self_resultobj}) { + if (defined blessed $args->{self_resultobj}) { +# $self->throw_exception( "Object '$args->{self_resultobj}' must be of class '@{[ $self->result_class ]}'" ) +# unless $args->{self_resultobj}->isa($self->result_class); + } + else { + $args->{self_resultobj} = DBIx::Class::Core->new({ + -result_source => $self, + %{ $args->{self_resultobj}||{} } + }); + } + } + + if (exists $args->{foreign_resultobj}) { + if (defined blessed $args->{foreign_resultobj}) { +# $self->throw_exception( "Object '$args->{foreign_resultobj}' must be of class '$rel_info->{class}'" ) +# unless $args->{foreign_resultobj}->isa($rel_info->{class}); + } + else { + $args->{foreign_resultobj} = DBIx::Class::Core->new({ + -result_source => $self->related_source($args->{rel_name}), + %{ $args->{foreign_resultobj}||{} } + }); + } + } + + my $ret; + + if (ref $args->{condition} eq 'CODE') { + + my $cref_args = { + rel_name => $args->{rel_name}, + self_resultsource => $self, + self_alias => $args->{self_alias}, + foreign_alias => $args->{foreign_alias}, + ( map + { (exists $args->{$_}) ? ( $_ => $args->{$_} ) : () } + qw( self_resultobj foreign_resultobj ) + ), + }; + + # legacy - never remove these!!! + $cref_args->{foreign_relname} = $cref_args->{rel_name}; + + $cref_args->{self_rowobj} = $cref_args->{self_resultobj} + if exists $cref_args->{self_resultobj}; + + ($ret->{condition}, $ret->{join_free_condition}, my @extra) = $args->{condition}->($cref_args); + + # FIXME sanity check + carp_unique('A custom condition coderef can return at most 2 conditions: extra return values discarded') + if @extra; + + if (my $jfc = $ret->{join_free_condition}) { + + $self->throw_exception ( + "The join-free condition returned for $exception_rel_id must be a hash reference" + ) unless ref $jfc eq 'HASH'; + + my ($joinfree_alias, $joinfree_source); + if (defined $args->{self_resultobj}) { + $joinfree_alias = $args->{foreign_alias}; + $joinfree_source = $self->related_source($args->{rel_name}); + } + elsif (defined $args->{foreign_resultobj}) { + $joinfree_alias = $args->{self_alias}; + $joinfree_source = $self; + } + + # FIXME sanity check until things stabilize, remove at some point + $self->throw_exception ( + "A join-free condition returned for $exception_rel_id without a result object to chain from" + ) unless $joinfree_alias; + + my $fq_col_list = { map + { ( "$joinfree_alias.$_" => 1 ) } + $joinfree_source->columns + }; + + $fq_col_list->{$_} or $self->throw_exception ( + "The join-free condition returned for $exception_rel_id may only " + . 'contain keys that are fully qualified column names of the corresponding source' + ) for keys %$jfc; + + } + } + elsif (ref $args->{condition} eq 'HASH') { + + # the condition is static - use parallel arrays + # for a "pivot" depending on which side of the + # rel did we get as an object + my (@f_cols, @l_cols); + for my $fc (keys %{$args->{condition}}) { + my $lc = $args->{condition}{$fc}; + + # FIXME STRICTMODE should probably check these are valid columns + $fc =~ s/^foreign\.// || + $self->throw_exception("Invalid rel cond key '$fc'"); + + $lc =~ s/^self\.// || + $self->throw_exception("Invalid rel cond val '$lc'"); + + push @f_cols, $fc; + push @l_cols, $lc; + } + + # construct the crosstable condition and the identity map + for (0..$#f_cols) { + $ret->{condition}{"$args->{foreign_alias}.$f_cols[$_]"} = { -ident => "$args->{self_alias}.$l_cols[$_]" }; + $ret->{identity_map}{$l_cols[$_]} = $f_cols[$_]; + }; + + if (exists $args->{self_resultobj} or exists $args->{foreign_resultobj}) { + + my ($obj, $obj_alias, $plain_alias, $obj_cols, $plain_cols) = defined $args->{self_resultobj} + ? ( @{$args}{qw( self_resultobj self_alias foreign_alias )}, \@l_cols, \@f_cols ) + : ( @{$args}{qw( foreign_resultobj foreign_alias self_alias )}, \@f_cols, \@l_cols ) + ; + + for my $i (0..$#$obj_cols) { + + if ( + defined $args->{self_resultobj} + and + ! $obj->has_column_loaded($obj_cols->[$i]) + ) { + + $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.', + $args->{rel_name}, + $obj, + $obj_cols->[$i], + ) if $obj->in_storage; + + # FIXME - temporarly force-override + delete $args->{require_join_free_condition}; + $ret->{join_free_condition} = UNRESOLVABLE_CONDITION; + last; + } + else { + $ret->{join_free_condition}{"$plain_alias.$plain_cols->[$i]"} = $obj->get_column($obj_cols->[$i]); + } + } + } + } + elsif (ref $args->{condition} eq 'ARRAY') { + if (@{$args->{condition}} == 0) { + $ret = { + condition => UNRESOLVABLE_CONDITION, + join_free_condition => UNRESOLVABLE_CONDITION, + }; + } + elsif (@{$args->{condition}} == 1) { + $ret = $self->_resolve_relationship_condition({ + %$args, + condition => $args->{condition}[0], + }); + } + else { + # we are discarding inferred values here... likely incorrect... + # then again - the entire thing is an OR, so we *can't* use them anyway + for my $subcond ( map + { $self->_resolve_relationship_condition({ %$args, condition => $_ }) } + @{$args->{condition}} + ) { + $self->throw_exception('Either all or none of the OR-condition members can resolve to a join-free condition') + if $ret->{join_free_condition} and ! $subcond->{join_free_condition}; + + $subcond->{$_} and push @{$ret->{$_}}, $subcond->{$_} for (qw(condition join_free_condition)); + } + } } else { - my $count = ++$seen->{$pre}; - my $as = ($count > 1 ? "${pre}_${count}" : $pre); - my $rel_info = $self->relationship_info( $pre ); - $self->throw_exception( $self->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') { - $self->throw_exception( - "Can't prefetch has_many ${pre} (join cond too complex)") - unless ref($rel_info->{cond}) eq 'HASH'; - 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)); + $self->throw_exception ("Can't handle condition $args->{condition} for $exception_rel_id yet :("); + } + + $self->throw_exception(ucfirst "$exception_rel_id does not resolve to a join-free condition fragment") if ( + $args->{require_join_free_condition} + and + ( ! $ret->{join_free_condition} or $ret->{join_free_condition} eq UNRESOLVABLE_CONDITION ) + ); + + # we got something back - sanity check and infer values if we can + my @nonvalues; + if ( my $jfc = $ret->{join_free_condition} and $ret->{join_free_condition} ne UNRESOLVABLE_CONDITION ) { + + my $jfc_eqs = $self->schema->storage->_extract_fixed_condition_columns($jfc, 'consider_nulls'); + + if (keys %$jfc_eqs) { + + for (keys %$jfc) { + # $jfc is fully qualified by definition + my ($col) = $_ =~ /\.(.+)/; + + if (exists $jfc_eqs->{$_} and ($jfc_eqs->{$_}||'') ne UNRESOLVABLE_CONDITION) { + $ret->{inferred_values}{$col} = $jfc_eqs->{$_}; + } + elsif ( !$args->{infer_values_based_on} or ! exists $args->{infer_values_based_on}{$col} ) { + push @nonvalues, $col; + } + } + + # all or nothing + delete $ret->{inferred_values} if @nonvalues; } + } + + # did the user explicitly ask + if ($args->{infer_values_based_on}) { + + $self->throw_exception(sprintf ( + "Unable to complete value inferrence - custom $exception_rel_id returns conditions instead of values for column(s): %s", + map { "'$_'" } @nonvalues + )) if @nonvalues; + + + $ret->{inferred_values} ||= {}; - return map { [ "${as}.$_", "${as_prefix}${pre}.$_", ] } - $rel_source->columns; - #warn $alias, Dumper (\@ret); - #return @ret; + $ret->{inferred_values}{$_} = $args->{infer_values_based_on}{$_} + for keys %{$args->{infer_values_based_on}}; } + + # add the identities based on the main condition + # (may already be there, since easy to calculate on the fly in the HASH case) + if ( ! $ret->{identity_map} ) { + + my $col_eqs = $self->schema->storage->_extract_fixed_condition_columns($ret->{condition}); + + my $colinfos; + for my $lhs (keys %$col_eqs) { + + next if $col_eqs->{$lhs} eq UNRESOLVABLE_CONDITION; + my ($rhs) = @{ is_literal_value( $ret->{condition}{$lhs} ) || next }; + + # there is no way to know who is right and who is left + # therefore the ugly scan below + $colinfos ||= $self->schema->storage->_resolve_column_info([ + { -alias => $args->{self_alias}, -rsrc => $self }, + { -alias => $args->{foreign_alias}, -rsrc => $self->related_source($args->{rel_name}) }, + ]); + + my ($l_col, $l_alias, $r_col, $r_alias) = map { + ( reverse $_ =~ / ^ (?: ([^\.]+) $ | ([^\.]+) \. (.+) ) /x )[0,1] + } ($lhs, $rhs); + + if ( + $colinfos->{$l_col} + and + $colinfos->{$r_col} + and + $colinfos->{$l_col}{-source_alias} ne $colinfos->{$r_col}{-source_alias} + ) { + ( $colinfos->{$l_col}{-source_alias} eq $args->{self_alias} ) + ? ( $ret->{identity_map}{$l_col} = $r_col ) + : ( $ret->{identity_map}{$r_col} = $l_col ) + ; + } + } + } + + $ret } =head2 related_source =over 4 -=item Arguments: $relname +=item Arguments: $rel_name + +=item Return Value: $source =back @@ -908,16 +2086,29 @@ 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 =over 4 -=item Arguments: $relname +=item Arguments: $rel_name + +=item Return Value: $classname =back @@ -928,110 +2119,148 @@ 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: L<$source_handle|DBIx::Class::ResultSourceHandle> -=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. - $source->resultset_class('My::ResultSet::Class'); +=cut -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. +sub handle { + 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 ) + , + }); +} -=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. + 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 handle +=head2 column_info_from_storage -Obtain a new handle to this source. Returns an instance of a -L<DBIx::Class::ResultSourceHandle>. +=over -=cut +=item Arguments: 1/0 (default: 0) -sub handle { - return new DBIx::Class::ResultSourceHandle({ - schema => $_[0]->schema, - source_moniker => $_[0]->source_name - }); -} +=item Return Value: 1/0 -=head2 throw_exception +=back -See L<DBIx::Class::Schema/"throw_exception">. + __PACKAGE__->column_info_from_storage(1); -=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 throw_exception { - my $self = shift; - if (defined $self->schema) { - $self->schema->throw_exception(@_); - } else { - croak(@_); - } -} -=head1 AUTHORS +=head1 AUTHOR AND CONTRIBUTORS -Matt S. Trout <mst@shadowcatsystems.co.uk> +See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class =head1 LICENSE @@ -1039,3 +2268,4 @@ You may distribute this code under the same terms as Perl itself. =cut +1;