Merge 'resultsetcolumn_custom_columns' into 'trunk'
Norbert Buchmuller [Mon, 17 Nov 2008 02:11:42 +0000 (03:11 +0100)]
r5165@vger:  mendel | 2008-11-16 20:11:42 +0100
 * Merged in changes from 'resultsetcolumn_custom_columns' branch,

16 files changed:
Changes
lib/DBIx/Class/Manual/Cookbook.pod
lib/DBIx/Class/Relationship/BelongsTo.pm
lib/DBIx/Class/ResultSet.pm
lib/DBIx/Class/ResultSetColumn.pm
lib/DBIx/Class/ResultSource.pm
lib/DBIx/Class/Storage/DBI.pm
t/66relationship.t
t/68inflate_resultclass_hashrefinflator.t
t/73oracle.t
t/76select.t
t/77prefetch.t
t/96multi_create.t
t/97result_class.t
t/lib/DBICTest/Schema/ForceForeign.pm
t/lib/sqlite.sql

diff --git a/Changes b/Changes
index 6b93887..8bd0118 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,4 +1,8 @@
 Revision history for DBIx::Class
+        - Allow a scalarref to be supplied to the 'from' resultset attribute
+        - Classes submitted as result_class for a resultsource are now
+          automatically loaded via ensure_loaded()
+        - 'result_class' resultset attribute, identical to result_class()
 
 0.08099_05 2008-10-30 21:30:00 (UTC)
         - Rewritte of Storage::DBI::connect_info(), extended with an
index 293363d..d2dfcf9 100644 (file)
@@ -141,7 +141,7 @@ you have to add to your User class:
   SQL 
 
   # Finally, register your new ResultSource with your Schema
-  My::Schema->register_source( 'UserFriendsComplex' => $new_source );
+  My::Schema->register_extra_source( 'UserFriendsComplex' => $new_source );
 
 Next, you can execute your complex query using bind parameters like this:
 
