X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSource.pm;h=bb178fc16eb1adf0e11028a8d247fd0ce889386b;hb=a4fcda000aa9833874693ea9bc940a92abbe1b6f;hp=6773ed4f0d6cdbea309bd4f8c107759066674074;hpb=ebc77b538ae411ffc5a5e6451eded2f5d999f2cb;p=dbsrgits%2FDBIx-Class.git diff --git a/lib/DBIx/Class/ResultSource.pm b/lib/DBIx/Class/ResultSource.pm index 6773ed4..bb178fc 100644 --- a/lib/DBIx/Class/ResultSource.pm +++ b/lib/DBIx/Class/ResultSource.pm @@ -4,20 +4,21 @@ use strict; use warnings; use DBIx::Class::ResultSet; +use DBIx::Class::ResultSourceHandle; use Carp::Clan qw/^DBIx::Class/; use Storable; use base qw/DBIx::Class/; -__PACKAGE__->load_components(qw/AccessorGroup/); __PACKAGE__->mk_group_accessors('simple' => qw/_ordered_columns _columns _primaries _unique_constraints name resultset_attributes - schema from _relationships/); + schema from _relationships column_info_from_storage source_info + source_name sqlt_deploy_callback/); __PACKAGE__->mk_group_accessors('component_class' => qw/resultset_class result_class/); -=head1 NAME +=head1 NAME DBIx::Class::ResultSource - Result source object @@ -28,14 +29,19 @@ DBIx::Class::ResultSource - Result source object A ResultSource is a component of a schema from which results can be directly retrieved, most usually a table (see L) +Basic view support also exists, see L<. + =head1 METHODS +=pod + =cut sub new { my ($class, $attrs) = @_; $class = ref $class if ref $class; - my $new = bless({ %{$attrs || {}}, _resultset => undef }, $class); + + my $new = bless { %{$attrs || {}} }, $class; $new->{resultset_class} ||= 'DBIx::Class::ResultSet'; $new->{resultset_attributes} = { %{$new->{resultset_attributes} || {}} }; $new->{_ordered_columns} = [ @{$new->{_ordered_columns}||[]}]; @@ -43,6 +49,7 @@ sub new { $new->{_relationships} = { %{$new->{_relationships}||{}} }; $new->{name} ||= "!!NAME NOT SET!!"; $new->{_columns_info_loaded} ||= 0; + $new->{sqlt_deploy_callback} ||= "default_sqlt_deploy_hook"; return $new; } @@ -50,84 +57,125 @@ sub new { =head2 add_columns - $table->add_columns(qw/col1 col2 col3/); +=over - $table->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...); +=item Arguments: @columns + +=item Return value: The ResultSource object + +=back -Adds columns to the result source. If supplied key => hashref pairs uses -the hashref as the column_info for that column. + $source->add_columns(qw/col1 col2 col3/); -Repeated calls of this method will add more columns, not replace them. + $source->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...); -The contents of the column_info are not set in stone, the following -keys are currently recognised/used by DBIx::Class. +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. + +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. + +The contents of the column_info are not set in stone. The following +keys are currently recognised/used by DBIx::Class: =over 4 -=item accessor +=item accessor -Use this to set the name of the accessor for this column. If unset, +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 +This contains the column type. It is automatically filled by the L producer, and the -L module. If you do not enter the +L module. If you do not enter a data_type, DBIx::Class will attempt to retrieve it from the -database for you, using Ls column_info method. The values of this +database for you, using L's column_info method. The values of this key are typically upper-cased. -Currently there is no standard set of values for the data_type, use -whatever your database(s) support. +Currently there is no standard set of values for the data_type. Use +whatever your database supports. =item size The length of your column, if it is a column type that can have a size -restriction. This is currently not used by DBIx::Class. +restriction. This is currently only used by L. =item is_nullable -If the column is allowed to contain NULL values, set a true value -(typically 1), here. This is currently not used by DBIx::Class. +Set this to a true value for a columns that is allowed to contain +NULL values. This is currently only used by L. =item is_auto_increment -Set this to a true value if this is a column that is somehow -automatically filled. This is used to determine which columns to empty -when cloning objects using C. +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 +L. =item is_foreign_key -Set this to a true value if this column represents a key from a -foreign table. This is currently not used by DBIx::Class. +Set this to a true value for a column that contains a key from a +foreign table. This is currently only used by +L. =item default_value -Set this to the default value which will be inserted into this column -by the database. Can contain either values or functions. This is -currently not used by DBIx::Class. +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 by L. + +See the note on L for more information about possible +issues related to db-side default values. =item sequence -Sets the name of the sequence to use to generate values. If not -specified, L will attempt to retrieve the -name of the sequence from the database automatically. +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 auto_nextval + +Set this to a true value for a column whose value is retrieved +automatically from an oracle sequence. If you do not use an Oracle +trigger to get the nextval, you have to set sequence as well. + +=item extra + +This is used by L and L +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. =back =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 sub add_columns { my ($self, @cols) = @_; $self->_ordered_columns(\@cols) unless $self->_ordered_columns; - + my @added; my $columns = $self->_columns; while (my $col = shift @cols) { @@ -141,13 +189,21 @@ 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 1 if the source has a column of this name, 0 otherwise. +Returns true if the source has a column of this name, false otherwise. =cut @@ -158,31 +214,46 @@ sub has_column { =head2 column_info - my $info = $obj->column_info($col); +=over + +=item Arguments: $colname -Returns the column metadata hashref for a column. See the description -of add_column for information on the contents of the hashref. +=item Return value: Hashref of info + +=back + + my $info = $source->column_info($col); + +Returns the column metadata hashref for a column, as originally passed +to L. See the description of L for information +on the contents of the hashref. =cut sub column_info { my ($self, $column) = @_; - $self->throw_exception("No such column $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->{_columns_info_loaded} + if ( ! $self->_columns->{$column}{data_type} + and $self->column_info_from_storage + and ! $self->{_columns_info_loaded} and $self->schema and $self->storage ) { $self->{_columns_info_loaded}++; - my $info; - # eval for the case of storage without table - eval { $info = $self->storage->columns_info_for($self->from) }; + 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}; + } foreach my $col ( keys %{$self->_columns} ) { - foreach my $i ( keys %{$info->{$col}} ) { - $self->_columns->{$col}{$i} = $info->{$col}{$i}; - } + $self->_columns->{$col} = { + %{ $self->_columns->{$col} }, + %{ $info->{$col} || $lc_info->{lc $col} || {} } + }; } } } @@ -191,9 +262,17 @@ sub column_info { =head2 columns - my @column_names = $obj->columns; +=over + +=item Arguments: None + +=item Return value: Ordered list of column names + +=back + + 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 @@ -205,21 +284,80 @@ sub columns { return @{$self->{_ordered_columns}||[]}; } +=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([ grep { not $to_remove{$_} } @{$self->_ordered_columns} ]); +} + +sub remove_column { shift->remove_columns(@_); } # DO NOT CHANGE THIS TO GLOB + =head2 set_primary_key =over 4 =item Arguments: @cols +=item Return value: undefined + =back Defines one or more columns as primary key for this source. Should be -called after C. +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. +retrieve automatically created values from the database. =cut @@ -237,7 +375,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 @@ -247,21 +394,43 @@ sub primary_columns { =head2 add_unique_constraint -Declare a unique constraint on this source. Call once for each unique -constraint. Unique constraints are used when you call C on a -L, only columns in the constraint are searched, +=over 4 + +=item Arguments: [ $name ], \@colnames -e.g., +=item Return value: undefined + +=back + +Declare a unique constraint on this source. Call once for each unique +constraint. # For UNIQUE (column1, column2) __PACKAGE__->add_unique_constraint( constraint_name => [ qw/column1 column2/ ], ); +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. + +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, $name, $cols) = @_; + my $self = shift; + my $cols = pop @_; + my $name = shift; + + $name ||= $self->name_unique_constraint($cols); foreach my $col (@$cols) { $self->throw_exception("No such column $col on table " . $self->name) @@ -273,9 +442,55 @@ sub add_unique_constraint { $self->_unique_constraints(\%unique_constraints); } +=head2 name_unique_constraint + +=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; +} + =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 @@ -283,17 +498,267 @@ sub unique_constraints { return %{shift->_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 + +sub unique_constraint_names { + my ($self) = @_; + + my %unique_constraints = $self->unique_constraints; + + return keys %unique_constraints; +} + +=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 + +sub unique_constraint_columns { + my ($self, $constraint_name) = @_; + + my %unique_constraints = $self->unique_constraints; + + $self->throw_exception( + "Unknown unique constraint $constraint_name on '" . $self->name . "'" + ) unless exists $unique_constraints{$constraint_name}; + + return @{ $unique_constraints{$constraint_name} }; +} + +=head2 sqlt_deploy_callback + +=over + +=item Arguments: $callback + +=back + + __PACKAGE__->sqlt_deploy_callback('mycallbackmethod'); + +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. + +If not set, the L</default_sqlt_deploy_hook> is called. + +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 + +=over + +=item Arguments: $source, $sqlt_table + +=item Return value: undefined + +=back + +This is the sensible default for L</sqlt_deploy_callback>. + +If a method named C<sqlt_deploy_hook> exists in your Result class, it +will be called and passed the current C<$source> and the +C<$sqlt_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::ResultSetClass; + use base 'DBIx::Class::ResultSet'; + ... + + $source->resultset_class('My::ResultSet::Class'); + +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 + + $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 @_; + + return $self->resultset_class->new( + $self, + { + %{$self->{resultset_attributes}}, + %{$self->schema->default_resultset_attributes} + }, + ); +} + +=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. +retrieval from this source. In the case of a database, the required FROM +clause contents. -=cut +=head2 schema + +=over 4 + +=item Arguments: None + +=item Return value: A schema object + +=back + + my $schema = $source->schema(); + +Returns the L<DBIx::Class::Schema> object that this result source +belongs to. =head2 storage -Returns the storage handle for the current schema. +=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> @@ -303,8 +768,20 @@ 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 @@ -314,9 +791,9 @@ the current schema. For example: 'foreign.book_id' => 'self.id', }); -The condition C<$cond> needs to be an SQL::Abstract-style +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' } @@ -340,29 +817,32 @@ the SQL command immediately before C<JOIN>. An arrayref containing a list of accessors in the foreign class to proxy in the main class. If, for example, you do the following: - + CD->might_have(liner_notes => 'LinerNotes', undef, { proxy => [ qw/notes/ ], }); - + Then, assuming LinerNotes has an accessor named notes, you can do: my $cd = CD->find(1); - $cd->notes('Notes go here'); # set notes -- LinerNotes object is - # created if it doesn't exist + # set notes -- LinerNotes object is created if it doesn't exist + $cd->notes('Notes go here'); =item accessor Specifies the type of accessor that should be created for the -relationship. Valid values are C<single> (for when there is only a single -related object), C<multi> (when there can be many), and C<filter> (for -when there is a single related object, but you also want the relationship -accessor to double as a column accessor). For C<multi> accessors, an -add_to_* method is also created, which calls C<create_related> for the +relationship. Valid values are C<single> (for when there is only a single +related object), C<multi> (when there can be many), and C<filter> (for +when there is a single related object, but you also want the relationship +accessor to double as a column accessor). For C<multi> accessors, an +add_to_* method is also created, which calls C<create_related> for the relationship. =back +Throws an exception if the condition is improperly supplied, or cannot +be resolved using L</resolve_join>. + =cut sub add_relationship { @@ -371,6 +851,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, @@ -384,10 +872,7 @@ sub add_relationship { my $f_source = $self->schema->source($f_source_name); unless ($f_source) { - eval "require $f_source_name;"; - if ($@) { - die $@ unless $@ =~ /Can't locate/; - } + $self->ensure_class_loaded($f_source_name); $f_source = $f_source_name->result_source; #my $s_class = ref($self->schema); #$f_source_name =~ m/^${s_class}::(.*)$/; @@ -399,7 +884,7 @@ sub add_relationship { eval { $self->resolve_join($rel, 'me') }; if ($@) { # If the resolve failed, back out and re-throw the error - delete $rels{$rel}; # + delete $rels{$rel}; # $self->_relationships(\%rels); $self->throw_exception("Error creating relationship $rel: $@"); } @@ -408,7 +893,17 @@ sub add_relationship { =head2 relationships -Returns all valid relationship names for this source +=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 @@ -422,16 +917,19 @@ sub relationships { =item Arguments: $relname +=item Return value: Hashref of relation data, + =back -Returns the relationship information for the specified relationship name +Returns a hash of relationship information for the specified relationship +name. The keys/values are as specified for L</add_relationship>. =cut sub relationship_info { my ($self, $rel) = @_; return $self->_relationships->{$rel}; -} +} =head2 has_relationship @@ -439,9 +937,11 @@ sub relationship_info { =item Arguments: $rel +=item Return value: 1/0 (true/false) + =back -Returns 1 if the source has a relationship of this name, 0 otherwise. +Returns true if the source has a relationship of this name, false otherwise. =cut @@ -450,29 +950,157 @@ sub has_relationship { return exists $self->_relationships->{$rel}; } +=head2 reverse_relationship_info + +=over 4 + +=item Arguments: $relname + +=item Return value: Hashref of relationship data + +=back + +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 it's 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 $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; + + # Get the related result source for this relationship + my $othertable = $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); + + my $back = $othertable->related_source($otherrel); + next unless $back->source_name eq $self->source_name; + + my @othertestconds; + + if (ref $otherrel_info->{cond} eq 'HASH') { + @othertestconds = ($otherrel_info->{cond}); + } + elsif (ref $otherrel_info->{cond} eq 'ARRAY') { + @othertestconds = @{$otherrel_info->{cond}}; + } + else { + next; + } + + 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; + } + } + return $ret; +} + +=head2 compare_relationship_keys + +=over 4 + +=item Arguments: \@keys1, \@keys2 + +=item Return value: 1/0 (true/false) + +=back + +Returns true if both sets of keynames are the same, false otherwise. + +=cut + +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; + } + } + 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; + } + } + + return $found; +} + =head2 resolve_join =over 4 =item Arguments: $relation +=item Return value: Join condition arrayref + =back -Returns the join structure required for the related result source +Returns the join structure required for the related result source. =cut sub resolve_join { - my ($self, $join, $alias, $seen) = @_; + my ($self, $join, $alias, $seen, $force_left) = @_; $seen ||= {}; + $force_left ||= { force => 0 }; if (ref $join eq 'ARRAY') { return map { $self->resolve_join($_, $alias, $seen) } @$join; } elsif (ref $join eq 'HASH') { return map { my $as = ($seen->{$_} ? $_.'_'.($seen->{$_}+1) : $_); - ($self->resolve_join($_, $alias, $seen), - $self->related_source($_)->resolve_join($join->{$_}, $as, $seen)); + local $force_left->{force}; + ( + $self->resolve_join($_, $alias, $seen, $force_left), + $self->related_source($_)->resolve_join( + $join->{$_}, $as, $seen, $force_left + ) + ); } keys %$join; } elsif (ref $join) { $self->throw_exception("No idea how to resolve join reftype ".ref $join); @@ -482,13 +1110,64 @@ sub resolve_join { 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} || ''; + my $type; + if ($force_left->{force}) { + $type = 'left'; + } else { + $type = $rel_info->{attrs}{join_type} || ''; + $force_left->{force} = 1 if lc($type) eq 'left'; + } return [ { $as => $self->related_source($join)->from, -join_type => $type }, $self->resolve_condition($rel_info->{cond}, $as, $alias) ]; } } +=head2 pk_depends_on + +=over 4 + +=item Arguments: $relname, $rel_data + +=item Return value: 1/0 (true/false) + +=back + +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. + +=cut + +sub pk_depends_on { + my ($self, $relname, $rel_data) = @_; + my $cond = $self->relationship_info($relname)->{cond}; + + return 0 unless ref($cond) eq 'HASH'; + + # map { foreign.foo => 'self.bar' } to { bar => 'foo' } + + my $keyhash = { map { my $x = $_; $x =~ s/.*\.//; $x; } reverse %$cond }; + + # assume anything that references our PK probably is dependent on us + # rather than vice versa, unless the far side is (a) defined or (b) + # auto-increment + + my $rel_source = $self->related_source($relname); + + foreach my $p ($self->primary_columns) { + if (exists $keyhash->{$p}) { + unless (defined($rel_data->{$keyhash->{$p}}) + || $rel_source->column_info($keyhash->{$p}) + ->{is_auto_increment}) { + return 0; + } + } + } + + return 1; +} + =head2 resolve_condition =over 4 @@ -503,23 +1182,43 @@ a related conditional from that object. =cut +our $UNRESOLVABLE_CONDITION = \'1 = 0'; + sub resolve_condition { my ($self, $cond, $as, $for) = @_; #warn %$cond; if (ref $cond eq 'HASH') { my %ret; - while (my ($k, $v) = each %{$cond}) { + 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}"); + $self->throw_exception("Invalid rel cond key ${k}"); $v =~ s/^self\.// || - $self->throw_exception("Invalid rel cond val ${v}"); + $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( + "Column ${v} not loaded or not passed to new() prior to insert()" + ." on ${for} trying to resolve relationship (maybe you forgot " + ."to call ->reload_from_storage to get defaults from the db)" + ); + } + 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}"; } @@ -577,7 +1276,7 @@ in the supplied relationships. Examples: # 'artist.name', # 'producer.producerid', # 'producer.name' - #) + #) =cut @@ -619,9 +1318,28 @@ sub resolve_prefetch { $self->throw_exception( "Can't prefetch has_many ${pre} (join cond too complex)") unless ref($rel_info->{cond}) eq 'HASH'; + my $dots = @{[$as_prefix =~ m/\./g]} + 1; # +1 to match the ".${as_prefix}" + if (my ($fail) = grep { @{[$_ =~ m/\./g]} == $dots } + keys %{$collapse}) { + my ($last) = ($fail =~ /([^\.]+)$/); + carp ( + "Prefetching multiple has_many rels ${last} and ${pre} " + .(length($as_prefix) + ? "at the same level (${as_prefix}) " + : "at top level " + ) + . 'will currently disrupt both the functionality of $rs->count(), ' + . 'and the amount of objects retrievable via $rs->next(). ' + . 'Use at your own risk.' + ); + } + #my @col = map { (/^self\.(.+)$/ ? ("${as_prefix}.$1") : ()); } + # values %{$rel_info->{cond}}; + $collapse->{".${as_prefix}${pre}"} = [ $rel_source->primary_columns ]; + # action at a distance. prepending the '.' allows simpler code + # in ResultSet->_collapse_result my @key = map { (/^foreign\.(.+)$/ ? ($1) : ()); } keys %{$rel_info->{cond}}; - $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} @@ -643,9 +1361,11 @@ sub resolve_prefetch { =item Arguments: $relname +=item Return value: $source + =back -Returns the result source object for the given relationship +Returns the result source object for the given relationship. =cut @@ -663,9 +1383,11 @@ sub related_source { =item Arguments: $relname +=item Return value: $classname + =back -Returns the class object for the given relationship +Returns the class name for objects in the given relationship. =cut @@ -677,55 +1399,70 @@ sub related_class { 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 - - $self->resultset_class->new($self, $self->resultset_attributes) - -but is cached from then on unless resultset_class changes. - -=head2 resultset_class - -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. - -=head2 resultset_attributes - -Specify here any attributes you wish to pass to your specialised resultset. +Obtain a new handle to this source. Returns an instance of a +L<DBIx::Class::ResultSourceHandle>. =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 @_; - return $self->{_resultset} - if ref $self->{_resultset} eq $self->resultset_class; - return $self->{_resultset} = $self->resultset_class->new( - $self, $self->{resultset_attributes} - ); +sub handle { + return new DBIx::Class::ResultSourceHandle({ + schema => $_[0]->schema, + source_moniker => $_[0]->source_name + }); } =head2 throw_exception -See throw_exception in L<DBIx::Class::Schema>. +See L<DBIx::Class::Schema/"throw_exception">. =cut sub throw_exception { my $self = shift; - if (defined $self->schema) { + if (defined $self->schema) { $self->schema->throw_exception(@_); } else { croak(@_); } } +=head2 source_info + +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: + + __PACKAGE__->source_info({ + "_tablespace" => 'fast_disk_array_3', + "_engine" => 'InnoDB', + }); + +=head2 new + + $class->new(); + + $class->new({attribute_name => value}); + +Creates a new ResultSource object. Not normally called directly by end users. + +=head2 column_info_from_storage + +=over + +=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 neccesary. This is *deprecated*, and +should not be used. It will be removed before 1.0. + =head1 AUTHORS @@ -737,3 +1474,4 @@ You may distribute this code under the same terms as Perl itself. =cut +1;