X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=blobdiff_plain;f=lib%2FDBIx%2FClass%2FResultSet.pm;h=233597e45c0943b635a34155a17d193d565e2b10;hb=cdacba7e0c9b7bc947ae47238be4c6a25f9c7b13;hp=e03a16d2a489fbcaf51e1bc6a6ee12b1e2f30a97;hpb=ce65bba6aa64106758695dce4e1c0ccadad0d4c6;p=dbsrgits%2FDBIx-Class-Historic.git diff --git a/lib/DBIx/Class/ResultSet.pm b/lib/DBIx/Class/ResultSet.pm index e03a16d..233597e 100644 --- a/lib/DBIx/Class/ResultSet.pm +++ b/lib/DBIx/Class/ResultSet.pm @@ -3,217 +3,381 @@ package DBIx::Class::ResultSet; use strict; use warnings; use overload - '0+' => \&count, - 'bool' => sub { 1; }, + '0+' => "count", + 'bool' => "_bool", fallback => 1; +use Carp::Clan qw/^DBIx::Class/; use Data::Page; use Storable; -use Scalar::Util qw/weaken/; - +use DBIx::Class::ResultSetColumn; +use DBIx::Class::ResultSourceHandle; +use List::Util (); +use Scalar::Util (); use base qw/DBIx::Class/; -__PACKAGE__->load_components(qw/AccessorGroup/); -__PACKAGE__->mk_group_accessors('simple' => qw/result_source result_class/); + +__PACKAGE__->mk_group_accessors('simple' => qw/_result_class _source_handle/); =head1 NAME -DBIx::Class::ResultSet - Responsible for fetching and creating resultset. +DBIx::Class::ResultSet - Represents a query used for fetching a set of results. =head1 SYNOPSIS - my $rs = $schema->resultset('User')->search(registered => 1); - my @rows = $schema->resultset('CD')->search(year => 2005); + my $users_rs = $schema->resultset('User'); + my $registered_users_rs = $schema->resultset('User')->search({ registered => 1 }); + my @cds_in_2005 = $schema->resultset('CD')->search({ year => 2005 })->all(); =head1 DESCRIPTION -The resultset is also known as an iterator. It is responsible for handling -queries that may return an arbitrary number of rows, e.g. via L -or a C relationship. +A ResultSet is an object which stores a set of conditions representing +a query. It is the backbone of DBIx::Class (i.e. the really +important/useful bit). -In the examples below, the following table classes are used: +No SQL is executed on the database when a ResultSet is created, it +just stores all the conditions needed to create the query. - package MyApp::Schema::Artist; - use base qw/DBIx::Class/; - __PACKAGE__->load_components(qw/Core/); - __PACKAGE__->table('artist'); - __PACKAGE__->add_columns(qw/artistid name/); - __PACKAGE__->set_primary_key('artistid'); - __PACKAGE__->has_many(cds => 'MyApp::Schema::CD'); - 1; +A basic ResultSet representing the data of an entire table is returned +by calling C on a L and passing in a +L name. - package MyApp::Schema::CD; - use base qw/DBIx::Class/; - __PACKAGE__->load_components(qw/Core/); - __PACKAGE__->table('cd'); - __PACKAGE__->add_columns(qw/cdid artist title year/); - __PACKAGE__->set_primary_key('cdid'); - __PACKAGE__->belongs_to(artist => 'MyApp::Schema::Artist'); - 1; + my $users_rs = $schema->resultset('User'); + +A new ResultSet is returned from calling L on an existing +ResultSet. The new one will contain all the conditions of the +original, plus any new conditions added in the C call. + +A ResultSet is also an iterator. L is used to return all the +Ls the ResultSet represents. + +The query that the ResultSet represents is B executed against +the database when these methods are called: + +=over + +=item L + +=item L + +=item L + +=item L + +=item L + +=item L + +=back + +=head1 EXAMPLES + +=head2 Chaining resultsets + +Let's say you've got a query that needs to be run to return some data +to the user. But, you have an authorization system in place that +prevents certain users from seeing certain information. So, you want +to construct the basic query in one method, but add constraints to it in +another. + + sub get_data { + my $self = shift; + my $request = $self->get_request; # Get a request object somehow. + my $schema = $self->get_schema; # Get the DBIC schema object somehow. + + my $cd_rs = $schema->resultset('CD')->search({ + title => $request->param('title'), + year => $request->param('year'), + }); + + $self->apply_security_policy( $cd_rs ); + + return $cd_rs->all(); + } + + sub apply_security_policy { + my $self = shift; + my ($rs) = @_; + + return $rs->search({ + subversive => 0, + }); + } + +=head2 Multiple queries + +Since a resultset just defines a query, you can do all sorts of +things with it with the same object. + + # Don't hit the DB yet. + my $cd_rs = $schema->resultset('CD')->search({ + title => 'something', + year => 2009, + }); + + # Each of these hits the DB individually. + my $count = $cd_rs->count; + my $most_recent = $cd_rs->get_column('date_released')->max(); + my @records = $cd_rs->all; + +And it's not just limited to SELECT statements. + + $cd_rs->delete(); + +This is even cooler: + + $cd_rs->create({ artist => 'Fred' }); + +Which is the same as: + + $schema->resultset('CD')->create({ + title => 'something', + year => 2009, + artist => 'Fred' + }); + +See: L, L, L, L, L. + +=head1 OVERLOADING + +If a resultset is used in a numeric context it returns the L. +However, if it is used in a booleand context it is always true. So if +you want to check if a resultset has any results use C. +C will always be true. =head1 METHODS =head2 new -=head3 Arguments: ($source, \%$attrs) +=over 4 + +=item Arguments: $source, \%$attrs + +=item Return Value: $rs + +=back The resultset constructor. Takes a source object (usually a -L) and an attribute hash (see L -below). Does not perform any queries -- these are executed as needed by the -other methods. +L) and an attribute hash (see +L below). Does not perform any queries -- these are +executed as needed by the other methods. Generally you won't need to construct a resultset manually. You'll automatically get one from e.g. a L called in scalar context: my $rs = $schema->resultset('CD')->search({ title => '100th Window' }); +IMPORTANT: If called on an object, proxies to new_result instead so + + my $cd = $schema->resultset('CD')->new({ title => 'Spoon' }); + +will return a CD object, not a ResultSet. + =cut sub new { my $class = shift; return $class->new_result(@_) if ref $class; - - my ($source, $attrs) = @_; - weaken $source; - $attrs = Storable::dclone($attrs || {}); # { %{ $attrs || {} } }; - #use Data::Dumper; warn Dumper($attrs); - my $alias = ($attrs->{alias} ||= 'me'); - - $attrs->{columns} ||= delete $attrs->{cols} if $attrs->{cols}; - delete $attrs->{as} if $attrs->{columns}; - $attrs->{columns} ||= [ $source->columns ] unless $attrs->{select}; - $attrs->{select} = [ map { m/\./ ? $_ : "${alias}.$_" } @{delete $attrs->{columns}} ] - if $attrs->{columns}; - $attrs->{as} ||= [ map { m/^\Q$alias.\E(.+)$/ ? $1 : $_ } @{$attrs->{select}} ]; - if (my $include = delete $attrs->{include_columns}) { - push(@{$attrs->{select}}, @$include); - push(@{$attrs->{as}}, map { m/([^.]+)$/; $1; } @$include); - } - #use Data::Dumper; warn Dumper(@{$attrs}{qw/select as/}); - - $attrs->{from} ||= [ { $alias => $source->from } ]; - $attrs->{seen_join} ||= {}; - my %seen; - if (my $join = delete $attrs->{join}) { - foreach my $j (ref $join eq 'ARRAY' ? @$join : ($join)) { - if (ref $j eq 'HASH') { - $seen{$_} = 1 foreach keys %$j; - } else { - $seen{$j} = 1; - } - } - push(@{$attrs->{from}}, $source->resolve_join($join, $attrs->{alias}, $attrs->{seen_join})); - } - - $attrs->{group_by} ||= $attrs->{select} if delete $attrs->{distinct}; - $attrs->{order_by} = [ $attrs->{order_by} ] if $attrs->{order_by} and !ref($attrs->{order_by}); - $attrs->{order_by} ||= []; - my $collapse = $attrs->{collapse} || {}; - if (my $prefetch = delete $attrs->{prefetch}) { - my @pre_order; - foreach my $p (ref $prefetch eq 'ARRAY' ? @$prefetch : ($prefetch)) { - if ( ref $p eq 'HASH' ) { - foreach my $key (keys %$p) { - push(@{$attrs->{from}}, $source->resolve_join($p, $attrs->{alias})) - unless $seen{$key}; - } - } else { - push(@{$attrs->{from}}, $source->resolve_join($p, $attrs->{alias})) - unless $seen{$p}; - } - my @prefetch = $source->resolve_prefetch( - $p, $attrs->{alias}, {}, \@pre_order, $collapse); - push(@{$attrs->{select}}, map { $_->[0] } @prefetch); - push(@{$attrs->{as}}, map { $_->[1] } @prefetch); - } - push(@{$attrs->{order_by}}, @pre_order); - } - $attrs->{collapse} = $collapse; -# use Data::Dumper; warn Dumper($collapse) if keys %{$collapse}; + my ($source, $attrs) = @_; + $source = $source->handle + unless $source->isa('DBIx::Class::ResultSourceHandle'); + $attrs = { %{$attrs||{}} }; if ($attrs->{page}) { $attrs->{rows} ||= 10; - $attrs->{offset} ||= 0; - $attrs->{offset} += ($attrs->{rows} * ($attrs->{page} - 1)); } - bless { - result_source => $source, - result_class => $attrs->{result_class} || $source->result_class, + $attrs->{alias} ||= 'me'; + + # Creation of {} and bless separated to mitigate RH perl bug + # see https://bugzilla.redhat.com/show_bug.cgi?id=196836 + my $self = { + _source_handle => $source, cond => $attrs->{where}, - from => $attrs->{from}, - collapse => $collapse, count => undef, - page => delete $attrs->{page}, pager => undef, attrs => $attrs - }, $class; + }; + + bless $self, $class; + + $self->result_class( + $attrs->{result_class} || $source->resolve->result_class + ); + + return $self; } =head2 search - my @cds = $rs->search({ year => 2001 }); # "... WHERE year = 2001" - my $new_rs = $rs->search({ year => 2005 }); +=over 4 + +=item Arguments: $cond, \%attrs? + +=item Return Value: $resultset (scalar context), @row_objs (list context) + +=back + + my @cds = $cd_rs->search({ year => 2001 }); # "... WHERE year = 2001" + my $new_rs = $cd_rs->search({ year => 2005 }); + + my $new_rs = $cd_rs->search([ { year => 2005 }, { year => 2004 } ]); + # year = 2005 OR year = 2004 If you need to pass in additional attributes but no additional condition, -call it as C. +call it as C. # "SELECT name, artistid FROM $artist_table" my @all_artists = $schema->resultset('Artist')->search(undef, { columns => [qw/name artistid/], }); +For a list of attributes that can be passed to C, see +L. For more examples of using this function, see +L. For a complete +documentation for the first argument, see L. + +For more help on using joins with search, see L. + =cut sub search { my $self = shift; + my $rs = $self->search_rs( @_ ); + return (wantarray ? $rs->all : $rs); +} - my $rs; - if( @_ ) { - - my $attrs = { %{$self->{attrs}} }; - my $having = delete $attrs->{having}; - $attrs = { %$attrs, %{ pop(@_) } } if @_ > 1 and ref $_[$#_] eq 'HASH'; +=head2 search_rs + +=over 4 + +=item Arguments: $cond, \%attrs? + +=item Return Value: $resultset + +=back + +This method does the same exact thing as search() except it will +always return a resultset, even in list context. + +=cut + +sub search_rs { + my $self = shift; + + my $attrs = {}; + $attrs = pop(@_) if @_ > 1 and ref $_[$#_] eq 'HASH'; + my $our_attrs = { %{$self->{attrs}} }; + my $having = delete $our_attrs->{having}; + my $where = delete $our_attrs->{where}; + + my $rows; + + my %safe = (alias => 1, cache => 1); - my $where = (@_ - ? ((@_ == 1 || ref $_[0] eq "HASH") + unless ( + (@_ && defined($_[0])) # @_ == () or (undef) + || + (keys %$attrs # empty attrs or only 'safe' attrs + && List::Util::first { !$safe{$_} } keys %$attrs) + ) { + # no search, effectively just a clone + $rows = $self->get_cache; + } + + my $new_attrs = { %{$our_attrs}, %{$attrs} }; + + # merge new attrs into inherited + foreach my $key (qw/join prefetch +select +as/) { + next unless exists $attrs->{$key}; + $new_attrs->{$key} = $self->_merge_attr($our_attrs->{$key}, $attrs->{$key}); + } + + my $cond = (@_ + ? ( + (@_ == 1 || ref $_[0] eq "HASH") + ? ( + (ref $_[0] eq 'HASH') + ? ( + (keys %{ $_[0] } > 0) ? shift - : ((@_ % 2) - ? $self->throw_exception( - "Odd number of arguments to search") - : {@_})) - : undef()); - if (defined $where) { - $attrs->{where} = (defined $attrs->{where} - ? { '-and' => - [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } - $where, $attrs->{where} ] } - : $where); - } + : undef + ) + : shift + ) + : ( + (@_ % 2) + ? $self->throw_exception("Odd number of arguments to search") + : {@_} + ) + ) + : undef + ); - if (defined $having) { - $attrs->{having} = (defined $attrs->{having} - ? { '-and' => - [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } - $having, $attrs->{having} ] } - : $having); - } + if (defined $where) { + $new_attrs->{where} = ( + defined $new_attrs->{where} + ? { '-and' => [ + map { + ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ + } $where, $new_attrs->{where} + ] + } + : $where); + } - $rs = (ref $self)->new($self->result_source, $attrs); + if (defined $cond) { + $new_attrs->{where} = ( + defined $new_attrs->{where} + ? { '-and' => [ + map { + ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ + } $cond, $new_attrs->{where} + ] + } + : $cond); } - else { - $rs = $self; - $rs->reset; + + if (defined $having) { + $new_attrs->{having} = ( + defined $new_attrs->{having} + ? { '-and' => [ + map { + ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ + } $having, $new_attrs->{having} + ] + } + : $having); } - return (wantarray ? $rs->all : $rs); + + my $rs = (ref $self)->new($self->result_source, $new_attrs); + if ($rows) { + $rs->set_cache($rows); + } + return $rs; } =head2 search_literal - my @obj = $rs->search_literal($literal_where_cond, @bind); - my $new_rs = $rs->search_literal($literal_where_cond, @bind); +=over 4 + +=item Arguments: $sql_fragment, @bind_values + +=item Return Value: $resultset (scalar context), @row_objs (list context) + +=back + + my @cds = $cd_rs->search_literal('year = ? AND title = ?', qw/2001 Reload/); + my $newrs = $artist_rs->search_literal('name = ?', 'Metallica'); Pass a literal chunk of SQL to be added to the conditional part of the -resultset. +resultset query. + +CAVEAT: C is provided for Class::DBI compatibility and should +only be used in that context. There are known problems using C +in chained queries; it can result in bind values in the wrong order. See +L and +L for searching techniques that do not +require C. =cut @@ -226,70 +390,224 @@ sub search_literal { =head2 find -=head3 Arguments: (@colvalues) | (\%cols, \%attrs?) +=over 4 + +=item Arguments: @values | \%cols, \%attrs? + +=item Return Value: $row_object | undef -Finds a row based on its primary key or unique constraint. For example: +=back + +Finds a row based on its primary key or unique constraint. For example, to find +a row by its primary key: my $cd = $schema->resultset('CD')->find(5); -Also takes an optional C attribute, to search by a specific key or unique -constraint. For example: +You can also find a row by a specific unique constraint using the C +attribute. For example: + + my $cd = $schema->resultset('CD')->find('Massive Attack', 'Mezzanine', { + key => 'cd_artist_title' + }); + +Additionally, you can specify the columns explicitly by name: my $cd = $schema->resultset('CD')->find( { artist => 'Massive Attack', title => 'Mezzanine', }, - { key => 'artist_title' } + { key => 'cd_artist_title' } ); -See also L and L. +If the C is specified as C, it searches only on the primary key. + +If no C is specified, it searches on all unique constraints defined on the +source for which column data is provided, including the primary key. + +If your table does not have a primary key, you B provide a value for the +C attribute matching one of the unique constraints on the source. + +In addition to C, L recognizes and applies standard +L in the same way as L does. + +Note: If your query does not return only one row, a warning is generated: + + Query returned more than one row + +See also L and L. For information on how to +declare unique constraints, see +L. =cut sub find { - my ($self, @vals) = @_; - my $attrs = (@vals > 1 && ref $vals[$#vals] eq 'HASH' ? pop(@vals) : {}); + my $self = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); - my @cols = $self->result_source->primary_columns; - if (exists $attrs->{key}) { - my %uniq = $self->result_source->unique_constraints; - $self->throw_exception( "Unknown key $attrs->{key} on '" . $self->result_source->name . "'" ) - unless exists $uniq{$attrs->{key}}; - @cols = @{ $uniq{$attrs->{key}} }; + # Default to the primary key, but allow a specific key + my @cols = exists $attrs->{key} + ? $self->result_source->unique_constraint_columns($attrs->{key}) + : $self->result_source->primary_columns; + $self->throw_exception( + "Can't find unless a primary key is defined or unique constraint is specified" + ) unless @cols; + + # Parse out a hashref from input + my $input_query; + if (ref $_[0] eq 'HASH') { + $input_query = { %{$_[0]} }; } - #use Data::Dumper; warn Dumper($attrs, @vals, @cols); - $self->throw_exception( "Can't find unless a primary key or unique constraint is defined" ) - unless @cols; + elsif (@_ == @cols) { + $input_query = {}; + @{$input_query}{@cols} = @_; + } + else { + # Compatibility: Allow e.g. find(id => $value) + carp "Find by key => value deprecated; please use a hashref instead"; + $input_query = {@_}; + } + + my (%related, $info); + + KEY: foreach my $key (keys %$input_query) { + if (ref($input_query->{$key}) + && ($info = $self->result_source->relationship_info($key))) { + my $val = delete $input_query->{$key}; + next KEY if (ref($val) eq 'ARRAY'); # has_many for multi_create + my $rel_q = $self->result_source->resolve_condition( + $info->{cond}, $val, $key + ); + die "Can't handle OR join condition in find" if ref($rel_q) eq 'ARRAY'; + @related{keys %$rel_q} = values %$rel_q; + } + } + if (my @keys = keys %related) { + @{$input_query}{@keys} = values %related; + } + + # Build the final query: Default to the disjunction of the unique queries, + # but allow the input query in case the ResultSet defines the query or the + # user is abusing find + my $alias = exists $attrs->{alias} ? $attrs->{alias} : $self->{attrs}{alias}; my $query; - if (ref $vals[0] eq 'HASH') { - $query = { %{$vals[0]} }; - } elsif (@cols == @vals) { - $query = {}; - @{$query}{@cols} = @vals; - } else { - $query = {@vals}; + if (exists $attrs->{key}) { + my @unique_cols = $self->result_source->unique_constraint_columns($attrs->{key}); + my $unique_query = $self->_build_unique_query($input_query, \@unique_cols); + $query = $self->_add_alias($unique_query, $alias); } - foreach my $key (grep { ! m/\./ } keys %$query) { - $query->{"$self->{attrs}{alias}.$key"} = delete $query->{$key}; + else { + my @unique_queries = $self->_unique_queries($input_query, $attrs); + $query = @unique_queries + ? [ map { $self->_add_alias($_, $alias) } @unique_queries ] + : $self->_add_alias($input_query, $alias); } - #warn Dumper($query); - + + # Run the query if (keys %$attrs) { - my $rs = $self->search($query,$attrs); - return keys %{$rs->{collapse}} ? $rs->next : $rs->single; - } else { - return keys %{$self->{collapse}} ? $self->search($query)->next : $self->single($query); + my $rs = $self->search($query, $attrs); + if (keys %{$rs->_resolved_attrs->{collapse}}) { + my $row = $rs->next; + carp "Query returned more than one row" if $rs->next; + return $row; + } + else { + return $rs->single; + } + } + else { + if (keys %{$self->_resolved_attrs->{collapse}}) { + my $rs = $self->search($query); + my $row = $rs->next; + carp "Query returned more than one row" if $rs->next; + return $row; + } + else { + return $self->single($query); + } + } +} + +# _add_alias +# +# Add the specified alias to the specified query hash. A copy is made so the +# original query is not modified. + +sub _add_alias { + my ($self, $query, $alias) = @_; + + my %aliased = %$query; + foreach my $col (grep { ! m/\./ } keys %aliased) { + $aliased{"$alias.$col"} = delete $aliased{$col}; + } + + return \%aliased; +} + +# _unique_queries +# +# Build a list of queries which satisfy unique constraints. + +sub _unique_queries { + my ($self, $query, $attrs) = @_; + + my @constraint_names = exists $attrs->{key} + ? ($attrs->{key}) + : $self->result_source->unique_constraint_names; + + my $where = $self->_collapse_cond($self->{attrs}{where} || {}); + my $num_where = scalar keys %$where; + + my @unique_queries; + foreach my $name (@constraint_names) { + my @unique_cols = $self->result_source->unique_constraint_columns($name); + my $unique_query = $self->_build_unique_query($query, \@unique_cols); + + my $num_cols = scalar @unique_cols; + my $num_query = scalar keys %$unique_query; + + my $total = $num_query + $num_where; + if ($num_query && ($num_query == $num_cols || $total == $num_cols)) { + # The query is either unique on its own or is unique in combination with + # the existing where clause + push @unique_queries, $unique_query; + } } + + return @unique_queries; +} + +# _build_unique_query +# +# Constrain the specified query hash based on the specified column names. + +sub _build_unique_query { + my ($self, $query, $unique_cols) = @_; + + return { + map { $_ => $query->{$_} } + grep { exists $query->{$_} } + @$unique_cols + }; } =head2 search_related - $rs->search_related('relname', $cond?, $attrs?); +=over 4 + +=item Arguments: $rel, $cond, \%attrs? + +=item Return Value: $new_resultset + +=back -Search the specified relationship. Optionally specify a condition for matching -records. + $new_rs = $cd_rs->search_related('artist', { + name => 'Emo-R-Us', + }); + +Searches the specified relationship, optionally specifying a condition and +attributes for matching records. See L for more information. =cut @@ -297,33 +615,91 @@ sub search_related { return shift->related_resultset(shift)->search(@_); } +=head2 search_related_rs + +This method works exactly the same as search_related, except that +it guarantees a restultset, even in list context. + +=cut + +sub search_related_rs { + return shift->related_resultset(shift)->search_rs(@_); +} + =head2 cursor -Returns a storage-driven cursor to the given resultset. +=over 4 + +=item Arguments: none + +=item Return Value: $cursor + +=back + +Returns a storage-driven cursor to the given resultset. See +L for more information. =cut sub cursor { my ($self) = @_; - my $attrs = { %{$self->{attrs}} }; + + my $attrs = { %{$self->_resolved_attrs} }; return $self->{cursor} - ||= $self->result_source->storage->select($self->{from}, $attrs->{select}, + ||= $self->result_source->storage->select($attrs->{from}, $attrs->{select}, $attrs->{where},$attrs); } =head2 single -Inflates the first result without creating a cursor +=over 4 + +=item Arguments: $cond? + +=item Return Value: $row_object? + +=back + + my $cd = $schema->resultset('CD')->single({ year => 2001 }); + +Inflates the first result without creating a cursor if the resultset has +any records in it; if not returns nothing. Used by L as a lean version of +L. + +While this method can take an optional search condition (just like L) +being a fast-code-path it does not recognize search attributes. If you need to +add extra joins or similar, call L and then chain-call L on the +L returned. + +=over + +=item B + +As of 0.08100, this method enforces the assumption that the preceeding +query returns only one row. If more than one row is returned, you will receive +a warning: + + Query returned more than one row + +In this case, you should be using L or L instead, or if you really +know what you are doing, use the L attribute to explicitly limit the size +of the resultset. + +=back =cut sub single { my ($self, $where) = @_; - my $attrs = { %{$self->{attrs}} }; + if(@_ > 2) { + $self->throw_exception('single() only takes search conditions, no attributes. You want ->search( $cond, $attrs )->single()'); + } + + my $attrs = { %{$self->_resolved_attrs} }; if ($where) { if (defined $attrs->{where}) { $attrs->{where} = { - '-and' => + '-and' => [ map { ref $_ eq 'ARRAY' ? [ -or => $_ ] : $_ } $where, delete $attrs->{where} ] }; @@ -331,153 +707,344 @@ sub single { $attrs->{where} = $where; } } + +# XXX: Disabled since it doesn't infer uniqueness in all cases +# unless ($self->_is_unique_query($attrs->{where})) { +# carp "Query not guaranteed to return a single row" +# . "; please declare your unique constraints or use search instead"; +# } + my @data = $self->result_source->storage->select_single( - $self->{from}, $attrs->{select}, - $attrs->{where},$attrs); - return (@data ? $self->_construct_object(@data) : ()); + $attrs->{from}, $attrs->{select}, + $attrs->{where}, $attrs + ); + + return (@data ? ($self->_construct_object(@data))[0] : undef); } +# _is_unique_query +# +# Try to determine if the specified query is guaranteed to be unique, based on +# the declared unique constraints. -=head2 search_like +sub _is_unique_query { + my ($self, $query) = @_; -Perform a search, but use C instead of equality as the condition. Note -that this is simply a convenience method; you most likely want to use -L with specific operators. + my $collapsed = $self->_collapse_query($query); + my $alias = $self->{attrs}{alias}; -For more information, see L. + foreach my $name ($self->result_source->unique_constraint_names) { + my @unique_cols = map { + "$alias.$_" + } $self->result_source->unique_constraint_columns($name); -=cut + # Count the values for each unique column + my %seen = map { $_ => 0 } @unique_cols; -sub search_like { - my $class = shift; - my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); - my $query = ref $_[0] eq 'HASH' ? { %{shift()} }: {@_}; - $query->{$_} = { 'like' => $query->{$_} } for keys %$query; - return $class->search($query, { %$attrs }); + foreach my $key (keys %$collapsed) { + my $aliased = $key =~ /\./ ? $key : "$alias.$key"; + next unless exists $seen{$aliased}; # Additional constraints are okay + $seen{$aliased} = scalar keys %{ $collapsed->{$key} }; + } + + # If we get 0 or more than 1 value for a column, it's not necessarily unique + return 1 unless grep { $_ != 1 } values %seen; + } + + return 0; } -=head2 slice +# _collapse_query +# +# Recursively collapse the query, accumulating values for each column. -=head3 Arguments: ($first, $last) +sub _collapse_query { + my ($self, $query, $collapsed) = @_; -Returns a subset of elements from the resultset. + $collapsed ||= {}; -=cut + if (ref $query eq 'ARRAY') { + foreach my $subquery (@$query) { + next unless ref $subquery; # -or +# warn "ARRAY: " . Dumper $subquery; + $collapsed = $self->_collapse_query($subquery, $collapsed); + } + } + elsif (ref $query eq 'HASH') { + if (keys %$query and (keys %$query)[0] eq '-and') { + foreach my $subquery (@{$query->{-and}}) { +# warn "HASH: " . Dumper $subquery; + $collapsed = $self->_collapse_query($subquery, $collapsed); + } + } + else { +# warn "LEAF: " . Dumper $query; + foreach my $col (keys %$query) { + my $value = $query->{$col}; + $collapsed->{$col}{$value}++; + } + } + } -sub slice { - my ($self, $min, $max) = @_; - my $attrs = { %{ $self->{attrs} || {} } }; - $attrs->{offset} ||= 0; - $attrs->{offset} += $min; - $attrs->{rows} = ($max ? ($max - $min + 1) : 1); - my $slice = (ref $self)->new($self->result_source, $attrs); - return (wantarray ? $slice->all : $slice); + return $collapsed; } -=head2 next +=head2 get_column -Returns the next element in the resultset (C is there is none). +=over 4 -Can be used to efficiently iterate over records in the resultset: +=item Arguments: $cond? - my $rs = $schema->resultset('CD')->search; - while (my $cd = $rs->next) { - print $cd->title; - } +=item Return Value: $resultsetcolumn + +=back + + my $max_length = $rs->get_column('length')->max; + +Returns a L instance for a column of the ResultSet. =cut -sub next { - my ($self) = @_; - if (@{$self->{all_cache} || []}) { - $self->{all_cache_position} ||= 0; - return $self->{all_cache}->[$self->{all_cache_position}++]; +sub get_column { + my ($self, $column) = @_; + my $new = DBIx::Class::ResultSetColumn->new($self, $column); + return $new; +} + +=head2 search_like + +=over 4 + +=item Arguments: $cond, \%attrs? + +=item Return Value: $resultset (scalar context), @row_objs (list context) + +=back + + # WHERE title LIKE '%blue%' + $cd_rs = $rs->search_like({ title => '%blue%'}); + +Performs a search, but uses C instead of C<=> as the condition. Note +that this is simply a convenience method retained for ex Class::DBI users. +You most likely want to use L with specific operators. + +For more information, see L. + +=cut + +sub search_like { + my $class = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + my $query = ref $_[0] eq 'HASH' ? { %{shift()} }: {@_}; + $query->{$_} = { 'like' => $query->{$_} } for keys %$query; + return $class->search($query, { %$attrs }); +} + +=head2 slice + +=over 4 + +=item Arguments: $first, $last + +=item Return Value: $resultset (scalar context), @row_objs (list context) + +=back + +Returns a resultset or object list representing a subset of elements from the +resultset slice is called on. Indexes are from 0, i.e., to get the first +three records, call: + + my ($one, $two, $three) = $rs->slice(0, 2); + +=cut + +sub slice { + my ($self, $min, $max) = @_; + my $attrs = {}; # = { %{ $self->{attrs} || {} } }; + $attrs->{offset} = $self->{attrs}{offset} || 0; + $attrs->{offset} += $min; + $attrs->{rows} = ($max ? ($max - $min + 1) : 1); + return $self->search(undef(), $attrs); + #my $slice = (ref $self)->new($self->result_source, $attrs); + #return (wantarray ? $slice->all : $slice); +} + +=head2 next + +=over 4 + +=item Arguments: none + +=item Return Value: $result? + +=back + +Returns the next element in the resultset (C is there is none). + +Can be used to efficiently iterate over records in the resultset: + + my $rs = $schema->resultset('CD')->search; + while (my $cd = $rs->next) { + print $cd->title; + } + +Note that you need to store the resultset object, and call C on it. +Calling C<< resultset('Table')->next >> repeatedly will always return the +first record from the resultset. + +=cut + +sub next { + my ($self) = @_; + if (my $cache = $self->get_cache) { + $self->{all_cache_position} ||= 0; + return $cache->[$self->{all_cache_position}++]; } if ($self->{attrs}{cache}) { $self->{all_cache_position} = 1; return ($self->all)[0]; } - my @row = (exists $self->{stashed_row} - ? @{delete $self->{stashed_row}} - : $self->cursor->next); -# warn Dumper(\@row); use Data::Dumper; - return unless (@row); - return $self->_construct_object(@row); + if ($self->{stashed_objects}) { + my $obj = shift(@{$self->{stashed_objects}}); + delete $self->{stashed_objects} unless @{$self->{stashed_objects}}; + return $obj; + } + my @row = ( + exists $self->{stashed_row} + ? @{delete $self->{stashed_row}} + : $self->cursor->next + ); + return undef unless (@row); + my ($row, @more) = $self->_construct_object(@row); + $self->{stashed_objects} = \@more if @more; + return $row; } sub _construct_object { my ($self, @row) = @_; - my @as = @{ $self->{attrs}{as} }; - - my $info = $self->_collapse_result(\@as, \@row); - - my $new = $self->result_class->inflate_result($self->result_source, @$info); - - $new = $self->{attrs}{record_filter}->($new) - if exists $self->{attrs}{record_filter}; - return $new; + my $info = $self->_collapse_result($self->{_attrs}{as}, \@row); + my @new = $self->result_class->inflate_result($self->result_source, @$info); + @new = $self->{_attrs}{record_filter}->(@new) + if exists $self->{_attrs}{record_filter}; + return @new; } sub _collapse_result { - my ($self, $as, $row, $prefix) = @_; - - my %const; + my ($self, $as_proto, $row) = @_; my @copy = @$row; - foreach my $this_as (@$as) { - my $val = shift @copy; - if (defined $prefix) { - if ($this_as =~ m/^\Q${prefix}.\E(.+)$/) { - my $remain = $1; - $remain =~ /^(?:(.*)\.)?([^.]+)$/; - $const{$1||''}{$2} = $val; - } - } else { - $this_as =~ /^(?:(.*)\.)?([^.]+)$/; - $const{$1||''}{$2} = $val; - } - } - my $info = [ {}, {} ]; - foreach my $key (keys %const) { - if (length $key) { - my $target = $info; - my @parts = split(/\./, $key); - foreach my $p (@parts) { - $target = $target->[1]->{$p} ||= []; + # 'foo' => [ undef, 'foo' ] + # 'foo.bar' => [ 'foo', 'bar' ] + # 'foo.bar.baz' => [ 'foo.bar', 'baz' ] + + my @construct_as = map { [ (/^(?:(.*)\.)?([^.]+)$/) ] } @$as_proto; + + my %collapse = %{$self->{_attrs}{collapse}||{}}; + + my @pri_index; + + # if we're doing collapsing (has_many prefetch) we need to grab records + # until the PK changes, so fill @pri_index. if not, we leave it empty so + # we know we don't have to bother. + + # the reason for not using the collapse stuff directly is because if you + # had for e.g. two artists in a row with no cds, the collapse info for + # both would be NULL (undef) so you'd lose the second artist + + # store just the index so we can check the array positions from the row + # without having to contruct the full hash + + if (keys %collapse) { + my %pri = map { ($_ => 1) } $self->result_source->primary_columns; + foreach my $i (0 .. $#construct_as) { + next if defined($construct_as[$i][0]); # only self table + if (delete $pri{$construct_as[$i][1]}) { + push(@pri_index, $i); } - $target->[0] = $const{$key}; - } else { - $info->[0] = $const{$key}; + last unless keys %pri; # short circuit (Johnny Five Is Alive!) } } - my @collapse = (defined($prefix) - ? (map { (m/^\Q${prefix}.\E(.+)$/ ? ($1) : ()); } - keys %{$self->{collapse}}) - : keys %{$self->{collapse}}); - if (@collapse) { - my ($c) = sort { length $a <=> length $b } @collapse; - my $target = $info; - foreach my $p (split(/\./, $c)) { - $target = $target->[1]->{$p} ||= []; + # no need to do an if, it'll be empty if @pri_index is empty anyway + + my %pri_vals = map { ($_ => $copy[$_]) } @pri_index; + + my @const_rows; + + do { # no need to check anything at the front, we always want the first row + + my %const; + + foreach my $this_as (@construct_as) { + $const{$this_as->[0]||''}{$this_as->[1]} = shift(@copy); } - my $c_prefix = (defined($prefix) ? "${prefix}.${c}" : $c); - my @co_key = @{$self->{collapse}{$c_prefix}}; - my %co_check = map { ($_, $target->[0]->{$_}); } @co_key; - my $tree = $self->_collapse_result($as, $row, $c_prefix); - my (@final, @raw); - while ( !(grep { - !defined($tree->[0]->{$_}) - || $co_check{$_} ne $tree->[0]->{$_} - } @co_key) ) { - push(@final, $tree); - last unless (@raw = $self->cursor->next); - $row = $self->{stashed_row} = \@raw; - $tree = $self->_collapse_result($as, $row, $c_prefix); - #warn Data::Dumper::Dumper($tree, $row); + + push(@const_rows, \%const); + + } until ( # no pri_index => no collapse => drop straight out + !@pri_index + or + do { # get another row, stash it, drop out if different PK + + @copy = $self->cursor->next; + $self->{stashed_row} = \@copy; + + # last thing in do block, counts as true if anything doesn't match + + # check xor defined first for NULL vs. NOT NULL then if one is + # defined the other must be so check string equality + + grep { + (defined $pri_vals{$_} ^ defined $copy[$_]) + || (defined $pri_vals{$_} && ($pri_vals{$_} ne $copy[$_])) + } @pri_index; + } + ); + + my $alias = $self->{attrs}{alias}; + my $info = []; + + my %collapse_pos; + + my @const_keys; + + foreach my $const (@const_rows) { + scalar @const_keys or do { + @const_keys = sort { length($a) <=> length($b) } keys %$const; + }; + foreach my $key (@const_keys) { + if (length $key) { + my $target = $info; + my @parts = split(/\./, $key); + my $cur = ''; + my $data = $const->{$key}; + foreach my $p (@parts) { + $target = $target->[1]->{$p} ||= []; + $cur .= ".${p}"; + if ($cur eq ".${key}" && (my @ckey = @{$collapse{$cur}||[]})) { + # collapsing at this point and on final part + my $pos = $collapse_pos{$cur}; + CK: foreach my $ck (@ckey) { + if (!defined $pos->{$ck} || $pos->{$ck} ne $data->{$ck}) { + $collapse_pos{$cur} = $data; + delete @collapse_pos{ # clear all positioning for sub-entries + grep { m/^\Q${cur}.\E/ } keys %collapse_pos + }; + push(@$target, []); + last CK; + } + } + } + if (exists $collapse{$cur}) { + $target = $target->[-1]; + } + } + $target->[0] = $data; + } else { + $info->[0] = $const->{$key}; + } } - @$target = @final; } return $info; @@ -485,18 +1052,57 @@ sub _collapse_result { =head2 result_source -Returns a reference to the result source for this recordset. +=over 4 + +=item Arguments: $result_source? + +=item Return Value: $result_source + +=back + +An accessor for the primary ResultSource object from which this ResultSet +is derived. + +=head2 result_class + +=over 4 + +=item Arguments: $result_class? + +=item Return Value: $result_class + +=back + +An accessor for the class to use when creating row objects. Defaults to +C<< result_source->result_class >> - which in most cases is the name of the +L<"table"|DBIx::Class::Manual::Glossary/"ResultSource"> class. =cut +sub result_class { + my ($self, $result_class) = @_; + if ($result_class) { + $self->ensure_class_loaded($result_class); + $self->_result_class($result_class); + } + $self->_result_class; +} =head2 count +=over 4 + +=item Arguments: $cond, \%attrs?? + +=item Return Value: $count + +=back + Performs an SQL C with the same query as the resultset was built with to find the number of elements. If passed arguments, does a search on the resultset and counts the results of that. -Note: When using C with C, L emulates C +Note: When using C with C, L emulates C using C. Some databases (notably SQLite) do not support C with multiple columns. If you are using such a database, you should only use columns from the main table in your C @@ -507,37 +1113,40 @@ clause. sub count { my $self = shift; return $self->search(@_)->count if @_ and defined $_[0]; - return scalar @{ $self->get_cache } if @{ $self->get_cache }; - + return scalar @{ $self->get_cache } if $self->get_cache; my $count = $self->_count; return 0 unless $count; - $count -= $self->{attrs}{offset} if $self->{attrs}{offset}; + # need to take offset from resolved attrs + + $count -= $self->{_attrs}{offset} if $self->{_attrs}{offset}; $count = $self->{attrs}{rows} if $self->{attrs}{rows} and $self->{attrs}{rows} < $count; + $count = 0 if ($count < 0); return $count; } sub _count { # Separated out so pager can get the full count my $self = shift; my $select = { count => '*' }; - my $attrs = { %{ $self->{attrs} } }; + + my $attrs = { %{$self->_resolved_attrs} }; if (my $group_by = delete $attrs->{group_by}) { delete $attrs->{having}; my @distinct = (ref $group_by ? @$group_by : ($group_by)); # todo: try CONCAT for multi-column pk my @pk = $self->result_source->primary_columns; if (@pk == 1) { + my $alias = $attrs->{alias}; foreach my $column (@distinct) { - if ($column =~ qr/^(?:\Q$attrs->{alias}.\E)?$pk[0]$/) { + if ($column =~ qr/^(?:\Q${alias}.\E)?$pk[0]$/) { @distinct = ($column); last; } - } + } } $select = { count => { distinct => \@distinct } }; - #use Data::Dumper; die Dumper $select; } $attrs->{select} = $select; @@ -545,14 +1154,28 @@ sub _count { # Separated out so pager can get the full count # offset, order by and page are not needed to count. record_filter is cdbi delete $attrs->{$_} for qw/rows offset order_by page pager record_filter/; - - my ($count) = (ref $self)->new($self->result_source, $attrs)->cursor->next; + + my $tmp_rs = (ref $self)->new($self->result_source, $attrs); + my ($count) = $tmp_rs->cursor->next; return $count; } +sub _bool { + return 1; +} + =head2 count_literal -Calls L with the passed arguments, then L. +=over 4 + +=item Arguments: $sql_fragment, @bind_values + +=item Return Value: $count + +=back + +Counts the results in a literal query. Equivalent to calling L +with the passed arguments, then L. =cut @@ -560,23 +1183,36 @@ sub count_literal { shift->search_literal(@_)->count; } =head2 all -Returns all elements in the resultset. Called implictly if the resultset +=over 4 + +=item Arguments: none + +=item Return Value: @objects + +=back + +Returns all elements in the resultset. Called implicitly if the resultset is returned in list context. =cut sub all { - my ($self) = @_; - return @{ $self->get_cache } if @{ $self->get_cache }; + my $self = shift; + if(@_) { + $self->throw_exception("all() doesn't take any arguments, you probably wanted ->search(...)->all()"); + } + + return @{ $self->get_cache } if $self->get_cache; my @obj; - if (keys %{$self->{collapse}}) { + # TODO: don't call resolve here + if (keys %{$self->_resolved_attrs->{collapse}}) { +# if ($self->{attrs}{prefetch}) { # Using $self->cursor->all is really just an optimisation. # If we're collapsing has_many prefetches it probably makes # very little difference, and this is cleaner than hacking # _construct_object to survive the approach - $self->cursor->reset; my @row = $self->cursor->next; while (@row) { push(@obj, $self->_construct_object(@row)); @@ -594,12 +1230,21 @@ sub all { =head2 reset +=over 4 + +=item Arguments: none + +=item Return Value: $self + +=back + Resets the resultset's cursor, so you can iterate through the elements again. =cut sub reset { my ($self) = @_; + delete $self->{_attrs} if exists $self->{_attrs}; $self->{all_cache_position} = 0; $self->cursor->reset; return $self; @@ -607,7 +1252,16 @@ sub reset { =head2 first -Resets the resultset and returns the first element. +=over 4 + +=item Arguments: none + +=item Return Value: $object? + +=back + +Resets the resultset and returns an object for the first result (if the +resultset returns anything). =cut @@ -615,33 +1269,121 @@ sub first { return $_[0]->reset->next; } +# _cond_for_update_delete +# +# update/delete require the condition to be modified to handle +# the differing SQL syntax available. This transforms the $self->{cond} +# appropriately, returning the new condition. + +sub _cond_for_update_delete { + my ($self, $full_cond) = @_; + my $cond = {}; + + $full_cond ||= $self->{cond}; + # No-op. No condition, we're updating/deleting everything + return $cond unless ref $full_cond; + + if (ref $full_cond eq 'ARRAY') { + $cond = [ + map { + my %hash; + foreach my $key (keys %{$_}) { + $key =~ /([^.]+)$/; + $hash{$1} = $_->{$key}; + } + \%hash; + } @{$full_cond} + ]; + } + elsif (ref $full_cond eq 'HASH') { + if ((keys %{$full_cond})[0] eq '-and') { + $cond->{-and} = []; + + my @cond = @{$full_cond->{-and}}; + for (my $i = 0; $i < @cond; $i++) { + my $entry = $cond[$i]; + + my $hash; + if (ref $entry eq 'HASH') { + $hash = $self->_cond_for_update_delete($entry); + } + else { + $entry =~ /([^.]+)$/; + $hash->{$1} = $cond[++$i]; + } + + push @{$cond->{-and}}, $hash; + } + } + else { + foreach my $key (keys %{$full_cond}) { + $key =~ /([^.]+)$/; + $cond->{$1} = $full_cond->{$key}; + } + } + } + else { + $self->throw_exception( + "Can't update/delete on resultset with condition unless hash or array" + ); + } + + return $cond; +} + + =head2 update -=head3 Arguments: (\%values) +=over 4 -Sets the specified columns in the resultset to the supplied values. +=item Arguments: \%values + +=item Return Value: $storage_rv + +=back + +Sets the specified columns in the resultset to the supplied values in a +single query. Return value will be true if the update succeeded or false +if no records were updated; exact type of success value is storage-dependent. =cut sub update { my ($self, $values) = @_; - $self->throw_exception("Values for update must be a hash") unless ref $values eq 'HASH'; + $self->throw_exception("Values for update must be a hash") + unless ref $values eq 'HASH'; + + carp( 'WARNING! Currently $rs->update() does not generate proper SQL' + . ' on joined resultsets, and may affect rows well outside of the' + . ' contents of $rs. Use at your own risk' ) + if ( $self->{attrs}{seen_join} ); + + my $cond = $self->_cond_for_update_delete; + return $self->result_source->storage->update( - $self->result_source->from, $values, $self->{cond}); + $self->result_source, $values, $cond + ); } =head2 update_all -=head3 Arguments: (\%values) +=over 4 + +=item Arguments: \%values + +=item Return Value: 1 -Fetches all objects and updates them one at a time. Note that C -will run cascade triggers while L will not. +=back + +Fetches all objects and updates them one at a time. Note that C +will run DBIC cascade triggers, while L will not. =cut sub update_all { my ($self, $values) = @_; - $self->throw_exception("Values for update must be a hash") unless ref $values eq 'HASH'; + $self->throw_exception("Values for update must be a hash") + unless ref $values eq 'HASH'; foreach my $obj ($self->all) { $obj->set_columns($values)->update; } @@ -650,68 +1392,248 @@ sub update_all { =head2 delete -Deletes the contents of the resultset from its result source. +=over 4 + +=item Arguments: none + +=item Return Value: 1 + +=back + +Deletes the contents of the resultset from its result source. Note that this +will not run DBIC cascade triggers. See L if you need triggers +to run. See also L. + +delete may not generate correct SQL for a query with joins or a resultset +chained from a related resultset. In this case it will generate a warning:- + + WARNING! Currently $rs->delete() does not generate proper SQL on + joined resultsets, and may delete rows well outside of the contents + of $rs. Use at your own risk + +In these cases you may find that delete_all is more appropriate, or you +need to respecify your query in a way that can be expressed without a join. =cut sub delete { my ($self) = @_; - my $del = {}; + $self->throw_exception("Delete should not be passed any arguments") + if $_[1]; + carp( 'WARNING! Currently $rs->delete() does not generate proper SQL' + . ' on joined resultsets, and may delete rows well outside of the' + . ' contents of $rs. Use at your own risk' ) + if ( $self->{attrs}{seen_join} ); + my $cond = $self->_cond_for_update_delete; + + $self->result_source->storage->delete($self->result_source, $cond); + return 1; +} + +=head2 delete_all + +=over 4 + +=item Arguments: none + +=item Return Value: 1 + +=back + +Fetches all objects and deletes them one at a time. Note that C +will run DBIC cascade triggers, while L will not. + +=cut + +sub delete_all { + my ($self) = @_; + $_->delete for $self->all; + return 1; +} - if (!ref($self->{cond})) { +=head2 populate - # No-op. No condition, we're deleting everything +=over 4 - } elsif (ref $self->{cond} eq 'ARRAY') { +=item Arguments: \@data; - $del = [ map { my %hash; - foreach my $key (keys %{$_}) { - $key =~ /([^.]+)$/; - $hash{$1} = $_->{$key}; - }; \%hash; } @{$self->{cond}} ]; +=back - } elsif (ref $self->{cond} eq 'HASH') { +Accepts either an arrayref of hashrefs or alternatively an arrayref of arrayrefs. +For the arrayref of hashrefs style each hashref should be a structure suitable +forsubmitting to a $resultset->create(...) method. - if ((keys %{$self->{cond}})[0] eq '-and') { +In void context, C in L is used +to insert the data, as this is a faster method. - $del->{-and} = [ map { my %hash; - foreach my $key (keys %{$_}) { - $key =~ /([^.]+)$/; - $hash{$1} = $_->{$key}; - }; \%hash; } @{$self->{cond}{-and}} ]; +Otherwise, each set of data is inserted into the database using +L, and a arrayref of the resulting row +objects is returned. - } else { +Example: Assuming an Artist Class that has many CDs Classes relating: - foreach my $key (keys %{$self->{cond}}) { - $key =~ /([^.]+)$/; - $del->{$1} = $self->{cond}{$key}; - } + my $Artist_rs = $schema->resultset("Artist"); + + ## Void Context Example + $Artist_rs->populate([ + { artistid => 4, name => 'Manufactured Crap', cds => [ + { title => 'My First CD', year => 2006 }, + { title => 'Yet More Tweeny-Pop crap', year => 2007 }, + ], + }, + { artistid => 5, name => 'Angsty-Whiny Girl', cds => [ + { title => 'My parents sold me to a record company' ,year => 2005 }, + { title => 'Why Am I So Ugly?', year => 2006 }, + { title => 'I Got Surgery and am now Popular', year => 2007 } + ], + }, + ]); + + ## Array Context Example + my ($ArtistOne, $ArtistTwo, $ArtistThree) = $Artist_rs->populate([ + { name => "Artist One"}, + { name => "Artist Two"}, + { name => "Artist Three", cds=> [ + { title => "First CD", year => 2007}, + { title => "Second CD", year => 2008}, + ]} + ]); + + print $ArtistOne->name; ## response is 'Artist One' + print $ArtistThree->cds->count ## reponse is '2' + +For the arrayref of arrayrefs style, the first element should be a list of the +fieldsnames to which the remaining elements are rows being inserted. For +example: + + $Arstist_rs->populate([ + [qw/artistid name/], + [100, 'A Formally Unknown Singer'], + [101, 'A singer that jumped the shark two albums ago'], + [102, 'An actually cool singer.'], + ]); + +Please note an important effect on your data when choosing between void and +wantarray context. Since void context goes straight to C in +L this will skip any component that is overriding +c. So if you are using something like L to +create primary keys for you, you will find that your PKs are empty. In this +case you will have to use the wantarray context in order to create those +values. + +=cut + +sub populate { + my $self = shift @_; + my $data = ref $_[0][0] eq 'HASH' + ? $_[0] : ref $_[0][0] eq 'ARRAY' ? $self->_normalize_populate_args($_[0]) : + $self->throw_exception('Populate expects an arrayref of hashes or arrayref of arrayrefs'); + + if(defined wantarray) { + my @created; + foreach my $item (@$data) { + push(@created, $self->create($item)); } + return @created; } else { - $self->throw_exception( - "Can't delete on resultset with condition unless hash or array"); - } + my ($first, @rest) = @$data; - $self->result_source->storage->delete($self->result_source->from, $del); - return 1; + my @names = grep {!ref $first->{$_}} keys %$first; + my @rels = grep { $self->result_source->has_relationship($_) } keys %$first; + my @pks = $self->result_source->primary_columns; + + ## do the belongs_to relationships + foreach my $index (0..$#$data) { + if( grep { !defined $data->[$index]->{$_} } @pks ) { + my @ret = $self->populate($data); + return; + } + + foreach my $rel (@rels) { + next unless $data->[$index]->{$rel} && ref $data->[$index]->{$rel} eq "HASH"; + my $result = $self->related_resultset($rel)->create($data->[$index]->{$rel}); + my ($reverse) = keys %{$self->result_source->reverse_relationship_info($rel)}; + my $related = $result->result_source->resolve_condition( + $result->result_source->relationship_info($reverse)->{cond}, + $self, + $result, + ); + + delete $data->[$index]->{$rel}; + $data->[$index] = {%{$data->[$index]}, %$related}; + + push @names, keys %$related if $index == 0; + } + } + + ## do bulk insert on current row + my @values = map { [ @$_{@names} ] } @$data; + + $self->result_source->storage->insert_bulk( + $self->result_source, + \@names, + \@values, + ); + + ## do the has_many relationships + foreach my $item (@$data) { + + foreach my $rel (@rels) { + next unless $item->{$rel} && ref $item->{$rel} eq "ARRAY"; + + my $parent = $self->find(map {{$_=>$item->{$_}} } @pks) + || $self->throw_exception('Cannot find the relating object.'); + + my $child = $parent->$rel; + + my $related = $child->result_source->resolve_condition( + $parent->result_source->relationship_info($rel)->{cond}, + $child, + $parent, + ); + + my @rows_to_add = ref $item->{$rel} eq 'ARRAY' ? @{$item->{$rel}} : ($item->{$rel}); + my @populate = map { {%$_, %$related} } @rows_to_add; + + $child->populate( \@populate ); + } + } + } } -=head2 delete_all +=head2 _normalize_populate_args ($args) -Fetches all objects and deletes them one at a time. Note that C -will run cascade triggers while L will not. +Private method used by L to normalize its incoming arguments. Factored +out in case you want to subclass and accept new argument structures to the +L method. =cut -sub delete_all { - my ($self) = @_; - $_->delete for $self->all; - return 1; +sub _normalize_populate_args { + my ($self, $data) = @_; + my @names = @{shift(@$data)}; + my @results_to_create; + foreach my $datum (@$data) { + my %result_to_create; + foreach my $index (0..$#names) { + $result_to_create{$names[$index]} = $$datum[$index]; + } + push @results_to_create, \%result_to_create; + } + return \@results_to_create; } =head2 pager -Returns a L object for the current resultset. Only makes +=over 4 + +=item Arguments: none + +=item Return Value: $pager + +=back + +Return Value a L object for the current resultset. Only makes sense for queries with a C attribute. =cut @@ -719,32 +1641,50 @@ sense for queries with a C attribute. sub pager { my ($self) = @_; my $attrs = $self->{attrs}; - $self->throw_exception("Can't create pager for non-paged rs") unless $self->{page}; + $self->throw_exception("Can't create pager for non-paged rs") + unless $self->{attrs}{page}; $attrs->{rows} ||= 10; return $self->{pager} ||= Data::Page->new( - $self->_count, $attrs->{rows}, $self->{page}); + $self->_count, $attrs->{rows}, $self->{attrs}{page}); } =head2 page -=head3 Arguments: ($page_num) +=over 4 + +=item Arguments: $page_number -Returns a new resultset for the specified page. +=item Return Value: $rs + +=back + +Returns a resultset for the $page_number page of the resultset on which page +is called, where each page contains a number of rows equal to the 'rows' +attribute set on the resultset (10 by default). =cut sub page { my ($self, $page) = @_; - my $attrs = { %{$self->{attrs}} }; - $attrs->{page} = $page; - return (ref $self)->new($self->result_source, $attrs); + return (ref $self)->new($self->result_source, { %{$self->{attrs}}, page => $page }); } =head2 new_result -=head3 Arguments: (\%vals) +=over 4 + +=item Arguments: \%vals + +=item Return Value: $rowobject -Creates a result in the resultset's result class. +=back + +Creates a new row object in the resultset's result class and returns +it. The row is not inserted into the database at this point, call +L to do that. Calling L +will tell you whether the row object has been inserted or not. + +Passes the hashref of input on to L. =cut @@ -752,259 +1692,942 @@ sub new_result { my ($self, $values) = @_; $self->throw_exception( "new_result needs a hash" ) unless (ref $values eq 'HASH'); - $self->throw_exception( "Can't abstract implicit construct, condition not a hash" ) - if ($self->{cond} && !(ref $self->{cond} eq 'HASH')); - my %new = %$values; + + my %new; my $alias = $self->{attrs}{alias}; - foreach my $key (keys %{$self->{cond}||{}}) { - $new{$1} = $self->{cond}{$key} if ($key =~ m/^(?:\Q${alias}.\E)?([^.]+)$/); + + if ( + defined $self->{cond} + && $self->{cond} eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION + ) { + %new = %{ $self->{attrs}{related_objects} || {} }; # nothing might have been inserted yet + $new{-from_resultset} = [ keys %new ] if keys %new; + } else { + $self->throw_exception( + "Can't abstract implicit construct, condition not a hash" + ) if ($self->{cond} && !(ref $self->{cond} eq 'HASH')); + + my $collapsed_cond = ( + $self->{cond} + ? $self->_collapse_cond($self->{cond}) + : {} + ); + + # precendence must be given to passed values over values inherited from + # the cond, so the order here is important. + my %implied = %{$self->_remove_alias($collapsed_cond, $alias)}; + while( my($col,$value) = each %implied ){ + if(ref($value) eq 'HASH' && keys(%$value) && (keys %$value)[0] eq '='){ + $new{$col} = $value->{'='}; + next; + } + $new{$col} = $value if $self->_is_deterministic_value($value); + } + } + + %new = ( + %new, + %{ $self->_remove_alias($values, $alias) }, + -source_handle => $self->_source_handle, + -result_source => $self->result_source, # DO NOT REMOVE THIS, REQUIRED + ); + + return $self->result_class->new(\%new); +} + +# _is_deterministic_value +# +# Make an effor to strip non-deterministic values from the condition, +# to make sure new_result chokes less + +sub _is_deterministic_value { + my $self = shift; + my $value = shift; + my $ref_type = ref $value; + return 1 if $ref_type eq '' || $ref_type eq 'SCALAR'; + return 1 if Scalar::Util::blessed($value); + return 0; +} + +# _collapse_cond +# +# Recursively collapse the condition. + +sub _collapse_cond { + my ($self, $cond, $collapsed) = @_; + + $collapsed ||= {}; + + if (ref $cond eq 'ARRAY') { + foreach my $subcond (@$cond) { + next unless ref $subcond; # -or +# warn "ARRAY: " . Dumper $subcond; + $collapsed = $self->_collapse_cond($subcond, $collapsed); + } } - my $obj = $self->result_class->new(\%new); - $obj->result_source($self->result_source) if $obj->can('result_source'); - return $obj; + elsif (ref $cond eq 'HASH') { + if (keys %$cond and (keys %$cond)[0] eq '-and') { + foreach my $subcond (@{$cond->{-and}}) { +# warn "HASH: " . Dumper $subcond; + $collapsed = $self->_collapse_cond($subcond, $collapsed); + } + } + else { +# warn "LEAF: " . Dumper $cond; + foreach my $col (keys %$cond) { + my $value = $cond->{$col}; + $collapsed->{$col} = $value; + } + } + } + + return $collapsed; +} + +# _remove_alias +# +# Remove the specified alias from the specified query hash. A copy is made so +# the original query is not modified. + +sub _remove_alias { + my ($self, $query, $alias) = @_; + + my %orig = %{ $query || {} }; + my %unaliased; + + foreach my $key (keys %orig) { + if ($key !~ /\./) { + $unaliased{$key} = $orig{$key}; + next; + } + $unaliased{$1} = $orig{$key} + if $key =~ m/^(?:\Q$alias\E\.)?([^.]+)$/; + } + + return \%unaliased; +} + +=head2 as_query + +=over 4 + +=item Arguments: none + +=item Return Value: \[ $sql, @bind ] + +=back + +Returns the SQL query and bind vars associated with the invocant. + +This is generally used as the RHS for a subquery. + +=cut + +sub as_query { return shift->cursor->as_query(@_) } + +=head2 find_or_new + +=over 4 + +=item Arguments: \%vals, \%attrs? + +=item Return Value: $rowobject + +=back + + my $artist = $schema->resultset('Artist')->find_or_new( + { artist => 'fred' }, { key => 'artists' }); + + $cd->cd_to_producer->find_or_new({ producer => $producer }, + { key => 'primary }); + +Find an existing record from this resultset, based on its primary +key, or a unique constraint. If none exists, instantiate a new result +object and return it. The object will not be saved into your storage +until you call L on it. + +You most likely want this method when looking for existing rows using +a unique constraint that is not the primary key, or looking for +related rows. + +If you want objects to be saved immediately, use L instead. + +B: C is probably not what you want when creating a +new row in a table that uses primary keys supplied by the +database. Passing in a primary key column with a value of I +will cause L to attempt to search for a row with a value of +I. + +=cut + +sub find_or_new { + my $self = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; + my $exists = $self->find($hash, $attrs); + return defined $exists ? $exists : $self->new_result($hash); } -=head2 create +=head2 create + +=over 4 + +=item Arguments: \%vals + +=item Return Value: a L $object + +=back + +Attempt to create a single new row or a row with multiple related rows +in the table represented by the resultset (and related tables). This +will not check for duplicate rows before inserting, use +L to do that. + +To create one row for this resultset, pass a hashref of key/value +pairs representing the columns of the table and the values you wish to +store. If the appropriate relationships are set up, foreign key fields +can also be passed an object representing the foreign row, and the +value will be set to its primary key. + +To create related objects, pass a hashref for the value if the related +item is a foreign key relationship (L), +and use the name of the relationship as the key. (NOT the name of the field, +necessarily). For C and C relationships, pass an arrayref +of hashrefs containing the data for each of the rows to create in the foreign +tables, again using the relationship name as the key. + +Instead of hashrefs of plain related data (key/value pairs), you may +also pass new or inserted objects. New objects (not inserted yet, see +L), will be inserted into their appropriate tables. + +Effectively a shortcut for C<< ->new_result(\%vals)->insert >>. + +Example of creating a new row. + + $person_rs->create({ + name=>"Some Person", + email=>"somebody@someplace.com" + }); + +Example of creating a new row and also creating rows in a related C +or C resultset. Note Arrayref. + + $artist_rs->create( + { artistid => 4, name => 'Manufactured Crap', cds => [ + { title => 'My First CD', year => 2006 }, + { title => 'Yet More Tweeny-Pop crap', year => 2007 }, + ], + }, + ); + +Example of creating a new row and also creating a row in a related +Cresultset. Note Hashref. + + $cd_rs->create({ + title=>"Music for Silly Walks", + year=>2000, + artist => { + name=>"Silly Musician", + } + }); + +=cut + +sub create { + my ($self, $attrs) = @_; + $self->throw_exception( "create needs a hashref" ) + unless ref $attrs eq 'HASH'; + return $self->new_result($attrs)->insert; +} + +=head2 find_or_create + +=over 4 + +=item Arguments: \%vals, \%attrs? + +=item Return Value: $rowobject + +=back + + $cd->cd_to_producer->find_or_create({ producer => $producer }, + { key => 'primary }); + +Tries to find a record based on its primary key or unique constraints; if none +is found, creates one and returns that instead. + + my $cd = $schema->resultset('CD')->find_or_create({ + cdid => 5, + artist => 'Massive Attack', + title => 'Mezzanine', + year => 2005, + }); + +Also takes an optional C attribute, to search by a specific key or unique +constraint. For example: + + my $cd = $schema->resultset('CD')->find_or_create( + { + artist => 'Massive Attack', + title => 'Mezzanine', + }, + { key => 'cd_artist_title' } + ); + +B: Because find_or_create() reads from the database and then +possibly inserts based on the result, this method is subject to a race +condition. Another process could create a record in the table after +the find has completed and before the create has started. To avoid +this problem, use find_or_create() inside a transaction. + +B: C is probably not what you want when creating +a new row in a table that uses primary keys supplied by the +database. Passing in a primary key column with a value of I +will cause L to attempt to search for a row with a value of +I. + +See also L and L. For information on how to declare +unique constraints, see L. + +=cut + +sub find_or_create { + my $self = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; + my $exists = $self->find($hash, $attrs); + return defined $exists ? $exists : $self->create($hash); +} + +=head2 update_or_create + +=over 4 + +=item Arguments: \%col_values, { key => $unique_constraint }? + +=item Return Value: $rowobject + +=back + + $resultset->update_or_create({ col => $val, ... }); + +First, searches for an existing row matching one of the unique constraints +(including the primary key) on the source of this resultset. If a row is +found, updates it with the other given column values. Otherwise, creates a new +row. + +Takes an optional C attribute to search on a specific unique constraint. +For example: + + # In your application + my $cd = $schema->resultset('CD')->update_or_create( + { + artist => 'Massive Attack', + title => 'Mezzanine', + year => 1998, + }, + { key => 'cd_artist_title' } + ); + + $cd->cd_to_producer->update_or_create({ + producer => $producer, + name => 'harry', + }, { + key => 'primary, + }); + + +If no C is specified, it searches on all unique constraints defined on the +source, including the primary key. + +If the C is specified as C, it searches only on the primary key. + +See also L and L. For information on how to declare +unique constraints, see L. + +B: C is probably not what you want when +looking for a row in a table that uses primary keys supplied by the +database, unless you actually have a key value. Passing in a primary +key column with a value of I will cause L to attempt to +search for a row with a value of I. + +=cut + +sub update_or_create { + my $self = shift; + my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); + my $cond = ref $_[0] eq 'HASH' ? shift : {@_}; + + my $row = $self->find($cond, $attrs); + if (defined $row) { + $row->update($cond); + return $row; + } + + return $self->create($cond); +} + +=head2 get_cache + +=over 4 + +=item Arguments: none + +=item Return Value: \@cache_objects? + +=back + +Gets the contents of the cache for the resultset, if the cache is set. + +The cache is populated either by using the L attribute to +L or by calling L. + +=cut + +sub get_cache { + shift->{all_cache}; +} + +=head2 set_cache + +=over 4 + +=item Arguments: \@cache_objects + +=item Return Value: \@cache_objects + +=back + +Sets the contents of the cache for the resultset. Expects an arrayref +of objects of the same class as those produced by the resultset. Note that +if the cache is set the resultset will return the cached objects rather +than re-querying the database even if the cache attr is not set. + +The contents of the cache can also be populated by using the +L attribute to L. + +=cut + +sub set_cache { + my ( $self, $data ) = @_; + $self->throw_exception("set_cache requires an arrayref") + if defined($data) && (ref $data ne 'ARRAY'); + $self->{all_cache} = $data; +} + +=head2 clear_cache + +=over 4 + +=item Arguments: none + +=item Return Value: [] + +=back + +Clears the cache for the resultset. + +=cut + +sub clear_cache { + shift->set_cache(undef); +} + +=head2 related_resultset + +=over 4 + +=item Arguments: $relationship_name + +=item Return Value: $resultset + +=back + +Returns a related resultset for the supplied relationship name. + + $artist_rs = $schema->resultset('CD')->related_resultset('Artist'); + +=cut + +sub related_resultset { + my ($self, $rel) = @_; + + $self->{related_resultsets} ||= {}; + return $self->{related_resultsets}{$rel} ||= do { + my $rel_obj = $self->result_source->relationship_info($rel); + + $self->throw_exception( + "search_related: result source '" . $self->result_source->source_name . + "' has no such relationship $rel") + unless $rel_obj; + + my ($from,$seen) = $self->_resolve_from($rel); + + my $join_count = $seen->{$rel}; + my $alias = ($join_count > 1 ? join('_', $rel, $join_count) : $rel); + + #XXX - temp fix for result_class bug. There likely is a more elegant fix -groditi + my %attrs = %{$self->{attrs}||{}}; + delete @attrs{qw(result_class alias)}; + + my $new_cache; + + if (my $cache = $self->get_cache) { + if ($cache->[0] && $cache->[0]->related_resultset($rel)->get_cache) { + $new_cache = [ map { @{$_->related_resultset($rel)->get_cache} } + @$cache ]; + } + } + + my $rel_source = $self->result_source->related_source($rel); + + my $new = do { + + # The reason we do this now instead of passing the alias to the + # search_rs below is that if you wrap/overload resultset on the + # source you need to know what alias it's -going- to have for things + # to work sanely (e.g. RestrictWithObject wants to be able to add + # extra query restrictions, and these may need to be $alias.) + + my $attrs = $rel_source->resultset_attributes; + local $attrs->{alias} = $alias; + + $rel_source->resultset + ->search_rs( + undef, { + %attrs, + join => undef, + prefetch => undef, + select => undef, + as => undef, + where => $self->{cond}, + seen_join => $seen, + from => $from, + }); + }; + $new->set_cache($new_cache) if $new_cache; + $new; + }; +} + +=head2 current_source_alias + +=over 4 + +=item Arguments: none -=head3 Arguments: (\%vals) +=item Return Value: $source_alias -Inserts a record into the resultset and returns the object. +=back -Effectively a shortcut for C<< ->new_result(\%vals)->insert >>. +Returns the current table alias for the result source this resultset is built +on, that will be used in the SQL query. Usually it is C. -=cut +Currently the source alias that refers to the result set returned by a +L/L family method depends on how you got to the resultset: it's +C by default, but eg. L aliases it to the related result +source name (and keeps C referring to the original result set). The long +term goal is to make L always alias the current resultset as C +(and make this method unnecessary). -sub create { - my ($self, $attrs) = @_; - $self->throw_exception( "create needs a hashref" ) unless ref $attrs eq 'HASH'; - return $self->new_result($attrs)->insert; -} +Thus it's currently necessary to use this method in predefined queries (see +L) when referring to the +source alias of the current result set: -=head2 find_or_create + # in a result set class + sub modified_by { + my ($self, $user) = @_; -=head3 Arguments: (\%vals, \%attrs?) + my $me = $self->current_source_alias; - $class->find_or_create({ key => $val, ... }); + return $self->search( + "$me.modified" => $user->id, + ); + } -Searches for a record matching the search condition; if it doesn't find one, -creates one and returns that instead. +=cut - my $cd = $schema->resultset('CD')->find_or_create({ - cdid => 5, - artist => 'Massive Attack', - title => 'Mezzanine', - year => 2005, - }); +sub current_source_alias { + my ($self) = @_; -Also takes an optional C attribute, to search by a specific key or unique -constraint. For example: + return ($self->{attrs} || {})->{alias} || 'me'; +} - my $cd = $schema->resultset('CD')->find_or_create( - { - artist => 'Massive Attack', - title => 'Mezzanine', - }, - { key => 'artist_title' } - ); +sub _resolve_from { + my ($self, $extra_join) = @_; + my $source = $self->result_source; + my $attrs = $self->{attrs}; + + my $from = $attrs->{from} + || [ { $attrs->{alias} => $source->from } ]; + + my $seen = { %{$attrs->{seen_join}||{}} }; -See also L and L. + my $join = ($attrs->{join} + ? [ $attrs->{join}, $extra_join ] + : $extra_join); -=cut + # we need to take the prefetch the attrs into account before we + # ->resolve_join as otherwise they get lost - captainL + my $merged = $self->_merge_attr( $join, $attrs->{prefetch} ); -sub find_or_create { - my $self = shift; - my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); - my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; - my $exists = $self->find($hash, $attrs); - return defined $exists ? $exists : $self->create($hash); -} + $from = [ + @$from, + ($join ? $source->resolve_join($merged, $attrs->{alias}, $seen) : ()), + ]; -=head2 update_or_create + return ($from,$seen); +} - $class->update_or_create({ key => $val, ... }); +sub _resolved_attrs { + my $self = shift; + return $self->{_attrs} if $self->{_attrs}; + + my $attrs = { %{ $self->{attrs} || {} } }; + my $source = $self->result_source; + my $alias = $attrs->{alias}; + + $attrs->{columns} ||= delete $attrs->{cols} if exists $attrs->{cols}; + my @colbits; + + # build columns (as long as select isn't set) into a set of as/select hashes + unless ( $attrs->{select} ) { + @colbits = map { + ( ref($_) eq 'HASH' ) ? $_ + : { + ( + /^\Q${alias}.\E(.+)$/ ? $1 + : $_ + ) => ( /\./ ? $_ : "${alias}.$_" ) + } + } ( ref($attrs->{columns}) eq 'ARRAY' ) ? @{ delete $attrs->{columns}} : (delete $attrs->{columns} || $source->columns ); + } + # add the additional columns on + foreach ( 'include_columns', '+columns' ) { + push @colbits, map { + ( ref($_) eq 'HASH' ) + ? $_ + : { ( split( /\./, $_ ) )[-1] => ( /\./ ? $_ : "${alias}.$_" ) } + } ( ref($attrs->{$_}) eq 'ARRAY' ) ? @{ delete $attrs->{$_} } : delete $attrs->{$_} if ( $attrs->{$_} ); + } -First, search for an existing row matching one of the unique constraints -(including the primary key) on the source of this resultset. If a row is -found, update it with the other given column values. Otherwise, create a new -row. + # start with initial select items + if ( $attrs->{select} ) { + $attrs->{select} = + ( ref $attrs->{select} eq 'ARRAY' ) + ? [ @{ $attrs->{select} } ] + : [ $attrs->{select} ]; + $attrs->{as} = ( + $attrs->{as} + ? ( + ref $attrs->{as} eq 'ARRAY' + ? [ @{ $attrs->{as} } ] + : [ $attrs->{as} ] + ) + : [ map { m/^\Q${alias}.\E(.+)$/ ? $1 : $_ } @{ $attrs->{select} } ] + ); + } + else { -Takes an optional C attribute to search on a specific unique constraint. -For example: + # otherwise we intialise select & as to empty + $attrs->{select} = []; + $attrs->{as} = []; + } - # In your application - my $cd = $schema->resultset('CD')->update_or_create( - { - artist => 'Massive Attack', - title => 'Mezzanine', - year => 1998, - }, - { key => 'artist_title' } - ); + # now add colbits to select/as + push( @{ $attrs->{select} }, map { values( %{$_} ) } @colbits ); + push( @{ $attrs->{as} }, map { keys( %{$_} ) } @colbits ); -If no C is specified, it searches on all unique constraints defined on the -source, including the primary key. + my $adds; + if ( $adds = delete $attrs->{'+select'} ) { + $adds = [$adds] unless ref $adds eq 'ARRAY'; + push( + @{ $attrs->{select} }, + map { /\./ || ref $_ ? $_ : "${alias}.$_" } @$adds + ); + } + if ( $adds = delete $attrs->{'+as'} ) { + $adds = [$adds] unless ref $adds eq 'ARRAY'; + push( @{ $attrs->{as} }, @$adds ); + } -If the C is specified as C, search only on the primary key. + $attrs->{from} ||= [ { $self->{attrs}{alias} => $source->from } ]; -See also L and L. + if ( exists $attrs->{join} || exists $attrs->{prefetch} ) { + my $join = delete $attrs->{join} || {}; -=cut + if ( defined $attrs->{prefetch} ) { + $join = $self->_merge_attr( $join, $attrs->{prefetch} ); -sub update_or_create { - my $self = shift; - my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {}); - my $hash = ref $_[0] eq 'HASH' ? shift : {@_}; + } - my %unique_constraints = $self->result_source->unique_constraints; - my @constraint_names = (exists $attrs->{key} - ? ($attrs->{key}) - : keys %unique_constraints); + $attrs->{from} = # have to copy here to avoid corrupting the original + [ + @{ $attrs->{from} }, + $source->resolve_join( + $join, $alias, { %{ $attrs->{seen_join} || {} } } + ) + ]; - my @unique_hashes; - foreach my $name (@constraint_names) { - my @unique_cols = @{ $unique_constraints{$name} }; - my %unique_hash = - map { $_ => $hash->{$_} } - grep { exists $hash->{$_} } - @unique_cols; + } - push @unique_hashes, \%unique_hash - if (scalar keys %unique_hash == scalar @unique_cols); + $attrs->{group_by} ||= $attrs->{select} + if delete $attrs->{distinct}; + if ( $attrs->{order_by} ) { + $attrs->{order_by} = ( + ref( $attrs->{order_by} ) eq 'ARRAY' + ? [ @{ $attrs->{order_by} } ] + : [ $attrs->{order_by} ] + ); + } + else { + $attrs->{order_by} = []; } - if (@unique_hashes) { - my $row = $self->single(\@unique_hashes); - if (defined $row) { - $row->set_columns($hash); - $row->update; - return $row; + my $collapse = $attrs->{collapse} || {}; + if ( my $prefetch = delete $attrs->{prefetch} ) { + $prefetch = $self->_merge_attr( {}, $prefetch ); + my @pre_order; + my $seen = { %{ $attrs->{seen_join} || {} } }; + foreach my $p ( ref $prefetch eq 'ARRAY' ? @$prefetch : ($prefetch) ) { + + # bring joins back to level of current class + my @prefetch = + $source->resolve_prefetch( $p, $alias, $seen, \@pre_order, $collapse ); + push( @{ $attrs->{select} }, map { $_->[0] } @prefetch ); + push( @{ $attrs->{as} }, map { $_->[1] } @prefetch ); } + push( @{ $attrs->{order_by} }, @pre_order ); } + $attrs->{collapse} = $collapse; - return $self->create($hash); -} + if ( $attrs->{page} ) { + $attrs->{offset} ||= 0; + $attrs->{offset} += ( $attrs->{rows} * ( $attrs->{page} - 1 ) ); + } -=head2 get_cache + return $self->{_attrs} = $attrs; +} -Gets the contents of the cache for the resultset. +sub _rollout_attr { + my ($self, $attr) = @_; + + if (ref $attr eq 'HASH') { + return $self->_rollout_hash($attr); + } elsif (ref $attr eq 'ARRAY') { + return $self->_rollout_array($attr); + } else { + return [$attr]; + } +} -=cut +sub _rollout_array { + my ($self, $attr) = @_; -sub get_cache { - shift->{all_cache} || []; + my @rolled_array; + foreach my $element (@{$attr}) { + if (ref $element eq 'HASH') { + push( @rolled_array, @{ $self->_rollout_hash( $element ) } ); + } elsif (ref $element eq 'ARRAY') { + # XXX - should probably recurse here + push( @rolled_array, @{$self->_rollout_array($element)} ); + } else { + push( @rolled_array, $element ); + } + } + return \@rolled_array; } -=head2 set_cache +sub _rollout_hash { + my ($self, $attr) = @_; -Sets the contents of the cache for the resultset. Expects an arrayref of objects of the same class as those produced by the resultset. + my @rolled_array; + foreach my $key (keys %{$attr}) { + push( @rolled_array, { $key => $attr->{$key} } ); + } + return \@rolled_array; +} -=cut +sub _calculate_score { + my ($self, $a, $b) = @_; -sub set_cache { - my ( $self, $data ) = @_; - $self->throw_exception("set_cache requires an arrayref") - if ref $data ne 'ARRAY'; - my $result_class = $self->result_class; - foreach( @$data ) { - $self->throw_exception("cannot cache object of type '$_', expected '$result_class'") - if ref $_ ne $result_class; + if (ref $b eq 'HASH') { + my ($b_key) = keys %{$b}; + if (ref $a eq 'HASH') { + my ($a_key) = keys %{$a}; + if ($a_key eq $b_key) { + return (1 + $self->_calculate_score( $a->{$a_key}, $b->{$b_key} )); + } else { + return 0; + } + } else { + return ($a eq $b_key) ? 1 : 0; + } + } else { + if (ref $a eq 'HASH') { + my ($a_key) = keys %{$a}; + return ($b eq $a_key) ? 1 : 0; + } else { + return ($b eq $a) ? 1 : 0; + } } - $self->{all_cache} = $data; } -=head2 clear_cache +sub _merge_attr { + my ($self, $orig, $import) = @_; -Clears the cache for the resultset. + return $import unless defined($orig); + return $orig unless defined($import); + + $orig = $self->_rollout_attr($orig); + $import = $self->_rollout_attr($import); + + my $seen_keys; + foreach my $import_element ( @{$import} ) { + # find best candidate from $orig to merge $b_element into + my $best_candidate = { position => undef, score => 0 }; my $position = 0; + foreach my $orig_element ( @{$orig} ) { + my $score = $self->_calculate_score( $orig_element, $import_element ); + if ($score > $best_candidate->{score}) { + $best_candidate->{position} = $position; + $best_candidate->{score} = $score; + } + $position++; + } + my ($import_key) = ( ref $import_element eq 'HASH' ) ? keys %{$import_element} : ($import_element); -=cut + if ($best_candidate->{score} == 0 || exists $seen_keys->{$import_key}) { + push( @{$orig}, $import_element ); + } else { + my $orig_best = $orig->[$best_candidate->{position}]; + # merge orig_best and b_element together and replace original with merged + if (ref $orig_best ne 'HASH') { + $orig->[$best_candidate->{position}] = $import_element; + } elsif (ref $import_element eq 'HASH') { + my ($key) = keys %{$orig_best}; + $orig->[$best_candidate->{position}] = { $key => $self->_merge_attr($orig_best->{$key}, $import_element->{$key}) }; + } + } + $seen_keys->{$import_key} = 1; # don't merge the same key twice + } -sub clear_cache { - shift->set_cache([]); + return $orig; } -=head2 related_resultset - -Returns a related resultset for the supplied relationship name. - - $artist_rs = $schema->resultset('CD')->related_resultset('Artist'); - -=cut +sub result_source { + my $self = shift; -sub related_resultset { - my ( $self, $rel, @rest ) = @_; - $self->{related_resultsets} ||= {}; - return $self->{related_resultsets}{$rel} ||= do { - #warn "fetching related resultset for rel '$rel'"; - my $rel_obj = $self->result_source->relationship_info($rel); - $self->throw_exception( - "search_related: result source '" . $self->result_source->name . - "' has no such relationship ${rel}") - unless $rel_obj; #die Dumper $self->{attrs}; - - my $rs = $self->search(undef, { join => $rel }); - my $alias = defined $rs->{attrs}{seen_join}{$rel} - && $rs->{attrs}{seen_join}{$rel} > 1 - ? join('_', $rel, $rs->{attrs}{seen_join}{$rel}) - : $rel; - - $self->result_source->schema->resultset($rel_obj->{class} - )->search( undef, - { %{$rs->{attrs}}, - alias => $alias, - select => undef, - as => undef } - )->search(@rest); - }; + if (@_) { + $self->_source_handle($_[0]->handle); + } else { + $self->_source_handle->resolve; + } } =head2 throw_exception -See Schema's throw_exception +See L for details. =cut sub throw_exception { my $self=shift; - $self->result_source->schema->throw_exception(@_); + if (ref $self && $self->_source_handle->schema) { + $self->_source_handle->schema->throw_exception(@_) + } else { + croak(@_); + } + } +# XXX: FIXME: Attributes docs need clearing up + =head1 ATTRIBUTES -The resultset takes various attributes that modify its behavior. Here's an -overview of them: +Attributes are used to refine a ResultSet in various ways when +searching for data. They can be passed to any method which takes an +C<\%attrs> argument. See L, L, L, +L. + +These are in no particular order: =head2 order_by +=over 4 + +=item Value: ($order_by | \@order_by) + +=back + Which column(s) to order the results by. This is currently passed through directly to SQL, so you can give e.g. C for a descending order on the column `year'. +Please note that if you have C enabled (see +L) you will need to do C<\'year DESC' > to +specify an order. (The scalar ref causes it to be passed as raw sql to the DB, +so you will need to manually quote things as appropriate.) + +If your L version supports it (>=1.50), you can also use +C<{-desc => 'year'}>, which takes care of the quoting for you. This is the +recommended syntax. + =head2 columns -=head3 Arguments: (arrayref) +=over 4 -Shortcut to request a particular set of columns to be retrieved. Adds -C onto the start of any column without a C<.> in it and sets C as normal. (You may also -use the C attribute, as in earlier versions of DBIC.) +=item Value: \@columns -=head2 include_columns +=back + +Shortcut to request a particular set of columns to be retrieved. Each +column spec may be a string (a table column name), or a hash (in which +case the key is the C value, and the value is used as the C from that, then auto-populates C from +C but adds columns to the selection. + +=back + +=head2 +as + +=over 4 + +Indicates additional column names for those added via L. See L. + +=back + =head2 as -=head3 Arguments: (arrayref) +=over 4 -Indicates column names for object inflation. This is used in conjunction with -C contains one or more function or stored +=item Value: \@inflation_names + +=back + +Indicates column names for object inflation. That is, C +indicates the name that the column can be accessed as via the +C method (or via the object accessor, B). It has nothing to do with the SQL code C, +usually when C attibute that contains the C text, eg: + + select => [\'myfield AS alias'] + =head2 join +=over 4 + +=item Value: ($rel_name | \@rel_names | \%rel_names) + +=back + Contains a list of relationships that should be joined for this query. For example: @@ -1085,6 +2750,19 @@ For example: } ); +You need to use the relationship (not the table) name in conditions, +because they are aliased as such. The current table is aliased as "me", so +you need to use me.column_name in order to avoid ambiguity. For example: + + # Get CDs from 1984 with a 'Foo' track + my $rs = $schema->resultset('CD')->search( + { + 'me.year' => 1984, + 'tracks.name' => 'Foo' + }, + { join => 'tracks' } + ); + If the same join is supplied twice, it will be aliased to _2 (and similarly for a third time). For e.g. @@ -1101,14 +2779,21 @@ to Earth' and a cd with title 'Popular'. If you want to fetch related objects from other tables as well, see C below. +For more help on using joins with search, see L. + =head2 prefetch -=head3 Arguments: arrayref/hashref +=over 4 + +=item Value: ($rel_name | \@rel_names | \%rel_names) -Contains one or more relationships that should be fetched along with the main -query (when they are accessed afterwards they will have already been -"prefetched"). This is useful for when you know you will need the related -objects, because it saves at least one query: +=back + +Contains one or more relationships that should be fetched along with +the main query (when they are accessed afterwards the data will +already be available, without extra queries to the database). This is +useful for when you know you will need the related objects, because it +saves at least one query: my $rs = $schema->resultset('Tag')->search( undef, @@ -1130,38 +2815,174 @@ C or C relationships, which saves us two SQL statements in this case. Simple prefetches will be joined automatically, so there is no need -for a C attribute in the above search. If you're prefetching to -depth (e.g. { cd => { artist => 'label' } or similar), you'll need to -specify the join as well. +for a C attribute in the above search. C can be used with the following relationship types: C, C (or if you're using C, any relationship declared -with an accessor type of 'single' or 'filter'). +with an accessor type of 'single' or 'filter'). A more complex example that +prefetches an artists cds, the tracks on those cds, and the tags associted +with that artist is given below (assuming many-to-many from artists to tags): + + my $rs = $schema->resultset('Artist')->search( + undef, + { + prefetch => [ + { cds => 'tracks' }, + { artist_tags => 'tags' } + ] + } + ); + + +B If you specify a C attribute, the C and C