index 5756f2b..272b01b 100644 (file)
@@ -9,6 +9,11 @@ use warnings;
 
 sub belongs_to {
   my ($class, $rel, $f_class, $cond, $attrs) = @_;
+
+  # assume a foreign key contraint unless defined otherwise
+  $attrs->{is_foreign_key_constraint} = 1 
+    if not exists $attrs->{is_foreign_key_constraint};
+
   # no join condition or just a column name
   if (!ref $cond) {
     $class->ensure_class_loaded($f_class);
index bd615d3..17d05f5 100644 (file)
@@ -15,7 +15,7 @@ use List::Util ();
 use Scalar::Util ();
 use base qw/DBIx::Class/;
 
-__PACKAGE__->mk_group_accessors('simple' => qw/result_class _source_handle/);
+__PACKAGE__->mk_group_accessors('simple' => qw/_result_class _source_handle/);
 
 =head1 NAME
 
@@ -108,7 +108,6 @@ sub new {
   # see https://bugzilla.redhat.com/show_bug.cgi?id=196836
   my $self = {
     _source_handle => $source,
-    result_class => $attrs->{result_class} || $source->resolve->result_class,
     cond => $attrs->{where},
     count => undef,
     pager => undef,
@@ -117,6 +116,10 @@ sub new {
 
   bless $self, $class;
 
+  $self->result_class(
+    $attrs->{result_class} || $source->resolve->result_class
+  );
+
   return $self;
 }
 
@@ -341,6 +344,9 @@ source for which column data is provided, including the primary key.
 If your table does not have a primary key, you B<must> provide a value for the
 C<key> attribute matching one of the unique constraints on the source.
 
+In addition to C<key>, L</find> recognizes and applies standard
+L<resultset attributes|/ATTRIBUTES> in the same way as L</search> does.
+
 Note: If your query does not return only one row, a warning is generated:
 
   Query returned more than one row
@@ -985,6 +991,14 @@ 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
 
@@ -2784,6 +2798,58 @@ with a father in the person table, we could explicitly use C<INNER JOIN>:
     # SELECT child.* FROM person child
     # INNER JOIN person father ON child.father_id = father.id
 
+If you need to express really complex joins or you need a subselect, you
+can supply literal SQL to C<from> via a scalar reference. In this case
+the contents of the scalar will replace the table name asscoiated with the
+resultsource.
+
+WARNING: This technique might very well not work as expected on chained
+searches - you have been warned.
+
+    # Assuming the Event resultsource is defined as:
+
+        MySchema::Event->add_columns (
+            sequence => {
+                data_type => 'INT',
+                is_auto_increment => 1,
+            },
+            location => {
+                data_type => 'INT',
+            },
+            type => {
+                data_type => 'INT',
+            },
+        );
+        MySchema::Event->set_primary_key ('sequence');
+
+    # This will get back the latest event for every location. The column
+    # selector is still provided by DBIC, all we do is add a JOIN/WHERE
+    # combo to limit the resultset
+
+    $rs = $schema->resultset('Event');
+    $table = $rs->result_source->name;
+    $latest = $rs->search (
+        undef,
+        { from => \ " 
+            (SELECT e1.* FROM $table e1 
+                JOIN $table e2 
+                    ON e1.location = e2.location 
+                    AND e1.sequence < e2.sequence 
+                WHERE e2.sequence is NULL 
+            ) me",
+        },
+    );
+
+    # Equivalent SQL (with the DBIC chunks added):
+
+    SELECT me.sequence, me.location, me.type FROM
+       (SELECT e1.* FROM events e1
+           JOIN events e2
+               ON e1.location = e2.location
+               AND e1.sequence < e2.sequence
+           WHERE e2.sequence is NULL
+       ) me;
+
 =head2 for
 
 =over 4
index 167cd02..7b0fee6 100644 (file)
@@ -40,10 +40,13 @@ sub new {
   my $attrs = $new_parent_rs->_resolved_attrs;
   $new_parent_rs->{attrs}->{$_} = undef for qw(prefetch include_columns +select +as); # prefetch, include_columns, +select, +as cause additional columns to be fetched
 
+  # If $column can be found in the 'as' list of the parent resultset, use the
+  # corresponding element of its 'select' list (to keep any custom column
+  # definition set up with 'select' or '+select' attrs), otherwise use $column
+  # (to create a new column definition on-the-fly).
   my $as_list = $attrs->{as} || [];
   my $select_list = $attrs->{select} || [];
   my $as_index = List::Util::first { ($as_list->[$_] || "") eq $column } 0..$#$as_list;
-
   my $select = defined $as_index ? $select_list->[$as_index] : $column;
 
   my $new = bless { _select => $select, _as => $column, _parent_resultset => $new_parent_rs }, $class;
index f3d6792..83ad7cc 100644 (file)
@@ -843,7 +843,7 @@ sub resolve_condition {
         #warn "$self $k $for $v";
         unless ($for->has_column_loaded($v)) {
           if ($for->in_storage) {
-            $self->throw_exception("Column ${v} not loaded on ${for} trying to reolve relationship");
+            $self->throw_exception("Column ${v} not loaded on ${for} trying to resolve relationship");
           }
           return $UNRESOLVABLE_CONDITION;
         }
index ea81980..b395932 100644 (file)
@@ -86,7 +86,12 @@ sub _find_syntax {
 
 sub select {
   my ($self, $table, $fields, $where, $order, @rest) = @_;
-  $table = $self->_quote($table) unless ref($table);
+  if (ref $table eq 'SCALAR') {
+    $table = $$table;
+  }
+  elsif (not ref $table) {
+    $table = $self->_quote($table);
+  }
   local $self->{rownum_hack_count} = 1
     if (defined $rest[0] && $self->{limit_dialect} eq 'RowNum');
   @rest = (-1) unless defined $rest[0];
index 4506bda..81646fa 100644 (file)
@@ -7,7 +7,7 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-plan tests => 67;
+plan tests => 69;
 
 # has_a test
 my $cd = $schema->resultset("CD")->find(4);
@@ -117,14 +117,11 @@ $cd = $artist->find_or_new_related( 'cds', {
 is( $cd->title, 'Greatest Hits 2: Louder Than Ever', 'find_or_new_related new record ok' );
 ok( ! $cd->in_storage, 'find_or_new_related on a new record: not in_storage' );
 
-# print STDERR Data::Dumper::Dumper($cd->get_columns);
-# $cd->result_source->schema->storage->debug(1);
 $cd->artist(undef);
 my $newartist = $cd->find_or_new_related( 'artist', {
   name => 'Random Boy Band Two',
   artistid => 200,
 } );
-# $cd->result_source->schema->storage->debug(0);
 is($newartist->name, 'Random Boy Band Two', 'find_or_new_related new artist record with id');
 is($newartist->id, 200, 'find_or_new_related new artist id set');
 
@@ -263,3 +260,12 @@ eval {
 is ($@, '', 'Staged insertion successful');
 ok($new_artist->in_storage, 'artist inserted');
 ok($new_related_cd->in_storage, 'new_related_cd inserted');
+
+# check if is_foreign_key_constraint attr is set
+my $rs_normal = $schema->source('Track');
+my $relinfo = $rs_normal->relationship_info ('cd');
+cmp_ok($relinfo->{attrs}{is_foreign_key_constraint}, '==', 1, "is_foreign_key_constraint defined for belongs_to relationships.");
+
+my $rs_overridden = $schema->source('ForceForeign');
+my $relinfo_with_attr = $rs_overridden->relationship_info ('cd_3');
+cmp_ok($relinfo_with_attr->{attrs}{is_foreign_key_constraint}, '==', 0, "is_foreign_key_constraint defined for belongs_to relationships with attr.");
index 214e11a..7138989 100644 (file)
@@ -4,7 +4,6 @@ use warnings;
 use Test::More qw(no_plan);
 use lib qw(t/lib);
 use DBICTest;
-use DBIx::Class::ResultClass::HashRefInflator;
 my $schema = DBICTest->init_schema();
 
 
@@ -62,6 +61,7 @@ sub check_cols_of {
 $schema->resultset('CD')->create({ title => 'Silence is golden', artist => 3, year => 2006 });
 
 # order_by to ensure both resultsets have the rows in the same order
+# also check result_class-as-an-attribute syntax
 my $rs_dbic = $schema->resultset('CD')->search(undef,
     {
         prefetch    => [ qw/ artist tracks / ],
@@ -72,9 +72,9 @@ my $rs_hashrefinf = $schema->resultset('CD')->search(undef,
     {
         prefetch    => [ qw/ artist tracks / ],
         order_by    => [ 'me.cdid', 'tracks.position' ],
+        result_class => 'DBIx::Class::ResultClass::HashRefInflator',
     }
 );
-$rs_hashrefinf->result_class('DBIx::Class::ResultClass::HashRefInflator');
 
 my @dbic        = $rs_dbic->all;
 my @hashrefinf  = $rs_hashrefinf->all;
@@ -98,8 +98,8 @@ $rs_hashrefinf = $schema->resultset ('Artist')->search ({ 'me.artistid' => 1}, {
     select   => [qw/name   tracks.title      tracks.cd       /],
     as       => [qw/name   cds.tracks.title  cds.tracks.cd   /],
     order_by => [qw/cds.cdid tracks.trackid/],
+    result_class => 'DBIx::Class::ResultClass::HashRefInflator',
 });
-$rs_hashrefinf->result_class('DBIx::Class::ResultClass::HashRefInflator');
 
 @dbic = map { $_->tracks->all } ($rs_dbic->first->cds->all);
 @hashrefinf  = $rs_hashrefinf->all;
index a53b3e4..12929b6 100644 (file)
@@ -32,7 +32,7 @@ $dbh->do("CREATE SEQUENCE artist_seq START WITH 1 MAXVALUE 999999 MINVALUE 0");
 $dbh->do("CREATE SEQUENCE pkid1_seq START WITH 1 MAXVALUE 999999 MINVALUE 0");
 $dbh->do("CREATE SEQUENCE pkid2_seq START WITH 10 MAXVALUE 999999 MINVALUE 0");
 $dbh->do("CREATE SEQUENCE nonpkid_seq START WITH 20 MAXVALUE 999999 MINVALUE 0");
-$dbh->do("CREATE TABLE artist (artistid NUMBER(12), name VARCHAR(255), rank NUMBER(38))");
+$dbh->do("CREATE TABLE artist (artistid NUMBER(12), name VARCHAR(255), rank NUMBER(38), charfield VARCHAR2(10))");
 $dbh->do("CREATE TABLE sequence_test (pkid1 NUMBER(12), pkid2 NUMBER(12), nonpkid NUMBER(12), name VARCHAR(255))");
 $dbh->do("CREATE TABLE cd (cdid NUMBER(12), artist NUMBER(12), title VARCHAR(255), year VARCHAR(4))");
 $dbh->do("CREATE TABLE track (trackid NUMBER(12), cd NUMBER(12), position NUMBER(12), title VARCHAR(255), last_updated_on DATE)");
index 213ecba..2d60873 100644 (file)
@@ -8,7 +8,7 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-plan tests => 7;
+plan tests => 11;
 
 my $rs = $schema->resultset('CD')->search({},
     {
@@ -42,3 +42,22 @@ $rs = $schema->resultset('CD')->search({},
 lives_ok(sub { $rs->first->get_column('count') }, '+select/+as chained search 1st rscolumn present');
 lives_ok(sub { $rs->first->get_column('addedtitle') }, '+select/+as chained search 1st rscolumn present');
 lives_ok(sub { $rs->first->get_column('addedtitle2') }, '+select/+as chained search 3rd rscolumn present');
+
+
+# test the from search attribute (gets between the FROM and WHERE keywords, allows arbitrary subselects)
+# also shows that outer select attributes are ok (i.e. order_by)
+#
+# from doesn't seem to be useful without using a scalarref - there were no initial tests >:(
+#
+my $cds = $schema->resultset ('CD')->search ({}, { order_by => 'me.cdid'}); # make sure order is consistent
+cmp_ok ($cds->count, '>', 2, 'Initially populated with more than 2 CDs');
+
+my $table = $cds->result_source->name;
+my $subsel = $cds->search ({}, {
+    columns => [qw/cdid title/],
+    from => \ "(SELECT cdid, title FROM $table LIMIT 2) me",
+});
+
+is ($subsel->count, 2, 'Subselect correctly limited the rs to 2 cds');
+is ($subsel->next->title, $cds->next->title, 'First CD title match');
+is ($subsel->next->title, $cds->next->title, 'Second CD title match');
index a91c429..608c8eb 100644 (file)
@@ -348,7 +348,6 @@ is($queries, 0, 'chained search_related after has_many->has_many prefetch ran no
 # (the TODO block itself contains tests ensuring that the warns are removed)
 TODO: {
     local $TODO = 'Prefetch of multiple has_many rels at the same level (currently warn to protect the clueless git)';
-    use DBIx::Class::ResultClass::HashRefInflator;
 
     #( 1 -> M + M )
     my $cd_rs = $schema->resultset('CD')->search ({ 'me.title' => 'Forkful of bees' });
index 6461ad7..f767327 100644 (file)
@@ -5,7 +5,7 @@ use Test::More;
 use lib qw(t/lib);
 use DBICTest;
 
-plan tests => 58;
+plan tests => 62;
 
 my $schema = DBICTest->init_schema();
 
@@ -510,3 +510,44 @@ eval {
   );
 };
 diag $@ if $@;
+
+## test might_have/has_many interactions
+my $ff = $schema->resultset('ForceForeign');
+
+my $thing = $ff->create(
+    {
+        artist_1 => 
+        { 
+            name => 'Crazy Frog',
+            cds => 
+                [
+                 {
+                     title => 'CD1',
+                     year => 2007,
+                     artist => {
+                         name => 'Artist 1',
+                     }
+                 },
+                 {
+                     title => 'CD2',
+                     year => 2007,
+                     artist => {
+                         name => 'Artist 2',
+                     }
+                 },
+                ],
+        },
+        cd_1 => {
+            title => 'CD3',
+            year => 2008,
+            artist => {
+                name => 'Artist 3',
+            }
+        }
+    }
+    );
+
+isa_ok($thing->artist_1, 'DBICTest::Schema::Artist', 'created might_have artist');
+is($thing->artist_1->name, 'Crazy Frog');
+isa_ok($thing->artist_1->cds, 'DBIx::Class::ResultSet', 'created artists cds');
+is($thing->artist_1->cds->count, 2, 'created two cds for artist');
index 7921158..0b0db50 100644 (file)
@@ -2,20 +2,39 @@ use strict;
 use warnings;  
 
 use Test::More;
+use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-plan tests => 9;
+plan tests => 12;
 
 {
   my $cd_rc = $schema->resultset("CD")->result_class;
   
+  throws_ok {
+    $schema->resultset("Artist")
+      ->search_rs({}, {result_class => "IWillExplode"})
+  } qr/Can't locate IWillExplode/, 'nonexistant result_class exception';
+
+# to make ensure_class_loaded happy, dies on inflate
+  eval 'package IWillExplode; sub dummy {}';
+
   my $artist_rs = $schema->resultset("Artist")
     ->search_rs({}, {result_class => "IWillExplode"});
   is($artist_rs->result_class, 'IWillExplode', 'Correct artist result_class');
-  
+
+  throws_ok {
+    $artist_rs->result_class('mtfnpy')
+  } qr/Can't locate mtfnpy/,
+  'nonexistant result_access exception (from accessor)';
+
+  throws_ok {
+    $artist_rs->first
+  } qr/Can't locate object method "inflate_result" via package "IWillExplode"/,
+  'IWillExplode explodes on inflate';
+
   my $cd_rs = $artist_rs->related_resultset('cds');
   is($cd_rs->result_class, $cd_rc, 'Correct cd result_class');
 
index 149f759..82829b8 100644 (file)
@@ -29,4 +29,13 @@ __PACKAGE__->might_have(
                        },
 );
 
+# Normally this would appear as a FK constraint
+__PACKAGE__->belongs_to(
+                       'cd_3', 'DBICTest::Schema::CD', {
+                           'foreign.cdid' => 'self.cd',
+                       }, {
+                           is_foreign_key_constraint => 0,
+                       },
+);
+
 1;
index c4ff125..1db9557 100644 (file)
@@ -1,6 +1,6 @@
 -- 
 -- Created by SQL::Translator::Producer::SQLite
--- Created on Sun Nov  9 16:28:17 2008
+-- Created on Mon Nov 10 23:52:55 2008
 -- 
 BEGIN TRANSACTION;
 
@@ -190,6 +190,7 @@ CREATE TABLE liner_notes (
   notes varchar(100) NOT NULL
 );
 
+CREATE INDEX liner_notes_idx_liner_id_liner ON liner_notes (liner_id);
 
 --
 -- Table: link