Merge 'subquery' into 'count_distinct'
Peter Rabbitson [Wed, 6 May 2009 15:40:31 +0000 (15:40 +0000)]
Add subquery/from test by michaelr (copied from subquery branch r5742)

123 files changed:
.gitignore
Changes
Features_09
MANIFEST.SKIP
Makefile.PL
lib/DBIx/Class.pm
lib/DBIx/Class/CDBICompat/AbstractSearch.pm
lib/DBIx/Class/CDBICompat/ColumnGroups.pm
lib/DBIx/Class/CDBICompat/ColumnsAsHash.pm
lib/DBIx/Class/CDBICompat/Copy.pm
lib/DBIx/Class/CDBICompat/Iterator.pm
lib/DBIx/Class/CDBICompat/NoObjectIndex.pm
lib/DBIx/Class/CDBICompat/Relationship.pm
lib/DBIx/Class/CDBICompat/Relationships.pm
lib/DBIx/Class/CDBICompat/Retrieve.pm
lib/DBIx/Class/Componentised.pm
lib/DBIx/Class/DB.pm
lib/DBIx/Class/InflateColumn/DateTime.pm
lib/DBIx/Class/Manual/Cookbook.pod
lib/DBIx/Class/Manual/FAQ.pod
lib/DBIx/Class/Manual/Glossary.pod
lib/DBIx/Class/Manual/Reading.pod
lib/DBIx/Class/Manual/Troubleshooting.pod
lib/DBIx/Class/Ordered.pm
lib/DBIx/Class/Relationship/Accessor.pm
lib/DBIx/Class/Relationship/Base.pm
lib/DBIx/Class/Relationship/BelongsTo.pm
lib/DBIx/Class/ResultClass/HashRefInflator.pm
lib/DBIx/Class/ResultSet.pm
lib/DBIx/Class/ResultSetColumn.pm
lib/DBIx/Class/ResultSource.pm
lib/DBIx/Class/ResultSource/View.pm
lib/DBIx/Class/ResultSourceHandle.pm
lib/DBIx/Class/Row.pm
lib/DBIx/Class/Schema.pm
lib/DBIx/Class/Schema/Versioned.pm
lib/DBIx/Class/Storage.pm
lib/DBIx/Class/Storage/DBI.pm
lib/DBIx/Class/Storage/DBI/MultiDistinctEmulation.pm [deleted file]
lib/DBIx/Class/Storage/DBI/NoBindVars.pm
lib/DBIx/Class/Storage/DBI/ODBC/ACCESS.pm
lib/DBIx/Class/Storage/DBI/Oracle/Generic.pm
lib/DBIx/Class/Storage/DBI/Replicated.pm
lib/DBIx/Class/Storage/DBI/SQLite.pm
lib/DBIx/Class/Storage/TxnScopeGuard.pm
lib/SQL/Translator/Parser/DBIx/Class.pm
t/03podcoverage.t
t/04dont_break_c3.t
t/100extra_source.t
t/19quotes.t
t/19quotes_newstyle.t
t/33storage_reconnect.t
t/39load_namespaces_rt41083.t
t/47bind_attribute.t
t/54taint.t
t/60core.t
t/63register_class.t
t/63register_source.t [new file with mode: 0644]
t/64db.t
t/66relationship.t
t/68inflate_resultclass_hashrefinflator.t
t/71mysql.t
t/72pg.t
t/73oracle.t
t/74mssql.t
t/76select.t
t/77prefetch.t [deleted file]
t/80unique.t
t/81transactions.t
t/87ordered.t
t/89dbicadmin.t
t/89inflate_datetime.t
t/91debug.t
t/93storage_replication.t
t/95sql_maker.t
t/95sql_maker_quote.t
t/98rows_prefetch.t [deleted file]
t/99dbic_sqlt_parser.t
t/cdbi/01-columns.t
t/cdbi/02-Film.t
t/cdbi/15-accessor.t
t/cdbi/22-deflate_order.t
t/cdbi/columns_dont_override_custom_accessors.t
t/cdbi/copy.t
t/cdbi/multi_column_set.t
t/cdbi/set_to_undef.t
t/cdbi/set_vs_DateTime.t
t/cdbi/testlib/Actor.pm
t/cdbi/testlib/ActorAlias.pm
t/cdbi/testlib/Blurb.pm
t/cdbi/testlib/CDBase.pm
t/cdbi/testlib/DBIC/Test/SQLite.pm [moved from lib/DBIx/Class/Test/SQLite.pm with 97% similarity]
t/cdbi/testlib/Director.pm
t/cdbi/testlib/Film.pm
t/cdbi/testlib/Lazy.pm
t/cdbi/testlib/MyBase.pm
t/cdbi/testlib/Order.pm
t/cdbi/testlib/OtherThing.pm
t/cdbi/testlib/Thing.pm
t/count/count_distinct.t [new file with mode: 0644]
t/count/count_joined.t [new file with mode: 0644]
t/from_subquery.t [new file with mode: 0644]
t/lib/DBICNGTest/Schema.pm [deleted file]
t/lib/DBICNGTest/Schema/Result.pm [deleted file]
t/lib/DBICNGTest/Schema/Result/FriendList.pm [deleted file]
t/lib/DBICNGTest/Schema/Result/Gender.pm [deleted file]
t/lib/DBICNGTest/Schema/Result/Person.pm [deleted file]
t/lib/DBICNGTest/Schema/ResultSet.pm [deleted file]
t/lib/DBICNGTest/Schema/ResultSet/Person.pm [deleted file]
t/lib/DBICTest/Schema/CD.pm
t/lib/DBICTest/Schema/EventTZ.pm
t/lib/DBICTest/Schema/EventTZDeprecated.pm [new file with mode: 0644]
t/lib/DBICTest/Taint/Classes/Auto.pm [new file with mode: 0644]
t/lib/DBICTest/Taint/Classes/Manual.pm [new file with mode: 0644]
t/lib/DBICTest/Taint/Namespaces/Result/Test.pm [new file with mode: 0644]
t/lib/sqlite.sql
t/prefetch/attrs_untouched.t [new file with mode: 0644]
t/prefetch/multiple_hasmany.t [new file with mode: 0644]
t/prefetch/pollute_already_joined.t [new file with mode: 0644]
t/prefetch/rows_bug.t [new file with mode: 0644]
t/prefetch/standard.t [new file with mode: 0644]
t/resultset/as_query.t
t/search/subquery.t

index 4d98a49..ebae942 100644 (file)
@@ -1,6 +1,7 @@
 Build
 Build.bat
 MANIFEST
+MANIFEST.bak
 META.yml
 Makefile
 Makefile.old
diff --git a/Changes b/Changes
index ebe5ca7..13bb6d8 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,4 +1,35 @@
 Revision history for DBIx::Class
+
+0.08102 2009-04-30 08:29:00 (UTC)
+        - Fixed two subtle bugs when using columns or select/as
+          paired with a join (limited prefetch)
+        - Fixed breakage of cdbi tests (RT#45551)
+        - Some POD improvements
+
+0.08101 2009-04-27 09:45:00 (UTC)
+        - Fix +select, +as, +columns and include_columns being stripped
+          by $rs->get_column
+        - move load_optional_class from DBIx::Class::Componentised to
+          Class::C3::Componentised, bump dependency
+        - register_extra_source() now *really* fixed wrt subclassing
+        - Added missing POD descriptions (RT#45195)
+        - Fix insert() to not store_column() every present object column
+        - Multiple Makefile.PL fixes
+
+0.08100 2009-04-19 11:39:35 (UTC)
+        - Todo out the register_extra_source test until after shipping
+
+0.08099_08 2009-03-30 00:00:00 (UTC)
+        - Fixed taint mode with load_namespaces
+        - Putting IC::DateTime locale, timezone or floating_tz_ok attributes into
+          extra => {} has been deprecated. The new way is to put these things
+          directly into the columns definition
+        - Switched MI code to MRO::Compat
+        - Document db-side default_value caveats
+        - Search_like() now warns to indicate deprecation in 0.09.
+        - TxnScopeGuard left experimental state
+
+0.08099_07 2009-02-27 02:00:00 (UTC)
         - multi-create using find_or_create rather than _related for post-insert
         - fix get_inflated_columns to check has_column_loaded
         - Add DBIC_MULTICREATE_DEBUG env var (undocumented, quasi-internal)
@@ -7,13 +38,17 @@ Revision history for DBIx::Class
           - not try and insert things tagged on via new_related unless required
         - Possible to set locale in IC::DateTime extra => {} config
         - Calling the accessor of a belongs_to when the foreign_key
-          was NULL and the row was not stored would unexpectedly fail (groditi)
+          was NULL and the row was not stored would unexpectedly fail 
         - Split sql statements for deploy only if SQLT::Producer returned a scalar
           containing all statements to be executed
         - Add as_query() for ResultSet and ResultSetColumn. This makes subqueries
-          possible. See the Cookbook for details. (robkinyon, michaelr)
+          possible. See the Cookbook for details.
         - Massive rewrite of Ordered to properly handle position constraints and
           to make it more matpath-friendly
+        - deploy_statements called ddl_filename with the $version and $dir arguments
+          in the wrong order.
+        - columns/+columns attributes now support { as => select } hahsrefs
+        - support for views both in DBIC and via deploy() in SQLT
 
 0.08099_06 2009-01-23 07:30:00 (UTC)
         - Allow a scalarref to be supplied to the 'from' resultset attribute
@@ -28,19 +63,18 @@ Revision history for DBIx::Class
           (http://msdn.microsoft.com/en-us/library/ms190315.aspx)
         - an sqlt_deploy_hook can now be shared between result sources using
           a configurable callback trigger
-        - new order_by => { -desc => 'colname' } syntax supported with
-          SQLA >= 1.50
-        - PG array datatype supported with SQLA >= 1.50
+        - new order_by => { -desc => 'colname' } syntax supported
+        - PG array datatype supported
         - insert should use store_column, not set_column to avoid marking
-          clean just-stored values as dirty. New test for this (groditi)
-        - regression test for source_name (groditi)
+          clean just-stored values as dirty. New test for this 
+        - regression test for source_name 
 
 0.08099_05 2008-10-30 21:30:00 (UTC)
-        - Rewritte of Storage::DBI::connect_info(), extended with an
+        - Rewrite of Storage::DBI::connect_info(), extended with an
           additional argument format type
         - InflateColumn::DateTime: add warning about floating timezone
         - InflateColumn::DateTime: possible to enforce/skip inflation
-        - delete throws exception if passed arguments to prevent drunken mishaps. (purge)
+        - delete throws exception if passed arguments to prevent drunken mishaps.
         - Fix storage to copy scalar conds before regexping to avoid
           trying to modify a constant in odd edge cases
         - Related resultsets on uninserted objects are now empty
index ba0e15f..24a6aa2 100644 (file)
@@ -1,23 +1,17 @@
 (Potential) Features for 0.09
 =============================
 
-Subselects - everyone wants these
- - Will require new/improved SQL::Abstract
- - The API will involve passing ResultSets to searches as conditions/values/whatever.
-
 Row/find caching - would be pretty useful
  - Need to have good definitions of when the cache should be queried and when invalidated
  - Be able to supply own expiry?
  - Be able to invalidate manually? Single item / entire cache / single table
 
-Deprecate compose_connection / DB.pm
+Remove compose_connection / DB.pm
  - Everyone has probably forgotten what this is anyway..
 
 Syntax improvements?
  - "as" to "alias" ?
  - "belongs_to" to "contains/refers/something"
- - order_by to take a arrayref/hashref so it also just works with quoting
- - ??
 
 Using inflated objects/references as values in searches
  - Goes together with subselects above
@@ -52,15 +46,10 @@ Storage API/restructure
 Relationships
  - single vs filter, discrepancies.. remove one of them and make behave the same?
 
-SQL::Abstract et al
- - be able to extract/view/dump what the SQL is *going to* be before running it
- - bind params sanity? (order of bind params broken when using a resultsource based on a select statement)
- - etc
 Joining/searching weird attribute tables?
  - Support legacy/badly designed junk better..
 
 Documentation - improvements
  - better indexing for finding of stuff in general
  - more cross-referencing of docs
+
index 07a5968..1e9b295 100644 (file)
@@ -1,3 +1,6 @@
+^(?!script/|examples/|lib/|inc/|t/|Makefile.PL$|README$|MANIFEST$|Changes$|META.yml$)
+
+
 # Avoid version control files.
 \bRCS\b
 \bCVS\b
index 2af68e3..bd6963a 100644 (file)
@@ -1,4 +1,4 @@
-use inc::Module::Install 0.67;
+use inc::Module::Install 0.79;
 use strict;
 use warnings;
 use POSIX ();
@@ -9,30 +9,32 @@ name     'DBIx-Class';
 perl_version '5.006001';
 all_from 'lib/DBIx/Class.pm';
 
+# configure_requires so _check_sqlite() below can run
+# remove once test deprecated
+configure_requires 'DBD::SQLite';
+
+requires 'DBD::SQLite'              => 1.23;
 requires 'Data::Page'               => 2.00;
 requires 'Scalar::Util'             => 0;
-requires 'SQL::Abstract'            => 1.49;
+requires 'SQL::Abstract'            => 1.53;
 requires 'SQL::Abstract::Limit'     => 0.13;
-requires 'Class::C3'                => 0.20;
-requires 'Class::C3::Componentised' => 0;
+requires 'Class::C3::Componentised' => 1.0005;
 requires 'Storable'                 => 0;
 requires 'Carp::Clan'               => 0;
 requires 'DBI'                      => 1.40;
 requires 'Module::Find'             => 0;
 requires 'Class::Inspector'         => 0;
-requires 'Class::Accessor::Grouped' => 0.08002;
+requires 'Class::Accessor::Grouped' => 0.08003;
 requires 'JSON::Any'                => 1.17;
 requires 'Scope::Guard'             => 0.03;
 requires 'Path::Class'              => 0;
 requires 'List::Util'               => 1.19;
 requires 'Sub::Name'                => 0.04;
-requires 'namespace::clean'         => 0.09;
+requires 'MRO::Compat'              => 0.09;
 
 # Perl 5.8.0 doesn't have utf8::is_utf8()
 requires 'Encode'                   => 0 if ($] <= 5.008000);  
 
-configure_requires 'DBD::SQLite'    => 1.14;
-
 test_requires 'Test::Builder'       => 0.33;
 test_requires 'Test::Warn'          => 0.11;
 test_requires 'Test::Exception'     => 0;
@@ -40,26 +42,54 @@ test_requires 'Test::Deep'          => 0;
 
 recommends 'SQL::Translator'        => 0.09004;
 
-install_script 'script/dbicadmin';
+install_script (qw|
+    script/dbicadmin
+|);
 
-tests_recursive 't';
+tests_recursive (qw|
+    t
+|);
 
-# re-build README and require CDBI modules for testing if we're in a checkout
+# re-build README and require extra modules for testing if we're in a checkout
 
 my %force_requires_if_author = (
+  'Test::Pod::Coverage'       => 1.04,
+  'SQL::Translator'           => 0.09004,
+
+  # CDBI-compat related
   'DBIx::ContextualFetch'     => 0,
+  'Class::DBI::Plugin::DeepAbstractSearch' => 0,
   'Class::Trigger'            => 0,
-  'Time::Piece'               => 0,
+  'Time::Piece::MySQL'        => 0,
   'Clone'                     => 0,
-  'Test::Pod::Coverage'       => 0,
+  'Date::Simple'              => 0,
+
+  # t/52cycle.t
   'Test::Memory::Cycle'       => 0,
-  'SQL::Translator'           => 0.09004,
+
+  # t/60core.t
+  'DateTime::Format::MySQL'   => 0,
+
+  # t/72pg.t
+  $ENV{DBICTEST_PG_DSN}
+    ? ('Sys::SigAction'=> 0)
+    : ()
+  ,
+
+  # t/93storage_replication.t
+  'Moose',                        => 0.54,
+  'Moose::Util::TypeConstraints'  => 0.54,
+  'MooseX::AttributeHelpers'      => 0.12,
+  'Class::MOP'                    => 0.63,
+
+  # t/96_is_deteministic_value.t
+  'DateTime::Format::Strptime' => 0,
 );
 
 if ($Module::Install::AUTHOR) {
 
   foreach my $module (keys %force_requires_if_author) {
-    requires ($module => $force_requires_if_author{$module});
+    build_requires ($module => $force_requires_if_author{$module});
   }
 
   system('pod2text lib/DBIx/Class.pm > README');
@@ -67,62 +97,65 @@ if ($Module::Install::AUTHOR) {
 
 auto_provides;
 
+if ($Module::Install::AUTHOR) {
+  warn <<'EOW';
+******************************************************************************
+******************************************************************************
+***                                                                        ***
+*** AUTHOR MODE: all optional test dependencies converted to hard requires ***
+***                                                                        ***
+******************************************************************************
+******************************************************************************
+
+EOW
+}
 auto_install;
 
 # Have all prerequisites, check DBD::SQLite sanity
-if (! $ENV{DBICTEST_NO_SQLITE_CHECK} ) {
+_check_sqlite() if (! $ENV{DBICTEST_NO_SQLITE_CHECK} );
 
-  my $pid = fork();
-  if (not defined $pid) {
-      die "Unable to fork(): $!";
-  }
-  elsif (! $pid) {
+WriteAll();
 
-      # Win32 does not have real fork()s so a segfault will bring
-      # everything down. Warn about it.
-      if ($^O eq 'MSWin32') {
-        print <<'EOW';
+if ($Module::Install::AUTHOR) {
+  # Need to do this _after_ WriteAll else it loses track of them
+  Meta->{values}{build_requires} = [ grep {
+    my $ok = 1;
+    foreach my $module (keys %force_requires_if_author) {
+      if ($_->[0] =~ /$module/) {
+        $ok = 0;
+        last;
+      }
+    }
+    $ok;
+  } @{Meta->{values}{build_requires}} ];
 
-######################################################################
-#                                                                    #
-# A short stress-testing of DBD::SQLite will follow. If you have a   #
-# buggy library this might very well be the last text you will see   #
-# before the installation silently terminates. If this happens it    #
-# would mean that you are running a buggy version of DBD::SQLite     #
-# known to randomly segfault on errors. Even if you have the latest  #
-# CPAN module version, the system sqlite3 dynamic library might have #
-# been compiled against an older buggy sqlite3 dev library (oddly    #
-# DBD::SQLite will prefer the system library against the one bundled #
-# with it). You are strongly advised to resolve this issue before    #
-# proceeding.                                                        #
-#                                                                    #
-# If this happens to you (this text is the last thing you see), and  #
-# you just want to install this module without worrying about the    #
-# tests (which will almost certainly fail) - set the environment     #
-# variable DBICTEST_NO_SQLITE_CHECK to a true value and try again.   #
-#                                                                    #
-######################################################################
+  Meta->{values}{resources} = [ 
+    [ 'MailingList', 'http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/dbix-class' ],
+    [ 'IRC', 'irc://irc.perl.org/#dbix-class' ],
+    [ 'license', 'http://dev.perl.org/licenses/' ],
+    [ 'repository', 'http://dev.catalyst.perl.org/svnweb/bast/browse/DBIx-Class/' ],
+  ];
+  Meta->write;
+}
 
-EOW
-      }
 
-      require DBI;
-      for (1 .. 100) {
-          my $dbh;
-          $dbh = DBI->connect ('dbi:SQLite::memory:', undef, undef, {
-              AutoCommit => 1,
-              RaiseError => 0,
-              PrintError => 0,
-          })
-              or die "Unable to connect to database: $@";
-          $dbh->do ('CREATE TABLE name_with_no_columns');   # a subtle syntax error
-          $dbh->do ('COMMIT');                              # followed by commit
-          $dbh->disconnect;
-      }
+# This is legacy code. Latest DBD::SQLite developments fixed all known bugs
+# in this area. Remove before some arbitrary next version
+sub _check_sqlite {
 
+  # Win32 does not have real fork()s so a segfault will bring
+  # everything down. Warn about it below, and don't try fork()
+  if ($^O ne 'MSWin32') {
+
+    my $pid = fork();
+    if (not defined $pid) {
+        die "Unable to fork(): $!";
+    }
+    elsif (! $pid) {
+      _torture_sqlite();
       exit 0;
-  }
-  else {
+    }
+    else {
       eval {
           local $SIG{ALRM} = sub { die "timeout\n" };
           alarm 5;
@@ -133,7 +166,7 @@ EOW
 
       my $sig = $? & 127;
 
-# make sure process actually dies
+      # make sure process actually dies
       $exception && kill POSIX::SIGKILL(), $pid;
 
       if ($exception || $sig == POSIX::SIGSEGV() || $sig == POSIX::SIGABRT()
@@ -159,39 +192,50 @@ EOE
           );
           exit 0 unless ($ans =~ /^y(es)?$/i);
       }
+    }
   }
-}
 
+  else {  # the win32 version
 
-WriteAll();
+    print <<'EOW';
+######################################################################
+#                                                                    #
+# A short stress-testing of DBD::SQLite will follow. If you have a   #
+# buggy library this might very well be the last text you will see   #
+# before the installation silently terminates. If this happens it    #
+# would mean that you are running a buggy version of DBD::SQLite     #
+# known to randomly segfault on errors. Even if you have the latest  #
+# CPAN module version, the system sqlite3 dynamic library might have #
+# been compiled against an older buggy sqlite3 dev library (oddly    #
+# DBD::SQLite will prefer the system library against the one bundled #
+# with it). You are strongly advised to resolve this issue before    #
+# proceeding.                                                        #
+#                                                                    #
+# If this happens to you (this text is the last thing you see), and  #
+# you just want to install this module without worrying about the    #
+# tests (which will almost certainly fail) - set the environment     #
+# variable DBICTEST_NO_SQLITE_CHECK to a true value and try again.   #
+#                                                                    #
+######################################################################
 
+EOW
 
-if ($Module::Install::AUTHOR) {
-  # Need to do this _after_ WriteAll else it looses track of them
-  Meta->{values}{build_requires} = [ grep {
-    my $ok = 1;
-    foreach my $module (keys %force_requires_if_author) {
-      if ($_->[0] =~ /$module/) {
-        $ok = 0;
-        last;
-      }
-    }
-    $ok;
-  } @{Meta->{values}{build_requires}} ];
+    _torture_sqlite();
+  }
+}
 
-  my @scalar_keys = Module::Install::Metadata::Meta_TupleKeys();
-  my $cr = Module::Install::Metadata->can("Meta_TupleKeys");
-  {
-    no warnings 'redefine';
-    *Module::Install::Metadata::Meta_TupleKeys = sub {
-      return $cr->(@_), 'resources';
-    };
+sub _torture_sqlite {
+  require DBI;
+
+  for (1 .. 100) {
+    my $dbh = DBI->connect ('dbi:SQLite::memory:', undef, undef, {
+      AutoCommit => 1,
+      RaiseError => 0,
+      PrintError => 0,
+    }) or die "Unable to connect to database: $@";
+
+    $dbh->do ('CREATE TABLE name_with_no_columns');   # a subtle syntax error
+    $dbh->do ('COMMIT');                              # followed by commit
+    $dbh->disconnect;
   }
-  Meta->{values}{resources} = [ 
-    [ 'MailingList', 'http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/dbix-class' ],
-    [ 'IRC', 'irc://irc.perl.org/#dbix-class' ],
-    [ 'license', 'http://dev.perl.org/licenses/' ],
-    [ 'repository', 'http://dev.catalyst.perl.org/svnweb/bast/browse/DBIx-Class/' ],
-  ];
-  Meta->write;
 }
index 6ea8116..32b58a1 100644 (file)
@@ -24,7 +24,7 @@ sub component_base_class { 'DBIx::Class' }
 # i.e. first release of 0.XX *must* be 0.XX000. This avoids fBSD ports
 # brain damage and presumably various other packaging systems too
 
-$VERSION = '0.08099_06';
+$VERSION = '0.08102';
 
 $VERSION = eval $VERSION; # numify for warning-free dev releases
 
@@ -207,6 +207,8 @@ andyg: Andy Grundman <andy@hybridized.org>
 
 ank: Andres Kievsky
 
+arcanez: Justin Hunter <justin.d.hunter@gmail.com>
+
 ash: Ash Berlin <ash@cpan.org>
 
 bert: Norbert Csongradi <bert@cpan.org>
@@ -219,8 +221,6 @@ bricas: Brian Cassidy <bricas@cpan.org>
 
 caelum: Rafael Kitover <rkitover@cpan.org>
 
-captainL: Luke Saunders <luke.saunders@gmail.com>
-
 castaway: Jess Robinson
 
 claco: Christopher H. Laco
@@ -243,6 +243,10 @@ gphat: Cory G Watson <gphat@cpan.org>
 
 groditi: Guillermo Roditi <groditi@cpan.org>
 
+ilmari: Dagfinn Ilmari MannsE<aring>ker <ilmari@ilmari.org>
+
+jasonmay: Jason May <jason.a.may@gmail.com>
+
 jesper: Jesper Krogh
 
 jgoulah: John Goulah <jgoulah@cpan.org>
@@ -257,16 +261,22 @@ jshirley: J. Shirley <jshirley@gmail.com>
 
 konobi: Scott McWhirter
 
+lukes: Luke Saunders <luke.saunders@gmail.com>
+
 marcus: Marcus Ramberg <mramberg@cpan.org>
 
 mattlaw: Matt Lawrence
 
+michaelr: Michael Reddick <michael.reddick@gmail.com>
+
 ned: Neil de Carteret
 
 nigel: Nigel Metheringham <nigelm@cpan.org>
 
 ningu: David Kamholz <dkamholz@cpan.org>
 
+norbi: Norbert Buchmuller <norbi@nix.hu>
+
 Numa: Dan Sully <daniel@cpan.org>
 
 oyse: Ã˜ystein Torget <oystein.torget@dnv.com>
@@ -277,6 +287,8 @@ penguin: K J Cheetham
 
 perigrin: Chris Prather <chris@prather.org>
 
+peter: Peter Collingbourne <peter@pcc.me.uk>
+
 phaylon: Robert Sedlacek <phaylon@dunkelheit.at>
 
 plu: Johannes Plunien <plu@cpan.org>
@@ -299,6 +311,8 @@ scotty: Scotty Allen <scotty@scottyallen.com>
 
 semifor: Marc Mims <marc@questright.com>
 
+solomon: Jared Johnson <jaredj@nmgi.com>
+
 sszabo: Stephan Szabo <sszabo@bigpanda.com>
 
 teejay : Aaron Trevena <teejay@cpan.org>
@@ -315,9 +329,9 @@ wdh: Will Hawes
 
 willert: Sebastian Willert <willert@cpan.org>
 
-zamolxes: Bogdan Lucaciu <bogdan@wiz.ro>
+wreis: Wallace Reis <wreis@cpan.org>
 
-norbi: Norbert Buchmuller <norbi@nix.hu>
+zamolxes: Bogdan Lucaciu <bogdan@wiz.ro>
 
 =head1 LICENSE
 
index 948dcd9..c02f4f7 100644 (file)
@@ -6,7 +6,7 @@ use warnings;
 
 =head1 NAME
 
-DBIx::Class::CDBICompat::AbstractSearch
+DBIx::Class::CDBICompat::AbstractSearch - Emulates Class::DBI::AbstractSearch
 
 =head1 SYNOPSIS
 
index 2dcd878..cbc1124 100644 (file)
@@ -155,7 +155,8 @@ sub _find_columns {
   return map { $class->find_column($_) } @col;
 }
 
-package DBIx::Class::CDBICompat::ColumnGroups::GrouperShim;
+package # hide from PAUSE (should be harmless, no POD no Version)
+    DBIx::Class::CDBICompat::ColumnGroups::GrouperShim;
 
 sub groups_for {
   my ($self, @cols) = @_;
@@ -167,6 +168,5 @@ sub groups_for {
   }
   return keys %groups;
 }
-    
 
 1;
index b5f1168..7b81f09 100644 (file)
@@ -7,7 +7,7 @@ use warnings;
 
 =head1 NAME
 
-DBIx::Class::CDBICompat::ColumnsAsHash
+DBIx::Class::CDBICompat::ColumnsAsHash - Emulates the behavior of Class::DBI where the object can be accessed as a hash of columns.
 
 =head1 SYNOPSIS
 
index 414cbd6..ed42d95 100644 (file)
@@ -8,7 +8,7 @@ use Carp;
 
 =head1 NAME
 
-DBIx::Class::CDBICompat::Copy
+DBIx::Class::CDBICompat::Copy - Emulates Class::DBI->copy($new_id)
 
 =head1 SYNOPSIS
 
index 3466769..80e788c 100644 (file)
@@ -6,7 +6,7 @@ use warnings;
 
 =head1 NAME
 
-DBIx::Class::CDBICompat::Iterator
+DBIx::Class::CDBICompat::Iterator - Emulates the extra behaviors of the Class::DBI search iterator.
 
 =head1 SYNOPSIS
 
index 003c875..5dd6268 100644 (file)
@@ -6,7 +6,7 @@ use warnings;
 
 =head1 NAME
 
-DBIx::Class::CDBICompat::NoObjectIndex
+DBIx::Class::CDBICompat::NoObjectIndex - Defines empty methods for object indexing. They do nothing
 
 =head1 SYNOPSIS
 
index 880ffc2..5d71924 100644 (file)
@@ -7,7 +7,7 @@ use Sub::Name ();
 
 =head1 NAME
 
-DBIx::Class::CDBICompat::Relationship
+DBIx::Class::CDBICompat::Relationship - Emulate the Class::DBI::Relationship object returned from meta_info()
 
 =head1 DESCRIPTION
 
index d6120bc..7572870 100644 (file)
@@ -14,7 +14,7 @@ __PACKAGE__->mk_classdata('__meta_info' => {});
 
 =head1 NAME
 
-DBIx::Class::CDBICompat::Relationships
+DBIx::Class::CDBICompat::Relationships - Emulate has_a(), has_many(), might_have() and meta_info()
 
 =head1 DESCRIPTION
 
index 4c36887..e701cfc 100644 (file)
@@ -50,11 +50,24 @@ sub retrieve_from_sql {
 
   $cond =~ s/^\s*WHERE//i;
 
-  if( $cond =~ s/\bLIMIT (\d+)\s*$//i ) {
-      push @rest, { rows => $1 };
+  # Need to parse the SQL clauses after WHERE in reverse
+  # order of appearance.
+
+  my %attrs;
+
+  if( $cond =~ s/\bLIMIT\s+(\d+)\s*$//i ) {
+      $attrs{rows} = $1;
+  }
+
+  if ( $cond =~ s/\bORDER\s+BY\s+(.*)\s*$//i ) {
+      $attrs{order_by} = $1;
+  }
+
+  if( $cond =~ s/\bGROUP\s+BY\s+(.*)\s*$//i ) {
+      $attrs{group_by} = $1;
   }
 
-  return $class->search_literal($cond, @rest);
+  return $class->search_literal($cond, @rest, ( %attrs ? \%attrs : () ) );
 }
 
 sub construct {
index a438c06..cc6c8c0 100644 (file)
@@ -31,25 +31,4 @@ sub inject_base {
   $class->next::method($target, @to_inject);
 }
 
-# Returns a true value if the specified class is installed and loaded
-# successfully, throws an exception if the class is found but not loaded
-# successfully, and false if the class is not installed
-sub load_optional_class {
-  my ($class, $f_class) = @_;
-  eval { $class->ensure_class_loaded($f_class) };
-  my $err = $@;   # so we don't lose it
-  if (! $err) {
-    return 1;
-  }
-  else {
-    my $fn = (join ('/', split ('::', $f_class) ) ) . '.pm';
-    if ($err =~ /Can't locate ${fn} in \@INC/ ) {
-      return 0;
-    }
-    else {
-      die $err;
-    }
-  }
-}
-
 1;
index 1f1ffd8..57381d0 100644 (file)
@@ -162,11 +162,12 @@ __PACKAGE__->mk_classdata('_result_source_instance' => []);
 sub _maybe_attach_source_to_schema {
   my ($class, $source) = @_;
   if (my $meth = $class->can('schema_instance')) {
-    my $schema = $class->$meth;
-    $schema->register_class($class, $class);
-    my $new_source = $schema->source($class);
-    %$source = %$new_source;
-    $schema->source_registrations->{$class} = $source;
+    if (my $schema = $class->$meth) {
+      $schema->register_class($class, $class);
+      my $new_source = $schema->source($class);
+      %$source = %$new_source;
+      $schema->source_registrations->{$class} = $source;
+    }
   }
 }
 
index 3024241..afd118d 100644 (file)
@@ -30,7 +30,7 @@ Then you can treat the specified column as a L<DateTime> object.
 If you want to set a specific timezone and locale for that field, use:
 
   __PACKAGE__->add_columns(
-    starts_when => { data_type => 'datetime', extra => { timezone => "America/Chicago", locale => "de_DE" } }
+    starts_when => { data_type => 'datetime', timezone => "America/Chicago", locale => "de_DE" }
   );
 
 If you want to inflate no matter what data_type your column is,
@@ -111,13 +111,20 @@ sub register_column {
 
   my $timezone;
   if ( defined $info->{extra}{timezone} ) {
+    warn "Putting timezone into extra => { timezone => '...' } has been deprecated, ".
+         "please put it directly into the columns definition.";
     $timezone = $info->{extra}{timezone};
   }
 
   my $locale;
   if ( defined $info->{extra}{locale} ) {
+    warn "Putting locale into extra => { locale => '...' } has been deprecated, ".
+         "please put it directly into the columns definition.";
     $locale = $info->{extra}{locale};
   }
+  
+  $locale   = $info->{locale}   if defined $info->{locale};
+  $timezone = $info->{timezone} if defined $info->{timezone};
 
   my $undef_if_invalid = $info->{datetime_undef_if_invalid};
 
@@ -137,7 +144,13 @@ sub register_column {
     #     closure &G, $info => $H
     #     $H => %E
     #
-    my $floating_tz_ok = $info->{extra}{floating_tz_ok};
+    my $floating_tz_ok;
+    if (defined $info->{extra}{floating_tz_ok}) {
+      warn "Putting floating_tz_ok into extra => { floating_tz_ok => 1 } has been deprecated, ".
+           "please put it directly into the columns definition.";
+      $floating_tz_ok = $info->{extra}{floating_tz_ok};
+    }
+    $floating_tz_ok = $info->{floating_tz_ok} if defined $info->{floating_tz_ok};
 
     $self->inflate_column(
       $column =>
@@ -189,7 +202,7 @@ timezone, you will get a warning (as there is a very good chance this will not h
 result you expect). For example:
 
   __PACKAGE__->add_columns(
-    starts_when => { data_type => 'datetime', extra => { timezone => "America/Chicago" } }
+    starts_when => { data_type => 'datetime', timezone => "America/Chicago" }
   );
 
   my $event = $schema->resultset('EventTZ')->create({
@@ -213,7 +226,7 @@ to be supply explicit times to the database:
 =item Suppress the check on per-column basis
 
   __PACKAGE__->add_columns(
-    starts_when => { data_type => 'datetime', extra => { timezone => "America/Chicago", floating_tz_ok => 1 } }
+    starts_when => { data_type => 'datetime', timezone => "America/Chicago", floating_tz_ok => 1 }
   );
 
 =item Suppress the check globally
@@ -222,7 +235,10 @@ Set the environment variable DBIC_FLOATING_TZ_OK to some true value.
 
 =back
 
-
+Putting extra attributes like timezone, locale or floating_tz_ok into extra => {} has been
+B<DEPRECATED> because this gets you into trouble using L<DBIx::Class::Schema::Versioned>.
+Instead put it directly into the columns definition like in the examples above. If you still
+use the old way you'll see a warning - please fix your code then!
 
 =head1 SEE ALSO
 
index 00c28e9..7d0de14 100644 (file)
@@ -19,19 +19,8 @@ paged resultset, which will fetch only a defined number of records at a time:
 
   return $rs->all(); # all records for page 1
 
-The C<page> attribute does not have to be specified in your search:
-
-  my $rs = $schema->resultset('Artist')->search(
-    undef,
-    {
-      rows => 10,
-    }
-  );
-
-  return $rs->page(1); # DBIx::Class::ResultSet containing first 10 records
-
-In either of the above cases, you can get a L<Data::Page> object for the
-resultset (suitable for use in e.g. a template) using the C<pager> method:
+You can get a L<Data::Page> object for the resultset (suitable for use
+in e.g. a template) using the C<pager> method:
 
   return $rs->pager();
 
@@ -116,7 +105,7 @@ reference (this is a feature of L<SQL::Abstract>).
 Say you want to run a complex custom query on your user data, here's what
 you have to add to your User class:
 
-  package My::Schema::User;
+  package My::Schema::Result::User;
   
   use base qw/DBIx::Class/;
   
@@ -160,10 +149,10 @@ files (instead of stuffing all of them into the same resultset class), you can
 achieve the same with subclassing the resultset class and defining the
 ResultSource there:
 
-  package My::Schema::UserFriendsComplex;
+  package My::Schema::Result::UserFriendsComplex;
 
-  use My::Schema::User;
-  use base qw/My::Schema::User/;
+  use My::Schema::Result::User;
+  use base qw/My::Schema::Result::User/;
 
   __PACKAGE__->table('dummy');  # currently must be called before anything else
 
@@ -295,7 +284,7 @@ Please see L<DBIx::Class::ResultSet/ATTRIBUTES> documentation if you
 are in any way unsure about the use of the attributes above (C< join
 >, C< select >, C< as > and C< group_by >).
 
-=head2 Subqueries
+=head2 Subqueries (EXPERIMENTAL)
 
 You can write subqueries relatively easily in DBIC.
 
@@ -343,6 +332,10 @@ That creates the following SQL:
        WHERE artistid = me.artistid
       )
 
+=head3 EXPERIMENTAL
+
+Please note that subqueries are considered an experimental feature.
+
 =head2 Predefined searches
 
 You can write your own L<DBIx::Class::ResultSet> class by inheriting from it
@@ -680,7 +673,7 @@ Employ the standard stringification technique by using the C<overload>
 module.
 
 To make an object stringify itself as a single column, use something
-like this (replace C<foo> with the column/method of your choice):
+like this (replace C<name> with the column/method of your choice):
 
   use overload '""' => sub { shift->name}, fallback => 1;
 
@@ -747,16 +740,16 @@ below:
  
 B<Schema Definition> 
  
-    package DB::Schema; 
+    package My::Schema; 
      
     use base qw/DBIx::Class::Schema/; 
  
-    __PACKAGE__->load_classes(qw/User/); 
+    __PACKAGE__->load_namespaces; 
  
  
 B<Proxy-Class definitions> 
  
-    package DB::Schema::User; 
+    package My::Schema::Result::User; 
      
     use strict; 
     use warnings; 
@@ -791,11 +784,11 @@ B<Proxy-Class definitions>
     } 
      
      
-    package DB::Schema::User::Admin; 
+    package My::Schema::Result::User::Admin; 
      
     use strict; 
     use warnings; 
-    use base qw/DB::Schema::User/; 
+    use base qw/My::Schema::Result::User/; 
      
     sub hello 
     { 
@@ -813,7 +806,7 @@ B<Test File> test.pl
  
     use warnings; 
     use strict; 
-    use DB::Schema; 
+    use My::Schema; 
      
     my $user_data = { email    => 'someguy@place.com',  
                       password => 'pass1',  
@@ -823,7 +816,7 @@ B<Test File> test.pl
                        password => 'pass2',  
                        admin    => 1 }; 
                            
-    my $schema = DB::Schema->connection('dbi:Pg:dbname=test'); 
+    my $schema = My::Schema->connection('dbi:Pg:dbname=test'); 
      
     $schema->resultset('User')->create( $user_data ); 
     $schema->resultset('User')->create( $admin_data ); 
@@ -861,11 +854,16 @@ To do this simply use L<DBIx::Class::ResultClass::HashRefInflator>.
 
 Wasn't that easy?
 
+Beware, changing the Result class using
+L<DBIx::Class::ResultSet/result_class> will replace any existing class
+completely including any special components loaded using
+load_components, eg L<DBIx::Class::InflateColumn::DateTime>.
+
 =head2 Get raw data for blindingly fast results
 
 If the L<HashRefInflator|DBIx::Class::ResultClass::HashRefInflator> solution
 above is not fast enough for you, you can use a DBIx::Class to return values
-exactly as they come out of the data base with none of the convenience methods
+exactly as they come out of the database with none of the convenience methods
 wrapped round them.
 
 This is used like so:
@@ -876,13 +874,13 @@ This is used like so:
   }
 
 You will need to map the array offsets to particular columns (you can
-use the I<select> attribute of C<search()> to force ordering).
+use the L<DBIx::Class::ResultSet/select> attribute of L<DBIx::Class::ResultSet/search> to force ordering).
 
 =head1 RESULTSET OPERATIONS
 
 =head2 Getting Schema from a ResultSet
 
-To get the schema object from a result set, do the following:
+To get the L<DBIx::Class::Schema> object from a ResultSet, do the following:
 
  $rs->result_source->schema
 
@@ -1022,6 +1020,98 @@ This is straightforward using L<ManyToMany|DBIx::Class::Relationship/many_to_man
   $rs = $user->addresses(); # get all addresses for a user
   $rs = $address->users(); # get all users for an address
 
+=head2 Relationships across DB schemas
+
+Mapping relationships across L<DB schemas|DBIx::Class::Manual::Glossary/DB schema>
+is easy as long as the schemas themselves are all accessible via the same DBI
+connection. In most cases, this means that they are on the same database host
+as each other and your connecting database user has the proper permissions to them.
+
+To accomplish this one only needs to specify the DB schema name in the table
+declaration, like so...
+
+  package MyDatabase::Main::Artist;
+  use base qw/DBIx::Class/;
+  __PACKAGE__->load_components(qw/PK::Auto Core/);
+  
+  __PACKAGE__->table('database1.artist'); # will use "database1.artist" in FROM clause
+  
+  __PACKAGE__->add_columns(qw/ artistid name /);
+  __PACKAGE__->set_primary_key('artistid');
+  __PACKAGE__->has_many('cds' => 'MyDatabase::Main::Cd');
+
+  1;
+
+Whatever string you specify there will be used to build the "FROM" clause in SQL
+queries.
+
+The big drawback to this is you now have DB schema names hardcoded in your
+class files. This becomes especially troublesome if you have multiple instances
+of your application to support a change lifecycle (e.g. DEV, TEST, PROD) and
+the DB schemas are named based on the environment (e.g. database1_dev).
+
+However, one can dynamically "map" to the proper DB schema by overriding the
+L<connection|DBIx::Class::Schama/connection> method in your Schema class and
+building a renaming facility, like so:
+
+  package MyDatabase::Schema;
+  use Moose;
+  
+  extends 'DBIx::Class::Schema';
+  
+  around connection => sub {
+    my ( $inner, $self, $dsn, $username, $pass, $attr ) = ( shift, @_ );
+   
+    my $postfix = delete $attr->{schema_name_postfix};
+    
+    $inner->(@_);
+    
+    if ( $postfix ) {
+        $self->append_db_name($postfix);
+    }
+  };
+
+  sub append_db_name {
+    my ( $self, $postfix ) = @_;
+    
+    my @sources_with_db 
+        = grep 
+            { $_->name =~ /^\w+\./mx } 
+            map 
+                { $self->source($_) } 
+                $self->sources;
+    
+    foreach my $source (@sources_with_db) {
+        my $name = $source->name;
+        $name =~ s{^(\w+)\.}{${1}${postfix}\.}mx;
+        
+        $source->name($name);
+    }
+  }
+
+  1;
+
+By overridding the L<connection|DBIx::Class::Schama/connection>
+method and extracting a custom option from the provided \%attr hashref one can
+then simply iterate over all the Schema's ResultSources, renaming them as
+needed.
+
+To use this facility, simply add or modify the \%attr hashref that is passed to 
+L<connection|DBIx::Class::Schama/connect>, as follows:
+
+  my $schema 
+    = MyDatabase::Schema->connect(
+      $dsn, 
+      $user, 
+      $pass,
+      {
+        schema_name_postfix => '_dev'
+        # ... Other options as desired ... 
+      })
+
+Obviously, one could accomplish even more advanced mapping via a hash map or a
+callback routine.
+
 =head1 TRANSACTIONS
 
 As of version 0.04001, there is improved transaction support in
@@ -1174,6 +1264,18 @@ Or use C<cursor>
   while (my @vals = $cursor->next) {
     print $vals[0]."\n";
   }
+
+In case you're going to use this "trick" together with L<DBIx::Class::Schema/deploy> or
+L<DBIx::Class::Schema/create_ddl_dir> a table called "dual" will be created in your
+current schema. This would overlap "sys.dual" and you could not fetch "sysdate" or
+"sequence.nextval" anymore from dual. To avoid this problem, just tell
+L<SQL::Translator> to not create table dual:
+
+    my $sqlt_args = {
+        add_drop_table => 1,
+        parser_args    => { sources => [ grep $_ ne 'Dual', schema->sources ] },
+    };
+    $schema->create_ddl_dir( [qw/Oracle/], undef, './sql', undef, $sqlt_args );
  
 Or use L<DBIx::Class::ResultClass::HashRefInflator>
  
@@ -1219,7 +1321,7 @@ class (refer to the advanced
 L<callback system|DBIx::Class::ResultSource/sqlt_deploy_callback> if you wish
 to share a hook between multiple sources):
 
- package My::Schema::Artist;
+ package My::Schema::Result::Artist;
 
  __PACKAGE__->table('artist');
  __PACKAGE__->add_columns(id => { ... }, name => { ... })
@@ -1381,10 +1483,10 @@ C<limit_dialect> key in the final hash as shown above.
 
 =head2 Working with PostgreSQL array types
 
-If your SQL::Abstract version (>= 1.50) supports it, you can assign to
-PostgreSQL array values by passing array references in the C<\%columns>
-(C<\%vals>) hashref of the L<DBIx::Class::ResultSet/create> and
-L<DBIx::Class::Row/update> family of methods:
+You can also assign values to PostgreSQL array columns by passing array
+references in the C<\%columns> (C<\%vals>) hashref of the
+L<DBIx::Class::ResultSet/create> and L<DBIx::Class::Row/update> family of
+methods:
 
   $resultset->create({
     numbers => [1, 2, 3]
@@ -1733,4 +1835,33 @@ If you are instead using the L<load_namespaces|DBIx::Class::Schema/load_namespac
 syntax to load the appropriate classes there is not a direct alternative
 avoiding L<Module::Find|Module::Find>.
 
+=head1 MEMORY USAGE
+
+=head2 Cached statements
+
+L<DBIx::Class> normally caches all statements with L<< prepare_cached()|DBI/prepare_cached >>.
+This is normally a good idea, but if too many statements are cached, the database may use too much
+memory and may eventually run out and fail entirely.  If you suspect this may be the case, you may want
+to examine DBI's L<< CachedKids|DBI/CachedKidsCachedKids_(hash_ref) >> hash:
+
+    # print all currently cached prepared statements
+    print for keys %{$schema->storage->dbh->{CachedKids}};
+    # get a count of currently cached prepared statements
+    my $count = scalar keys %{$schema->storage->dbh->{CachedKids}};
+
+If it's appropriate, you can simply clear these statements, automatically deallocating them in the
+database:
+
+    my $kids = $schema->storage->dbh->{CachedKids};
+    delete @{$kids}{keys %$kids} if scalar keys %$kids > 100;
+
+But what you probably want is to expire unused statements and not those that are used frequently.
+You can accomplish this with L<Tie::Cache> or L<Tie::Cache::LRU>:
+
+    use Tie::Cache;
+    use DB::Main;
+    my $schema = DB::Main->connect($dbi_dsn, $user, $pass, {
+        on_connect_do => sub { tie %{shift->_dbh->{CachedKids}}, 'Tie::Cache', 100 },
+    });
+
 =cut
index 273397a..7d4505e 100644 (file)
@@ -274,7 +274,7 @@ replaced with the following.)
 
 Or, if you have quoting off:
 
- ->search({ 'YEAR(date_of_birth' => 1979 });
+ ->search({ 'YEAR(date_of_birth)' => 1979 });
 
 =item .. find more help on constructing searches?
 
@@ -353,6 +353,20 @@ to get a new, fresh copy of the row, just re-fetch the row from storage.
 L<DBIx::Class::PK/discard_changes> does just that by re-fetching the row from storage
 using the row's primary key.
 
+=item .. fetch my data a "page" at a time?
+
+Pass the C<rows> and C<page> attributes to your search, eg:
+
+  ->search({}, { rows => 10, page => 1});
+
+=item .. get a count of all rows even when paging?
+
+Call C<pager> on the paged resultset, it will return a L<Data::Page>
+object. Calling C<total_entries> on the pager will return the correct
+total.
+
+C<count> on the resultset will only return the total number in the page.
+
 =back
 
 =head2 Inserting and updating data
index a70ffa1..b245dc9 100644 (file)
@@ -9,6 +9,17 @@ explain them.
 
 =head1 TERMS
 
+=head2 DB schema
+
+Refers to a single physical schema within an RDBMS. Synonymous with the terms
+'database', for MySQL; and 'schema', for most other RDBMS(s).
+
+In other words, it's the 'xyz' _thing_ you're connecting to when using any of
+the following L<DSN|DBI/connect>(s):
+
+  dbi:DriverName:xyz@hostname:port
+  dbi:DriverName:database=xyz;host=hostname;port=port
+
 =head2 Inflation
 
 The act of turning database row data into objects in
index 6aa4830..02b6dcd 100644 (file)
@@ -34,13 +34,18 @@ object that they will be called on.
 
 =item *
 
-Each method starts with a "head2" statement of it's name.
+Each method starts with a "head2" statement of its name.
+
+Just the plain method name, not an example of how to call it, or a link.
+This is to ensure easy linking to method documentation from other POD.
 
 =item *
 
-The header is followed by a one-item list.
+The header is followed by a two-item list. This contains a description
+of the arguments the method is expected to take, and an indication of
+what the method returns.
 
-The single item provides a list of all possible values for the
+The first item provides a list of all possible values for the
 arguments of the method in order, separated by C<, >, preceeded by the
 text "Arguments: "
 
@@ -70,23 +75,59 @@ $var - A scalar (string or numeric) variable.
 
 =item *
 
+%var - A hashref variable (list of key/value pairs) - rarely used in DBIx::Class.
+
+Reading an argument as a hash variable will consume all subsequent
+method arguments, use with caution.
+
+=item *
+
+@var - An array variable (list of values).
+
+Reading an argument as a array variable will consume all subsequent
+method arguments, use with caution.
+
+=item *
+
 ? - Optional, should be placed after the argument type and name.
 
+  ## Correct
+  \%myhashref|\@myarrayref?
+
+  ## Wrong
+  \%myhashref?|\@myarrayref
+
+Applies to the entire argument.
+
+Optional arguments can be left out of method calls, unless the caller
+needs to pass in any of the following arguments. In which case the
+caller should pass C<undef> in place of the missing argument.
+
 =item *
 
-| - Alternate argument types.
+| - Alternate argument content types. 
+
+At least one of these must be supplied unless the argument is also
+marked optional.
 
 =back
 
-NOTES:
+The second item starts with the text "Return value:". The remainder of
+the line is either the text "undefined", a text describing the result of
+the method, or a variable with a descriptive name.
 
-If several arguments are optional, it is always possible to pass
-C<undef> as one optional argument in order to skip it and provide a
-value for the following ones. This does not need to be indicated in
-the Arguments line, it is assumed.
+  ## Good examples
+  =item Return value: undefined
+  =item Return value: A schema object
+  =item Return value: $classname
 
-The C<?> for optional arguments always applies to the entire argument
-value, not a particular type or argument.
+  ## Bad examples
+  =item Return value: The names
+
+"undefined" means the method does not deliberately return a value, and
+the caller should not use or rely on anything it does return. (Perl
+functions always return something, usually the result of the last code
+statement, if there is no explicit return statement.)
 
 =item *
 
@@ -98,6 +139,9 @@ the method does.
 The description paragraph is followed by another list. Each item in
 the list explains one of the possible argument/type combinations.
 
+This list may be omitted if the author feels that the variable names are
+self-explanatory enough to not require it. Use best judgement.
+
 =item *
 
 The argument list is followed by some examples of how to use the
index b870b3b..2a19c50 100644 (file)
@@ -47,7 +47,7 @@ correctly.
 
 L<DBI> version 1.50 and L<DBD::Pg> 1.43 are known to work.
 
-=head2 ... Can't locate object method "source_name" via package ...
+=head2 Can't locate object method "source_name" via package
 
 There's likely a syntax error in the table class referred to elsewhere
 in this error message.  In particular make sure that the package
index e95b693..dc51856 100644 (file)
@@ -349,8 +349,9 @@ sub move_to {
 
     my $position_column = $self->position_column;
 
-    # FIXME this needs to be wrapped in a transaction
     {
+        my $guard = $self->result_source->schema->txn_scope_guard;
+
         my ($direction, @between);
         if ( $from_position < $to_position ) {
             $direction = -1;
@@ -362,10 +363,17 @@ sub move_to {
         }
 
         my $new_pos_val = $self->_position_value ($to_position);                              # record this before the shift
-        $self->_ordered_internal_update({ $position_column => $self->null_position_value });  # take the row out of the picture for a bit
+
+        # we need to null-position the moved row if the position column is part of a constraint
+        if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $self->result_source->unique_constraints }} ) ) ) {
+            $self->_ordered_internal_update({ $position_column => $self->null_position_value });
+        }
+
         $self->_shift_siblings ($direction, @between);
         $self->_ordered_internal_update({ $position_column => $new_pos_val });
 
+        $guard->commit;
+
         return 1;
     }
 }
@@ -407,8 +415,9 @@ sub move_to_group {
         return $self->move_to ($to_position);
     }
 
-    # FIXME this needs to be wrapped in a transaction
     {
+        my $guard = $self->result_source->schema->txn_scope_guard;
+
         # Move to end of current group to adjust siblings
         $self->move_last;
 
@@ -431,6 +440,8 @@ sub move_to_group {
 
         $self->_ordered_internal_update;
 
+        $guard->commit;
+
         return 1;
     }
 }
@@ -488,8 +499,9 @@ sub update {
         return $self->next::method( \%changes, @_ );
     }
 
-    # FIXME this needs to be wrapped in a transaction
     {
+        my $guard = $self->result_source->schema->txn_scope_guard;
+
         # if any of our grouping columns have been changed
         if (grep { exists $changes{$_} } ($self->_grouping_columns) ) {
 
@@ -517,7 +529,20 @@ sub update {
             $self->move_to(delete $changes{$position_column});
         }
 
-        return $self->next::method( \%changes, @_ );
+        my @res;
+        my $want = wantarray();
+        if (not defined $want) {
+            $self->next::method( \%changes, @_ );
+        }
+        elsif ($want) {
+            @res = $self->next::method( \%changes, @_ );
+        }
+        else {
+            $res[0] = $self->next::method( \%changes, @_ );
+        }
+
+        $guard->commit;
+        return $want ? @res : $res[0];
     }
 }
 
@@ -531,14 +556,28 @@ integrity of the positions.
 
 sub delete {
     my $self = shift;
-    # FIXME this needs to be wrapped in a transaction
-    {
-        $self->move_last;
-        return $self->next::method( @_ );
+
+    my $guard = $self->result_source->schema->txn_scope_guard;
+
+    $self->move_last;
+
+    my @res;
+    my $want = wantarray();
+    if (not defined $want) {
+        $self->next::method( @_ );
+    }
+    elsif ($want) {
+        @res = $self->next::method( @_ );
     }
+    else {
+        $res[0] = $self->next::method( @_ );
+    }
+
+    $guard->commit;
+    return $want ? @res : $res[0];
 }
 
-=head1 Methods for extending Ordered
+=head1 METHODS FOR EXTENDING ORDERED
 
 You would want to override the methods below if you use sparse
 (non-linear) or non-numeric position values. This can be useful
@@ -549,7 +588,7 @@ or if you need to work with materialized path columns.
 
   my $num_pos = $item->_position;
 
-Returns the absolute numeric position of the current object, with the
+Returns the B<absolute numeric position> of the current object, with the
 first object being at position 1, its sibling at position 2 and so on.
 By default simply returns the value of L</position_column>.
 
@@ -567,7 +606,7 @@ sub _position {
 
   my $pos_value = $item->_position_value ( $pos )
 
-Returns the value of L</position_column> of the object at numeric
+Returns the B<value> of L</position_column> of the object at numeric
 position C<$pos>. By default simply returns C<$pos>.
 
 =cut
@@ -589,7 +628,7 @@ sub _position_value {
 
   __PACKAGE__->_initial_position_value(0);
 
-This method specifies a value of L</position_column> which is assigned
+This method specifies a B<value> of L</position_column> which is assigned
 to the first inserted element of a group, if no value was supplied at
 insertion time. All subsequent values are derived from this one by
 L</_next_position_value> below. Defaults to 1.
@@ -602,7 +641,7 @@ __PACKAGE__->mk_classdata( '_initial_position_value' => 1 );
 
   my $new_value = $item->_next_position_value ( $position_value )
 
-Returns a position value that would be considered C<next> with
+Returns a position B<value> that would be considered C<next> with
 regards to C<$position_value>. Can be pretty much anything, given
 that C<< $position_value < $new_value >> where C<< < >> is the
 SQL comparison operator (usually works fine on strings). The
@@ -618,10 +657,16 @@ sub _next_position_value {
 
   $item->_shift_siblings ($direction, @between)
 
-Shifts all siblings with position in the range @between (inclusive)
-by one position as specified by $direction (left if < 0, right if > 0).
-By default simply increments/decrements each L<position_column> value
-by 1.
+Shifts all siblings with B<positions values> in the range @between
+(inclusive) by one position as specified by $direction (left if < 0,
+ right if > 0). By default simply increments/decrements each
+L<position_column> value by 1, doing so in a way as to not violate
+any existing constraints.
+
+Note that if you override this method and have unique constraints
+including the L<position_column> the shift is not a trivial task.
+Refer to the implementation source of the default method for more
+information.
 
 =cut
 sub _shift_siblings {
@@ -647,8 +692,7 @@ sub _shift_siblings {
     # position column is part of a unique constraint, and do a
     # one-by-one update if this is the case
 
-    my %uc = $self->result_source->unique_constraints;
-    if (grep { $_ eq $position_column } ( map { @$_ } (values %uc) ) ) {
+    if (grep { $_ eq $position_column } ( map { @$_ } (values %{{ $self->result_source->unique_constraints }} ) ) ) {
 
         my $rs = $shift_rs->search ({}, { order_by => { "-$ord", $position_column } } );
         # FIXME - no need to inflate each row
@@ -668,7 +712,7 @@ need to use them.
 
 =head2 _group_rs
 
-This method returns a resultset containing all memebers of the row
+This method returns a resultset containing all members of the row
 group (including the row itself).
 
 =cut
@@ -746,6 +790,23 @@ sub _is_in_group {
     return 1;
 }
 
+=head2 _ordered_internal_update
+
+This is a short-circuited method, that is used internally by this
+module to update positioning values in isolation (i.e. without
+triggering any of the positioning integrity code).
+
+Some day you might get confronted by datasets that have ambiguous
+positioning data (i.e. duplicate position values within the same group,
+in a table without unique constraints). When manually fixing such data
+keep in mind that you can not invoke L<DBIx::Class::Row/update> like
+you normally would, as it will get confused by the wrong data before
+having a chance to update the ill-defined row. If you really know what
+you are doing use this method which bypasses any hooks introduced by
+this module.
+
+=cut
+
 sub _ordered_internal_update {
     my $self = shift;
     local $self->{_ORDERED_INTERNAL_UPDATE} = 1;
@@ -784,9 +845,18 @@ ResultSet class that supports a parent() method, for example.  Another
 solution is to somehow automagically modify the objects that exist 
 in the current object's result set to have the new position value.
 
+=head2 Default Values
+
+Using a database defined default_value on one of your group columns
+could result in the position not being assigned correctly.
+
 =head1 AUTHOR
 
-Aran Deltac <bluefeet@cpan.org>
+ Original code framework
+   Aran Deltac <bluefeet@cpan.org>
+
+ Constraints support and code generalisation
+   Peter Rabbitson <ribasushi@cpan.org>
 
 =head1 LICENSE
 
index 6ec2f25..065cf69 100644 (file)
@@ -31,11 +31,12 @@ sub add_relationship_accessor {
           $rel_info->{cond}, $rel, $self
         );
         if ($rel_info->{attrs}->{undef_on_null_fk}){
-          return unless ref($cond) eq 'HASH';
-          return if grep { not defined } values %$cond;
+          return undef unless ref($cond) eq 'HASH';
+          return undef if grep { not defined $_ } values %$cond;
         }
         my $val = $self->find_related($rel, {}, {});
-        return unless $val;
+        return $val unless $val;  # $val instead of undef so that null-objects can go through
+
         return $self->{_relationship_data}{$rel} = $val;
       }
     };
index 9e4a35a..23df27e 100644 (file)
@@ -209,7 +209,7 @@ sub related_resultset {
         if (ref $_ eq 'HASH') {
           my $hash;
           foreach my $key (keys %$_) {
-            my $newkey = $key =~ /\./ ? "me.$key" : $key;
+            my $newkey = $key !~ /\./ ? "me.$key" : $key;
             $hash->{$newkey} = $_->{$key};
           }
           $hash;
index eb10752..a4dce6e 100644 (file)
@@ -79,12 +79,14 @@ sub belongs_to {
   return 1;
 }
 
-=head1 AUTHORS
+# Attempt to remove the POD so it (maybe) falls off the indexer
 
-Alexander Hartmaier <Alexander.Hartmaier@t-systems.at>
-
-Matt S. Trout <mst@shadowcatsystems.co.uk>
-
-=cut
+#=head1 AUTHORS
+#
+#Alexander Hartmaier <Alexander.Hartmaier@t-systems.at>
+#
+#Matt S. Trout <mst@shadowcatsystems.co.uk>
+#
+#=cut
 
 1;
index 5fffad4..45251fa 100644 (file)
@@ -5,7 +5,7 @@ use warnings;
 
 =head1 NAME
 
-DBIx::Class::ResultClass::HashRefInflator
+DBIx::Class::ResultClass::HashRefInflator - Get raw hashrefs from a resultset
 
 =head1 SYNOPSIS
 
index 92cbbd7..aa6fa59 100644 (file)
@@ -102,6 +102,21 @@ another.
     });
   }
 
+=head3 Resolving conditions and attributes
+
+When a resultset is chained from another resultset, conditions and
+attributes with the same keys need resolving.
+
+L</join>, L</prefetch>, L</+select>, L</+as> attributes are merged
+into the existing ones from the original resultset.
+
+The L</where>, L</having> attribute, and any search conditions are
+merged with an SQL C<AND> to the existing condition from the original
+resultset.
+
+All other attributes are overridden by any new ones supplied in the
+search attributes.
+
 =head2 Multiple queries
 
 Since a resultset just defines a query, you can do all sorts of
@@ -264,6 +279,11 @@ always return a resultset, even in list context.
 sub search_rs {
   my $self = shift;
 
+  # Special-case handling for (undef, undef).
+  if ( @_ == 2 && !defined $_[1] && !defined $_[0] ) {
+    pop(@_); pop(@_);
+  }
+
   my $attrs = {};
   $attrs = pop(@_) if @_ > 1 and ref $_[$#_] eq 'HASH';
   my $our_attrs = { %{$self->{attrs}} };
@@ -287,7 +307,7 @@ sub search_rs {
   my $new_attrs = { %{$our_attrs}, %{$attrs} };
 
   # merge new attrs into inherited
-  foreach my $key (qw/join prefetch +select +as/) {
+  foreach my $key (qw/join prefetch +select +as bind/) {
     next unless exists $attrs->{$key};
     $new_attrs->{$key} = $self->_merge_attr($our_attrs->{$key}, $attrs->{$key});
   }
@@ -373,19 +393,29 @@ Pass a literal chunk of SQL to be added to the conditional part of the
 resultset query.
 
 CAVEAT: C<search_literal> is provided for Class::DBI compatibility and should
-only be used in that context. There are known problems using C<search_literal>
-in chained queries; it can result in bind values in the wrong order.  See
-L<DBIx::Class::Manual::Cookbook/Searching> and
+only be used in that context. C<search_literal> is a convenience method. 
+It is equivalent to calling $schema->search(\[]), but if you want to ensure
+columns are bound correctly, use C<search>.
+
+Example of how to use C<search> instead of C<search_literal>
+
+  my @cds = $cd_rs->search_literal('cdid = ? AND (artist = ? OR artist = ?)', (2, 1, 2));
+  my @cds = $cd_rs->search(\[ 'cdid = ? AND (artist = ? OR artist = ?)', [ 'cdid', 2 ], [ 'artist', 1 ], [ 'artist', 2 ] ]);
+
+
+See L<DBIx::Class::Manual::Cookbook/Searching> and 
 L<DBIx::Class::Manual::FAQ/Searching> for searching techniques that do not
 require C<search_literal>.
 
 =cut
 
 sub search_literal {
-  my ($self, $cond, @vals) = @_;
-  my $attrs = (ref $vals[$#vals] eq 'HASH' ? { %{ pop(@vals) } } : {});
-  $attrs->{bind} = [ @{$self->{attrs}{bind}||[]}, @vals ];
-  return $self->search(\$cond, $attrs);
+  my ($self, $sql, @bind) = @_; 
+  my $attr;
+  if ( @bind && ref($bind[-1]) eq 'HASH' ) {
+    $attr = pop @bind;
+  }
+  return $self->search(\[ $sql, map [ __DUMMY__ => $_ ], @bind ], ($attr || () ));
 }
 
 =head2 find
@@ -830,10 +860,24 @@ You most likely want to use L</search> with specific operators.
 
 For more information, see L<DBIx::Class::Manual::Cookbook>.
 
+This method is deprecated and will be removed in 0.09. Use L</search()>
+instead. An example conversion is:
+
+  ->search_like({ foo => 'bar' });
+
+  # Becomes
+
+  ->search({ foo => { like => 'bar' } });
+
 =cut
 
 sub search_like {
   my $class = shift;
+  carp join ("\n",
+    'search_like() is deprecated and will be removed in 0.09.',
+    'Instead use ->search({ x => { -like => "y%" } })',
+    '(note the outer pair of {}s - they are important!)'
+  );
   my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {});
   my $query = ref $_[0] eq 'HASH' ? { %{shift()} }: {@_};
   $query->{$_} = { 'like' => $query->{$_} } for keys %$query;
@@ -1077,6 +1121,11 @@ 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.
 
+Note that changing the result_class will also remove any components
+that were originally loaded in the source class via
+L<DBIx::Class::ResultSource/load_components>. Any overloaded methods
+in the original source class will not run.
+
 =cut
 
 sub result_class {
@@ -1102,12 +1151,6 @@ Performs an SQL C<COUNT> 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<count> with C<group_by>, L<DBIx::Class> emulates C<GROUP BY>
-using C<COUNT( DISTINCT( columns ) )>. Some databases (notably SQLite) do
-not support C<DISTINCT> with multiple columns. If you are using such a
-database, you should only use columns from the main table in your C<group_by>
-clause.
-
 =cut
 
 sub count {
@@ -1128,32 +1171,21 @@ sub count {
 
 sub _count { # Separated out so pager can get the full count
   my $self = shift;
-  my $select = { count => '*' };
-
   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${alias}.\E)?$pk[0]$/) {
-          @distinct = ($column);
-          last;
-        }
-      }
-    }
 
-    $select = { count => { distinct => \@distinct } };
+  if (my $group_by = $attrs->{group_by}) {
+    delete $attrs->{order_by};
+
+    $attrs->{select} = $group_by; 
+    $attrs->{from} = [ { 'mesub' => (ref $self)->new($self->result_source, $attrs)->cursor->as_query } ];
+    delete $attrs->{where};
   }
 
-  $attrs->{select} = $select;
+  $attrs->{select} = { count => '*' };
   $attrs->{as} = [qw/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/;
+  # offset, order by, group by, where and page are not needed to count. record_filter is cdbi
+  delete $attrs->{$_} for qw/rows offset order_by group_by page pager record_filter/;
 
   my $tmp_rs = (ref $self)->new($self->result_source, $attrs);
   my ($count) = $tmp_rs->cursor->next;
@@ -1636,6 +1668,9 @@ sub _normalize_populate_args {
 Return Value a L<Data::Page> object for the current resultset. Only makes
 sense for queries with a C<page> attribute.
 
+To get the full count of entries for a paged resultset, call
+C<total_entries> on the L<Data::Page> object.
+
 =cut
 
 sub pager {
@@ -1700,7 +1735,7 @@ sub new_result {
     defined $self->{cond}
     && $self->{cond} eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION
   ) {
-    %new = %{$self->{attrs}{related_objects}};
+    %new = %{ $self->{attrs}{related_objects} || {} };  # nothing might have been inserted yet
     $new{-from_resultset} = [ keys %new ] if keys %new;
   } else {
     $self->throw_exception(
@@ -1807,7 +1842,7 @@ sub _remove_alias {
   return \%unaliased;
 }
 
-=head2 as_query
+=head2 as_query (EXPERIMENTAL)
 
 =over 4
 
@@ -1821,6 +1856,8 @@ Returns the SQL query and bind vars associated with the invocant.
 
 This is generally used as the RHS for a subquery.
 
+B<NOTE>: This feature is still experimental.
+
 =cut
 
 sub as_query { return shift->cursor->as_query(@_) }
@@ -2067,6 +2104,63 @@ sub update_or_create {
   return $self->create($cond);
 }
 
+=head2 update_or_new
+
+=over 4
+
+=item Arguments: \%col_values, { key => $unique_constraint }?
+
+=item Return Value: $rowobject
+
+=back
+
+  $resultset->update_or_new({ 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, instantiate
+a new result object and return it. The object will not be saved into your storage
+until you call L<DBIx::Class::Row/insert> on it.
+
+Takes an optional C<key> attribute to search on a specific unique constraint.
+For example:
+
+  # In your application
+  my $cd = $schema->resultset('CD')->update_or_new(
+    {
+      artist => 'Massive Attack',
+      title  => 'Mezzanine',
+      year   => 1998,
+    },
+    { key => 'cd_artist_title' }
+  );
+
+  if ($cd->in_storage) {
+      # the cd was updated
+  }
+  else {
+      # the cd is not yet in the database, let's insert it
+      $cd->insert;
+  }
+
+See also L</find>, L</find_or_create> and L<find_or_new>.
+
+=cut
+
+sub update_or_new {
+    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->new_result($cond);
+}
+
 =head2 get_cache
 
 =over 4
@@ -2283,97 +2377,135 @@ sub _resolved_attrs {
   my $self = shift;
   return $self->{_attrs} if $self->{_attrs};
 
-  my $attrs = { %{$self->{attrs}||{}} };
+  my $attrs  = { %{ $self->{attrs} || {} } };
   my $source = $self->result_source;
-  my $alias = $attrs->{alias};
+  my $alias  = $attrs->{alias};
 
   $attrs->{columns} ||= delete $attrs->{cols} if exists $attrs->{cols};
-  if ($attrs->{columns}) {
-    delete $attrs->{as};
-  } elsif (!$attrs->{select}) {
-    $attrs->{columns} = [ $source->columns ];
+  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 );
   }
-  $attrs->{select} = 
-    ($attrs->{select}
-      ? (ref $attrs->{select} eq 'ARRAY'
-          ? [ @{$attrs->{select}} ]
-          : [ $attrs->{select} ])
-      : [ map { m/\./ ? $_ : "${alias}.$_" } @{delete $attrs->{columns}} ]
-    );
-  $attrs->{as} =
-    ($attrs->{as}
-      ? (ref $attrs->{as} eq 'ARRAY'
-          ? [ @{$attrs->{as}} ]
-          : [ $attrs->{as} ])
-      : [ map { m/^\Q${alias}.\E(.+)$/ ? $1 : $_ } @{$attrs->{select}} ]
+  # 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->{$_} );
+  }
+
+  # 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} } ]
     );
-  
-  my $adds;
-  if ($adds = delete $attrs->{include_columns}) {
-    $adds = [$adds] unless ref $adds eq 'ARRAY';
-    push(@{$attrs->{select}}, @$adds);
-    push(@{$attrs->{as}}, map { m/([^.]+)$/; $1 } @$adds);
   }
-  if ($adds = delete $attrs->{'+select'}) {
+  else {
+
+    # otherwise we intialise select & as to empty
+    $attrs->{select} = [];
+    $attrs->{as}     = [];
+  }
+
+  # now add colbits to select/as
+  push( @{ $attrs->{select} }, map { values( %{$_} ) } @colbits );
+  push( @{ $attrs->{as} },     map { keys( %{$_} ) } @colbits );
+
+  my $adds;
+  if ( $adds = delete $attrs->{'+select'} ) {
     $adds = [$adds] unless ref $adds eq 'ARRAY';
-    push(@{$attrs->{select}},
-           map { /\./ || ref $_ ? $_ : "${alias}.$_" } @$adds);
+    push(
+      @{ $attrs->{select} },
+      map { /\./ || ref $_ ? $_ : "${alias}.$_" } @$adds
+    );
   }
-  if (my $adds = delete $attrs->{'+as'}) {
+  if ( $adds = delete $attrs->{'+as'} ) {
     $adds = [$adds] unless ref $adds eq 'ARRAY';
-    push(@{$attrs->{as}}, @$adds);
+    push( @{ $attrs->{as} }, @$adds );
   }
 
   $attrs->{from} ||= [ { $self->{attrs}{alias} => $source->from } ];
 
-  if (exists $attrs->{join} || exists $attrs->{prefetch}) {
+  if ( exists $attrs->{join} || exists $attrs->{prefetch} ) {
     my $join = delete $attrs->{join} || {};
 
-    if (defined $attrs->{prefetch}) {
-      $join = $self->_merge_attr(
-        $join, $attrs->{prefetch}
-      );
-      
+    if ( defined $attrs->{prefetch} ) {
+      $join = $self->_merge_attr( $join, $attrs->{prefetch} );
+
     }
 
-    $attrs->{from} =   # have to copy here to avoid corrupting the original
+    $attrs->{from} =    # have to copy here to avoid corrupting the original
       [
-        @{$attrs->{from}}, 
-        $source->resolve_join($join, $alias, { %{$attrs->{seen_join}||{}} })
+      @{ $attrs->{from} },
+      $source->resolve_join(
+        $join, $alias, { %{ $attrs->{seen_join} || {} } }
+      )
       ];
 
   }
 
-  $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} = [];    
+  $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} = [];
   }
 
   my $collapse = $attrs->{collapse} || {};
-  if (my $prefetch = delete $attrs->{prefetch}) {
-    $prefetch = $self->_merge_attr({}, $prefetch);
+  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)) {
+    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);
+      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);
+    push( @{ $attrs->{order_by} }, @pre_order );
   }
   $attrs->{collapse} = $collapse;
 
-  if ($attrs->{page}) {
+  if ( $attrs->{page} ) {
     $attrs->{offset} ||= 0;
-    $attrs->{offset} += ($attrs->{rows} * ($attrs->{page} - 1));
+    $attrs->{offset} += ( $attrs->{rows} * ( $attrs->{page} - 1 ) );
   }
 
   return $self->{_attrs} = $attrs;
@@ -2525,22 +2657,26 @@ These are in no particular order:
 
 =over 4
 
-=item Value: ($order_by | \@order_by)
+=item Value: ( $order_by | \@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<year DESC> for a
-descending order on the column `year'.
+Which column(s) to order the results by. If a single column name, or
+an arrayref of names is supplied, the argument is passed through
+directly to SQL. The hashref syntax allows for connection-agnostic
+specification of ordering direction:
+
+ For descending order:
+
+  order_by => { -desc => [qw/col1 col2 col3/] }
 
-Please note that if you have C<quote_char> enabled (see
-L<DBIx::Class::Storage::DBI/connect_info>) 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.)
+ For explicit ascending order:
 
-If your L<SQL::Abstract> 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.
+  order_by => { -asc => 'col' }
+
+The old scalarref syntax (i.e. order_by => \'year DESC') is still
+supported, although you are strongly encouraged to use the hashref
+syntax as outlined above.
 
 =head2 columns
 
@@ -2550,12 +2686,15 @@ recommended syntax.
 
 =back
 
-Shortcut to request a particular set of columns to be retrieved.  Adds
-C<me.> onto the start of any column without a C<.> in it and sets C<select>
-from that, then auto-populates C<as> from C<select> as normal. (You may also
-use the C<cols> attribute, as in earlier versions of DBIC.)
+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<as> value, and the value is used as the C<select>
+expression). Adds C<me.> onto the start of any column without a C<.> in
+it and sets C<select> from that, then auto-populates C<as> from
+C<select> as normal. (You may also use the C<cols> attribute, as in
+earlier versions of DBIC.)
 
-=head2 include_columns
+=head2 +columns
 
 =over 4
 
@@ -2563,10 +2702,13 @@ use the C<cols> attribute, as in earlier versions of DBIC.)
 
 =back
 
-Shortcut to include additional columns in the returned results - for example
+Indicates additional columns to be selected from storage. Works the same
+as L</columns> but adds columns to the selection. (You may also use the
+C<include_columns> attribute, as in earlier versions of DBIC). For
+example:-
 
   $schema->resultset('CD')->search(undef, {
-    include_columns => ['artist.name'],
+    '+columns' => ['artist.name'],
     join => ['artist']
   });
 
@@ -2575,6 +2717,16 @@ passed to object inflation. Note that the 'artist' is the name of the
 column (or relationship) accessor, and 'name' is the name of the column
 accessor in the related table.
 
+=head2 include_columns
+
+=over 4
+
+=item Value: \@columns
+
+=back
+
+Deprecated.  Acts as a synonym for L</+columns> for backward compatibility.
+
 =head2 select
 
 =over 4
@@ -2805,6 +2957,10 @@ on it.
 
 If L<rows> attribute is not specified it defualts to 10 rows per page.
 
+When you have a paged resultset, L</count> will only return the number
+of rows in the page. To get the total, use the L</pager> and call
+C<total_entries> on it.
+
 =head2 rows
 
 =over 4
index 3248ecb..596df7c 100644 (file)
@@ -38,7 +38,7 @@ sub new {
   $class = ref $class if ref $class;
   my $new_parent_rs = $rs->search_rs; # we don't want to mess up the original, so clone it
   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
+  $new_parent_rs->{attrs}->{prefetch} = undef; # prefetch 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
@@ -54,7 +54,7 @@ sub new {
   return $new;
 }
 
-=head2 as_query
+=head2 as_query (EXPERIMENTAL)
 
 =over 4
 
@@ -68,6 +68,8 @@ Returns the SQL query and bind vars associated with the invocant.
 
 This is generally used as the RHS for a subquery.
 
+B<NOTE>: This feature is still experimental.
+
 =cut
 
 sub as_query { return shift->_resultset->as_query }
index 0d49c00..0094fa4 100644 (file)
@@ -125,8 +125,12 @@ L<DBIx::Class::Schema/deploy>.
 =item default_value
 
 Set this to the default value which will be inserted into a column
-by the database. Can contain either a value or a function. This is
-currently only used by L<DBIx::Class::Schema/deploy>.
+by the database. Can contain either a value or a function (use a
+reference to a scalar e.g. C<\'now()'> if you want a function). This
+is currently only used by L<DBIx::Class::Schema/deploy>.
+
+See the note on L<DBIx::Class::Row/new> for more information about possible
+issues related to db-side default values.
 
 =item sequence
 
@@ -1085,12 +1089,16 @@ sub resolve_join {
   $seen ||= {};
   $force_left ||= { force => 0 };
   if (ref $join eq 'ARRAY') {
-    return map { $self->resolve_join($_, $alias, $seen) } @$join;
+    return
+      map {
+        local $force_left->{force} = $force_left->{force};
+        $self->resolve_join($_, $alias, $seen, $force_left);
+      } @$join;
   } elsif (ref $join eq 'HASH') {
     return
       map {
         my $as = ($seen->{$_} ? $_.'_'.($seen->{$_}+1) : $_);
-        local $force_left->{force};
+        local $force_left->{force} = $force_left->{force};
         (
           $self->resolve_join($_, $alias, $seen, $force_left),
           $self->related_source($_)->resolve_join(
@@ -1196,7 +1204,11 @@ 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 resolve relationship");
+            $self->throw_exception(
+              "Column ${v} not loaded or not passed to new() prior to insert()"
+                ." on ${for} trying to resolve relationship (maybe you forgot "
+                  ."to call ->reload_from_storage to get defaults from the db)"
+            );
           }
           return $UNRESOLVABLE_CONDITION;
         }
index f1ab5f1..0bcb0fc 100644 (file)
@@ -46,7 +46,7 @@ L<DBIx::Class::Schema/deploy> unless you set L</is_virtual> to true.
 Deploying the view does B<not> translate it between different database
 syntaxes, so be careful what you write in your view SQL.
 
-Virtual views (L</is_virtual> unset or false), are assumed to not
+Virtual views (L</is_virtual> true), are assumed to not
 exist in your database as a real view. The L</view_definition> in this
 case replaces the view name in a FROM clause in a subselect.
 
@@ -54,13 +54,13 @@ case replaces the view name in a FROM clause in a subselect.
 
 =over
 
-=item is_virtual set to true
+=item is_virtual set to false
 
   $schema->resultset('Year2000CDs')->all();
 
   SELECT cdid, artist, title FROM year2000cds me
 
-=item is_virtual set to false
+=item is_virtual set to true
 
   $schema->resultset('Year2000CDs')->all();
 
@@ -115,6 +115,8 @@ Guillermo Roditi E<lt>groditi@cpan.orgE<gt>
 
 Jess Robinson <castaway@desert-island.me.uk>
 
+Wallace Reis <wreis@cpan.org>
+
 =head1 LICENSE
 
 You may distribute this code under the same terms as Perl itself.
index 9354318..6d6353f 100644 (file)
@@ -20,7 +20,7 @@ our $thaw_schema;
 
 =head1 NAME
 
-DBIx::Class::ResultSourceHandle
+DBIx::Class::ResultSourceHandle - Decouple Rows/ResultSets objects from their Source objects
 
 =head1 DESCRIPTION
 
index b004d24..c6fd923 100644 (file)
@@ -8,6 +8,10 @@ use Carp::Clan qw/^DBIx::Class/;
 use Scalar::Util ();
 use Scope::Guard;
 
+###
+### Internal method
+### Do not use
+###
 BEGIN {
   *MULTICREATE_DEBUG =
     $ENV{DBIC_MULTICREATE_DEBUG}
@@ -73,6 +77,23 @@ passed objects.
 
 For a more involved explanation, see L<DBIx::Class::ResultSet/create>.
 
+Please note that if a value is not passed to new, no value will be sent
+in the SQL INSERT call, and the column will therefore assume whatever
+default value was specified in your database. While DBIC will retrieve the
+value of autoincrement columns, it will never make an explicit database
+trip to retrieve default values assigned by the RDBMS. You can explicitly
+request that all values be fetched back from the database by calling
+L</discard_changes>, or you can supply an explicit C<undef> to columns
+with NULL as the default, and save yourself a SELECT.
+
+ CAVEAT:
+
+ The behavior described above will backfire if you use a foreign key column
+ with a database-defined default. If you call the relationship accessor on
+ an object that doesn't have a set value for the FK column, DBIC will throw
+ an exception, as it has no way of knowing the PK of the related object (if
+ there is one).
+
 =cut
 
 ## It needs to store the new objects somewhere, and call insert on that list later when insert is called on this object. We may need an accessor for these so the user can retrieve them, if just doing ->new().
@@ -960,6 +981,9 @@ for example to rebless the result into a different class.
 Reblessing can also be done more easily by setting C<result_class> in
 your Result class. See L<DBIx::Class::ResultSource/result_class>.
 
+Different types of results can also be created from a particular
+L<DBIx::Class::ResultSet>, see L<DBIx::Class::ResultSet/result_class>.
+
 =cut
 
 sub inflate_result {
@@ -1005,7 +1029,6 @@ sub inflate_result {
         $fetched = $pre_source->result_class->inflate_result(
                       $pre_source, @{$pre_val});
       }
-      $new->related_resultset($pre)->set_cache([ $fetched ]);
       my $accessor = $source->relationship_info($pre)->{attrs}{accessor};
       $class->throw_exception("No accessor for prefetched $pre")
        unless defined $accessor;
@@ -1016,6 +1039,7 @@ sub inflate_result {
       } else {
        $class->throw_exception("Prefetch not supported with accessor '$accessor'");
       }
+      $new->related_resultset($pre)->set_cache([ $fetched ]);
     }
   }
   return $new;
index 1c75a50..71faab7 100644 (file)
@@ -157,6 +157,21 @@ sub _expand_relative_name {
   return $name;
 }
 
+# Finds all modules in the supplied namespace, or if omitted in the
+# namespace of $class. Untaints all findings as they can be assumed
+# to be safe
+sub _findallmod {
+  my $proto = shift;
+  my $ns = shift || ref $proto || $proto;
+
+  my @mods = Module::Find::findallmod($ns);
+
+  # try to untaint module names. mods where this fails
+  # are left alone so we don't have to change the old behavior
+  no locale; # localized \w doesn't untaint expression
+  return map { $_ =~ m/^( (?:\w+::)* \w+ )$/x ? $1 : $_ } @mods;
+}
+
 # returns a hash of $shortname => $fullname for every package
 #  found in the given namespaces ($shortname is with the $fullname's
 #  namespace stripped off)
@@ -168,7 +183,7 @@ sub _map_namespaces {
     push(
       @results_hash,
       map { (substr($_, length "${namespace}::"), $_) }
-      Module::Find::findallmod($namespace)
+      $class->_findallmod($namespace)
     );
   }
 
@@ -314,7 +329,7 @@ sub load_classes {
     }
   } else {
     my @comp = map { substr $_, length "${class}::"  }
-                 Module::Find::findallmod($class);
+                 $class->_findallmod;
     $comps_for{$class} = \@comp;
   }
 
@@ -325,13 +340,6 @@ sub load_classes {
     foreach my $prefix (keys %comps_for) {
       foreach my $comp (@{$comps_for{$prefix}||[]}) {
         my $comp_class = "${prefix}::${comp}";
-        { # try to untaint module name. mods where this fails
-          # are left alone so we don't have to change the old behavior
-          no locale; # localized \w doesn't untaint expression
-          if ( $comp_class =~ m/^( (?:\w+::)* \w+ )$/x ) {
-            $comp_class = $1;
-          }
-        }
         $class->ensure_class_loaded($comp_class);
 
         my $snsub = $comp_class->can('source_name');
@@ -475,6 +483,12 @@ Note that C<connect_info> expects an arrayref of arguments, but
 C<connect> does not. C<connect> wraps it's arguments in an arrayref
 before passing them to C<connect_info>.
 
+=head3 Overloading
+
+C<connect> is a convenience method. It is equivalent to calling
+$schema->clone->connection(@connectinfo). To write your own overloaded
+version, overload L</connection> instead.
+
 =cut
 
 sub connect { shift->clone->connection(@_) }
@@ -603,7 +617,7 @@ sub txn_do {
   $self->storage->txn_do(@_);
 }
 
-=head2 txn_scope_guard (EXPERIMENTAL)
+=head2 txn_scope_guard
 
 Runs C<txn_scope_guard> on the schema's storage. See 
 L<DBIx::Class::Storage/txn_scope_guard>.
@@ -752,6 +766,9 @@ Similar to L</connect> except sets the storage object and connection
 data in-place on the Schema class. You should probably be calling
 L</connect> to get a proper Schema object instead.
 
+=head3 Overloading
+
+Overload C<connection> to change the behaviour of C<connect>.
 
 =cut
 
@@ -990,7 +1007,9 @@ Attempts to deploy the schema to the current storage using L<SQL::Translator>.
 
 See L<SQL::Translator/METHODS> for a list of values for C<$sqlt_args>. The most
 common value for this would be C<< { add_drop_table => 1, } >> to have the SQL
-produced include a DROP TABLE statement for each table created.
+produced include a DROP TABLE statement for each table created. For quoting
+purposes use C<producer_options> value with C<quote_table_names> and
+C<quote_field_names>.
 
 Additionally, the DBIx::Class parser accepts a C<sources> parameter as a hash 
 ref or an array ref, containing a list of source to deploy. If present, then 
@@ -1248,24 +1267,33 @@ sub register_extra_source {
 sub _register_source {
   my ($self, $moniker, $source, $params) = @_;
 
+  my $orig_source = $source;
+
   $source = $source->new({ %$source, source_name => $moniker });
+  $source->schema($self);
+  weaken($source->{schema}) if ref($self);
+
+  my $rs_class = $source->result_class;
 
   my %reg = %{$self->source_registrations};
   $reg{$moniker} = $source;
   $self->source_registrations(\%reg);
 
-  $source->schema($self);
-  weaken($source->{schema}) if ref($self);
   return if ($params->{extra});
-
-  if ($source->result_class) {
-    my %map = %{$self->class_mappings};
-    if (exists $map{$source->result_class}) {
-      warn $source->result_class . ' already has a source, use register_extra_source for additional sources';
-    }
-    $map{$source->result_class} = $moniker;
-    $self->class_mappings(\%map);
+  return unless defined($rs_class) && $rs_class->can('result_source_instance');
+
+  my %map = %{$self->class_mappings};
+  if (
+    exists $map{$rs_class}
+      and
+    $map{$rs_class} ne $moniker
+      and
+    $rs_class->result_source_instance ne $orig_source
+  ) {
+    carp "$rs_class already has a source, use register_extra_source for additional sources";
   }
+  $map{$rs_class} = $moniker;
+  $self->class_mappings(\%map);
 }
 
 sub _unregister_source {
index eb4c352..2c29e0e 100644 (file)
@@ -533,7 +533,6 @@ sub _create_db_to_schema_diff {
   $db_tr->producer($db);
   my $dbic_tr = SQL::Translator->new;
   $dbic_tr->parser('SQL::Translator::Parser::DBIx::Class');
-  $dbic_tr = $self->storage->configure_sqlt($dbic_tr, $db);
   $dbic_tr->data($self);
   $dbic_tr->producer($db);
 
@@ -614,7 +613,7 @@ sub _source_exists
 
 =head1 AUTHORS
 
-Jess Robinson <castaway@desert-island.demon.co.uk>
+Jess Robinson <castaway@desert-island.me.uk>
 Luke Saunders <luke@shadowcatsystems.co.uk>
 
 =head1 LICENSE
index 604ad5b..eb87d4f 100644 (file)
@@ -299,21 +299,25 @@ sub svp_rollback { die "Virtual method!" }
 
 =for comment
 
-=head2 txn_scope_guard (EXPERIMENTAL)
+=head2 txn_scope_guard
 
-An alternative way of using transactions to C<txn_do>:
+An alternative way of transaction handling based on
+L<DBIx::Class::Storage::TxnScopeGuard>:
 
- my $txn = $storage->txn_scope_guard;
+ my $txn_guard = $storage->txn_scope_guard;
 
  $row->col1("val1");
  $row->update;
 
- $txn->commit;
+ $txn_guard->commit;
 
-If a exception occurs, the transaction will be rolled back. This is still very
-experiemental, and we are not 100% sure it is working right when nested. The
-onus is on you as the user to make sure you dont forget to call
-$C<$txn->commit>.
+If an exception occurs, or the guard object otherwise leaves the scope
+before C<< $txn_guard->commit >> is called, the transaction will be rolled
+back by an explicit L</txn_rollback> call. In essence this is akin to
+using a L</txn_begin>/L</txn_commit> pair, without having to worry
+about calling L</txn_rollback> at the right places. Note that since there
+is no defined code closure, there will be no retries and other magic upon
+database disconnection. If you need such functionality see L</txn_do>.
 
 =cut
 
index 6679a57..8fdf151 100644 (file)
@@ -95,6 +95,9 @@ sub _find_syntax {
 
 sub select {
   my ($self, $table, $fields, $where, $order, @rest) = @_;
+  local $self->{having_bind} = [];
+  local $self->{from_bind} = [];
+
   if (ref $table eq 'SCALAR') {
     $table = $$table;
   }
@@ -106,8 +109,7 @@ sub select {
   @rest = (-1) unless defined $rest[0];
   die "LIMIT 0 Does Not Compute" if $rest[0] == 0;
     # and anyway, SQL::Abstract::Limit will cause a barf if we don't first
-  local $self->{having_bind} = [];
-  my ($sql, @ret) = $self->SUPER::select(
+  my ($sql, @where_bind) = $self->SUPER::select(
     $table, $self->_recurse_fields($fields), $where, $order, @rest
   );
   $sql .= 
@@ -119,7 +121,7 @@ sub select {
     ) :
     ''
   ;
-  return wantarray ? ($sql, @ret, @{$self->{having_bind}}) : $sql;
+  return wantarray ? ($sql, @{$self->{from_bind}}, @where_bind, @{$self->{having_bind}}) : $sql;
 }
 
 sub insert {
@@ -267,11 +269,10 @@ sub _recurse_from {
 }
 
 sub _bind_to_sql {
-  my $self = shift;
-  my $arr  = shift;
-  my $sql = shift @$$arr;
-  $sql =~ s/\?/$self->_quote((shift @$$arr)->[1])/eg;
-  return $sql
+  my ($self, $arr) = @_;
+  my ($sql, @bind) = @{${$arr}};
+  push (@{$self->{from_bind}}, @bind);
+  return $sql;
 }
 
 sub _make_as {
@@ -1239,7 +1240,7 @@ sub _query_end {
 
 sub _dbh_execute {
   my ($self, $dbh, $op, $extra_bind, $ident, $bind_attributes, @args) = @_;
-  
+
   my ($sql, $bind) = $self->_prep_for_execute($op, $extra_bind, $ident, \@args);
 
   $self->_query_start( $sql, @$bind );
@@ -1286,20 +1287,22 @@ sub insert {
   my $ident = $source->from; 
   my $bind_attributes = $self->source_bind_attributes($source);
 
+  my $updated_cols = {};
+
   $self->ensure_connected;
   foreach my $col ( $source->columns ) {
     if ( !defined $to_insert->{$col} ) {
       my $col_info = $source->column_info($col);
 
       if ( $col_info->{auto_nextval} ) {
-        $to_insert->{$col} = $self->_sequence_fetch( 'nextval', $col_info->{sequence} || $self->_dbh_get_autoinc_seq($self->dbh, $source) );
+        $updated_cols->{$col} = $to_insert->{$col} = $self->_sequence_fetch( 'nextval', $col_info->{sequence} || $self->_dbh_get_autoinc_seq($self->dbh, $source) );
       }
     }
   }
 
   $self->_execute('insert' => [], $source, $bind_attributes, $to_insert);
 
-  return $to_insert;
+  return $updated_cols;
 }
 
 ## Still not quite perfect, and EXPERIMENTAL
@@ -1384,14 +1387,6 @@ sub _select_args {
   my ($self, $ident, $select, $condition, $attrs) = @_;
   my $order = $attrs->{order_by};
 
-  if (ref $condition eq 'SCALAR') {
-    my $unwrap = ${$condition};
-    if ($unwrap =~ s/ORDER BY (.*)$//i) {
-      $order = $1;
-      $condition = \$unwrap;
-    }
-  }
-
   my $for = delete $attrs->{for};
   my $sql_maker = $self->sql_maker;
   $sql_maker->{for} = $for;
@@ -1565,9 +1560,18 @@ Return the row id of the last insert.
 =cut
 
 sub _dbh_last_insert_id {
-    my ($self, $dbh, $source, $col) = @_;
-    # XXX This is a SQLite-ism as a default... is there a DBI-generic way?
-    $dbh->func('last_insert_rowid');
+    # All Storage's need to register their own _dbh_last_insert_id
+    # the old SQLite-based method was highly inappropriate
+
+    my $self = shift;
+    my $class = ref $self;
+    $self->throw_exception (<<EOE);
+
+No _dbh_last_insert_id() method found in $class.
+Since the method of obtaining the autoincrement id of the last insert
+operation varies greatly between different databases, this method must be
+individually implemented for every storage class.
+EOE
 }
 
 sub last_insert_id {
@@ -1650,7 +1654,6 @@ sub create_ddl_dir {
 
   foreach my $db (@$databases) {
     $sqlt->reset();
-    $sqlt = $self->configure_sqlt($sqlt, $db);
     $sqlt->{schema} = $sqlt_schema;
     $sqlt->producer($db);
 
@@ -1696,7 +1699,6 @@ sub create_ddl_dir {
       $t->debug( 0 );
       $t->trace( 0 );
       $t->parser( $db )                       or die $t->error;
-      $t = $self->configure_sqlt($t, $db);
       my $out = $t->translate( $prefilename ) or die $t->error;
       $source_schema = $t->schema;
       unless ( $source_schema->name ) {
@@ -1714,7 +1716,6 @@ sub create_ddl_dir {
       $t->debug( 0 );
       $t->trace( 0 );
       $t->parser( $db )                    or die $t->error;
-      $t = $self->configure_sqlt($t, $db);
       my $out = $t->translate( $filename ) or die $t->error;
       $dest_schema = $t->schema;
       $dest_schema->name( $filename )
@@ -1734,17 +1735,6 @@ sub create_ddl_dir {
   }
 }
 
-sub configure_sqlt() {
-  my $self = shift;
-  my $tr = shift;
-  my $db = shift || $self->sqlt_type;
-  if ($db eq 'PostgreSQL') {
-    $tr->quote_table_names(0);
-    $tr->quote_field_names(0);
-  }
-  return $tr;
-}
-
 =head2 deployment_statements
 
 =over 4
@@ -1775,7 +1765,7 @@ sub deployment_statements {
   $type ||= $self->sqlt_type;
   $version ||= $schema->schema_version || '1.x';
   $dir ||= './';
-  my $filename = $schema->ddl_filename($type, $dir, $version);
+  my $filename = $schema->ddl_filename($type, $version, $dir);
   if(-f $filename)
   {
       my $file;
diff --git a/lib/DBIx/Class/Storage/DBI/MultiDistinctEmulation.pm b/lib/DBIx/Class/Storage/DBI/MultiDistinctEmulation.pm
deleted file mode 100644 (file)
index 7ab7846..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-package DBIx::Class::Storage::DBI::MultiDistinctEmulation;
-
-use strict;
-use warnings;
-
-use base qw/DBIx::Class::Storage::DBI/;
-
-sub _select {
-  my ($self, $ident, $select, $condition, $attrs) = @_;
-
-  # hack to make count distincts with multiple columns work in SQLite and Oracle
-  if (ref $select eq 'ARRAY') { 
-      @{$select} = map {$self->replace_distincts($_)} @{$select};
-  } else { 
-      $select = $self->replace_distincts($select);
-  }
-
-  return $self->next::method($ident, $select, $condition, $attrs);
-}
-
-sub replace_distincts {
-    my ($self, $select) = @_;
-
-    $select->{count}->{distinct} = join("||", @{$select->{count}->{distinct}}) 
-       if (ref $select eq 'HASH' && $select->{count} && ref $select->{count} eq 'HASH' && 
-           $select->{count}->{distinct} && ref $select->{count}->{distinct} eq 'ARRAY');
-
-    return $select;
-}
-
-1;
-
-=head1 NAME 
-
-DBIx::Class::Storage::DBI::MultiDistinctEmulation - Some databases can't handle count distincts with multiple cols. They should use base on this.
-
-=head1 SYNOPSIS
-
-=head1 DESCRIPTION
-
-This class allows count distincts with multiple columns for retarded databases (Oracle and SQLite)
-
-=head1 AUTHORS
-
-Luke Saunders <luke.saunders@gmail.com>
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
-
-=cut
index 80f367d..349f658 100644 (file)
@@ -38,6 +38,9 @@ Manually subs in the values for the usual C<?> placeholders.
 
 sub _prep_for_execute {
   my $self = shift;
+
+  my ($op, $extra_bind, $ident) = @_;
+
   my ($sql, $bind) = $self->next::method(@_);
 
   # stringify args, quote via $dbh, and manually insert
@@ -46,12 +49,14 @@ sub _prep_for_execute {
   my $new_sql;
 
   foreach my $bound (@$bind) {
-    shift @$bound;
+    my $col = shift @$bound;
+    my $datatype = 'FIXME!!!';
     foreach my $data (@$bound) {
         if(ref $data) {
             $data = ''.$data;
         }
-        $new_sql .= shift(@sql_part) . $self->_dbh->quote($data);
+        $data = $self->_dbh->quote($data);
+        $new_sql .= shift(@sql_part) . $data;
     }
   }
   $new_sql .= join '', @sql_part;
index 7d5bf10..96ff543 100644 (file)
@@ -2,8 +2,6 @@ package DBIx::Class::Storage::DBI::ODBC::ACCESS;
 use strict;\r
 use warnings;\r
 \r
-use Data::Dump qw( dump );\r
-\r
 use DBI;\r
 use base qw/DBIx::Class::Storage::DBI/;\r
 \r
index 2e9a8c1..e3d08d0 100644 (file)
@@ -26,7 +26,7 @@ This class implements autoincrements for Oracle.
 
 use Carp::Clan qw/^DBIx::Class/;
 
-use base qw/DBIx::Class::Storage::DBI::MultiDistinctEmulation/;
+use base qw/DBIx::Class::Storage::DBI/;
 
 # __PACKAGE__->load_components(qw/PK::Auto/);
 
index 3b99b3f..89979f1 100644 (file)
@@ -313,7 +313,6 @@ has 'write_handler' => (
 
     reload_row
     _prep_for_execute
-    configure_sqlt
     
   /],
 );
index e29c2ee..5e125c9 100644 (file)
@@ -6,7 +6,7 @@ use POSIX 'strftime';
 use File::Copy;
 use File::Spec;
 
-use base qw/DBIx::Class::Storage::DBI::MultiDistinctEmulation/;
+use base qw/DBIx::Class::Storage::DBI/;
 
 sub _dbh_last_insert_id {
   my ($self, $dbh, $source, $col) = @_;
@@ -45,21 +45,6 @@ sub backup
   return $backupfile;
 }
 
-sub disconnect {
-
-  # As described in this node http://www.perlmonks.org/?node_id=666210
-  # there seems to be no sane way to ->disconnect a SQLite database with
-  # cached statement handles. As per mst we just zap the cache and 
-  # proceed as normal.
-
-  my $self = shift;
-  if ($self->connected) {
-    $self->_dbh->{CachedKids} = {};
-    $self->next::method (@_);
-  }
-}
-
-
 1;
 
 =head1 NAME
index ca7cad7..e55691a 100644 (file)
@@ -1,5 +1,4 @@
-package # Hide from pause for now - till we get it working
-  DBIx::Class::Storage::TxnScopeGuard;
+package DBIx::Class::Storage::TxnScopeGuard;
 
 use strict;
 use warnings;
@@ -47,7 +46,7 @@ __END__
 
 =head1 NAME
 
-DBIx::Class::Storage::TxnScopeGuard - Experimental
+DBIx::Class::Storage::TxnScopeGuard - Scope-based transaction handling
 
 =head1 SYNOPSIS
 
@@ -70,14 +69,15 @@ right thing with transactions in DBIx::Class.
 
 =head2 new
 
-Creating an instance of this class will start a new transaction. Expects a
+Creating an instance of this class will start a new transaction (by
+implicitly calling L<DBIx::Class::Storage/txn_begin>. Expects a
 L<DBIx::Class::Storage> object as its only argument.
 
 =head2 commit
 
 Commit the transaction, and stop guarding the scope. If this method is not
-called (i.e. an exception is thrown) and this object goes out of scope then
-the transaction is rolled back.
+called and this object goes out of scope (i.e. an exception is thrown) then
+the transaction is rolled back, via L<DBIx::Class::Storage/txn_rollback>
 
 =cut
 
index bff7ddc..57dad33 100644 (file)
@@ -246,6 +246,8 @@ sub parse {
         if ($source->result_class->can('sqlt_deploy_hook')) {
           $source->result_class->sqlt_deploy_hook($view);
         }
+
+        $source->_invoke_sqlt_deploy_hook($view);
     }
 
     if ($dbicschema->can('sqlt_deploy_hook')) {
@@ -291,14 +293,14 @@ C<SQL::Translator::Parser::DBIx::Class> reads a DBIx::Class schema,
 interrogates the columns, and stuffs it all in an $sqlt_schema object.
 
 It's primary use is in deploying database layouts described as a set
-of L<DBIx::Class> classes, to a database. To do this, see the
-L<DBIx::Class::Schema/deploy> method.
+of L<DBIx::Class> classes, to a database. To do this, see
+L<DBIx::Class::Schema/deploy>.
 
 This can also be achieved by having DBIx::Class export the schema as a
 set of SQL files ready for import into your database, or passed to
 other machines that need to have your application installed but don't
-have SQL::Translator installed. To do this see the
-L<DBIx::Class::Schema/create_ddl_dir> method.
+have SQL::Translator installed. To do this see
+L<DBIx::Class::Schema/create_ddl_dir>.
 
 =head1 SEE ALSO
 
index 18c5292..b82d4f7 100644 (file)
@@ -44,6 +44,11 @@ my $exceptions = {
             qw(setup_connection_class)
         ]
     },
+    'DBIx::Class::Storage::DBI::Sybase' => {
+        ignore => [
+            qw/should_quote_data_type/,
+        ]
+    },
     'DBIx::Class::CDBICompat::AccessorMapping'          => { skip => 1 },
     'DBIx::Class::CDBICompat::AbstractSearch' => {
         ignore => [qw(search_where)]
@@ -94,7 +99,6 @@ my $exceptions = {
     'DBIx::Class::Storage::DBI'                         => { skip => 1 },
     'DBIx::Class::Storage::DBI::DB2'                    => { skip => 1 },
     'DBIx::Class::Storage::DBI::MSSQL'                  => { skip => 1 },
-    'DBIx::Class::Storage::DBI::MultiDistinctEmulation' => { skip => 1 },
     'DBIx::Class::Storage::DBI::ODBC400'                => { skip => 1 },
     'DBIx::Class::Storage::DBI::ODBC::DB2_400_SQL'      => { skip => 1 },
     'DBIx::Class::Storage::DBI::Oracle'                 => { skip => 1 },
index 5869869..d5cb5d1 100644 (file)
@@ -4,6 +4,7 @@
 
 use strict;
 use Test::More tests => 2;
+use MRO::Compat;
 
 {
 package AAA;
@@ -26,8 +27,8 @@ use base 'AAA';
 __PACKAGE__->inject_base( __PACKAGE__, 'DBIx::Class::Core' );
 }
 
-eval { Class::C3::calculateMRO('BBB'); };
+eval { mro::get_linear_isa('BBB'); };
 ok (! $@, "Correctly skipped injecting a direct parent of class BBB");
 
-eval { Class::C3::calculateMRO('CCC'); };
+eval { mro::get_linear_isa('CCC'); };
 ok (! $@, "Correctly skipped injecting an indirect parent of class BBB");
index 5c531fd..05d99b9 100644 (file)
@@ -12,7 +12,7 @@ use DBICTest;
     use base qw/DBIx::Class::ResultSource::Table/;
 }
 
-plan tests => 3;
+plan tests => 4;
 
 my $schema = DBICTest->init_schema();
 my $artist_source = $schema->source('Artist');
@@ -36,6 +36,12 @@ local $SIG{__WARN__} = sub { $warn = shift };
 }
 
 {
+  my $source = $schema->source('DBICTest::Artist');
+  $schema->register_source($source->source_name, $source);
+  is($warn, '', "re-registering an existing source under the same name causes no errors");
+}
+
+{
   my $new_source_name = 'Artist->preview(artist_preview)';
   $schema->register_source( $new_source_name => $new_source );
 
index d8ba469..622eefb 100644 (file)
@@ -19,7 +19,7 @@ use_ok('DBICTest');
 use_ok('DBIC::DebugObj');
 my $schema = DBICTest->init_schema();
 
-diag('Testing against ' . join(' ', map { $schema->storage->dbh->get_info($_) } qw/17 18/));
+#diag('Testing against ' . join(' ', map { $schema->storage->dbh->get_info($_) } qw/17 18/));
 
 $schema->storage->sql_maker->quote_char('`');
 $schema->storage->sql_maker->name_sep('.');
index 748b112..80e6d04 100644 (file)
@@ -19,7 +19,7 @@ use_ok('DBIC::DebugObj');
 
 my $schema = DBICTest->init_schema();
 
-diag('Testing against ' . join(' ', map { $schema->storage->dbh->get_info($_) } qw/17 18/));
+#diag('Testing against ' . join(' ', map { $schema->storage->dbh->get_info($_) } qw/17 18/));
 
 my $dsn = $schema->storage->_dbi_connect_info->[0];
 $schema->connection(
index 993cfad..8f1eba1 100644 (file)
@@ -21,12 +21,11 @@ cmp_ok(@art, '==', 3, "Three artists returned");
 
 # Disconnect the dbh, and be sneaky about it
 # Also test if DBD::SQLite finaly knows how to ->disconnect properly
-TODO: {
-    local $TODO = 'SQLite is evil/braindead. Once this test starts passing, remove the related atrocity from DBIx::Class::Storage::DBI::SQLite';
-    my $w;
-    local $SIG{__WARN__} = sub { $w = shift };
-    $schema->storage->_dbh->disconnect;
-    ok ($w !~ /active statement handles/, 'SQLite can disconnect properly \o/');
+{
+  my $w;
+  local $SIG{__WARN__} = sub { $w = shift };
+  $schema->storage->_dbh->disconnect;
+  ok ($w !~ /active statement handles/, 'SQLite can disconnect properly');
 }
 
 # Try the operation again - What should happen here is:
index ba99fe5..c99528b 100644 (file)
@@ -2,41 +2,65 @@
 
 use strict;
 use warnings;
-use Test::More;
 
 use lib 't/lib';
-
-plan tests => 4;
+use Test::More tests => 8;
 
 sub _chk_warning {
-       defined $_[0]? 
-               $_[0] !~ qr/We found ResultSet class '([^']+)' for '([^']+)', but it seems that you had already set '([^']+)' to use '([^']+)' instead/ :
-               1
+  defined $_[0]?
+    $_[0] !~ qr/We found ResultSet class '([^']+)' for '([^']+)', but it seems that you had already set '([^']+)' to use '([^']+)' instead/ :
+    1
+}
+
+sub _chk_extra_sources_warning {
+  my $p = qr/already has a source, use register_extra_source for additional sources/;
+  defined $_[0]? $_[0] !~ /$p/ : 1;
+}
+
+sub _verify_sources {
+  my @monikers = @_;
+  is_deeply (
+    [ sort DBICNSTest::RtBug41083->sources ],
+    \@monikers,
+    'List of resultsource registrations',
+  );
 }
 
-my $warnings;
-eval {
+{
+  my $warnings;
+  eval {
     local $SIG{__WARN__} = sub { $warnings .= shift };
     package DBICNSTest::RtBug41083;
     use base 'DBIx::Class::Schema';
     __PACKAGE__->load_namespaces(
-       result_namespace => 'Schema_A',
-       resultset_namespace => 'ResultSet_A',
-       default_resultset_class => 'ResultSet'
+      result_namespace => 'Schema_A',
+      resultset_namespace => 'ResultSet_A',
+      default_resultset_class => 'ResultSet'
     );
-};
-ok(!$@) or diag $@;
-ok(_chk_warning($warnings), 'expected no complaint');
+  };
 
-eval {
+  ok(!$@) or diag $@;
+  ok(_chk_warning($warnings), 'expected no resultset complaint');
+  ok(_chk_extra_sources_warning($warnings), 'expected no extra sources complaint') or diag($warnings);
+
+  _verify_sources (qw/A A::Sub/);
+}
+
+{
+  my $warnings;
+  eval {
     local $SIG{__WARN__} = sub { $warnings .= shift };
     package DBICNSTest::RtBug41083;
     use base 'DBIx::Class::Schema';
     __PACKAGE__->load_namespaces(
-       result_namespace => 'Schema',
-       resultset_namespace => 'ResultSet',
-       default_resultset_class => 'ResultSet'
+      result_namespace => 'Schema',
+      resultset_namespace => 'ResultSet',
+      default_resultset_class => 'ResultSet'
     );
-};
-ok(!$@) or diag $@;
-ok(_chk_warning($warnings), 'expected no complaint') or diag $warnings;
+  };
+  ok(!$@) or diag $@;
+  ok(_chk_warning($warnings), 'expected no resultset complaint') or diag $warnings;
+  ok(_chk_extra_sources_warning($warnings), 'expected no extra sources complaint') or diag($warnings);
+
+  _verify_sources (qw/A A::Sub Foo Foo::Sub/);
+}
index 3bc1935..be662a2 100644 (file)
@@ -3,7 +3,9 @@ use warnings;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
+use DBIC::SqlMakerTest;
+
+use_ok('DBICTest');
 
 my $schema = DBICTest->init_schema;
 
@@ -11,11 +13,9 @@ BEGIN {
     eval "use DBD::SQLite";
     plan $@
         ? ( skip_all => 'needs DBD::SQLite for testing' )
-        : ( tests => 7 );
+        : ( tests => 9 );
 }
 
-### $schema->storage->debug(1);
-
 my $where_bind = {
     where => \'name like ?',
     bind  => [ 'Cat%' ],
@@ -55,10 +55,10 @@ my $new_source = $source->new($source);
 $new_source->source_name('Complex');
 
 $new_source->name(\<<'');
-( select a.*, cd.cdid as cdid, cd.title as title, cd.year as year 
-  from artist a
-  join cd on cd.artist=a.artistid
-  where cd.year=?)
+( SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year 
+  FROM artist a
+  JOIN cd ON cd.artist = a.artistid
+  WHERE cd.year = ?)
 
 $schema->register_extra_source('Complex' => $new_source);
 
@@ -72,11 +72,22 @@ $rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] })
     ->search({ 'artistid' => 1 });
 is ( $rs->count, 1, '...cookbook (bind first) + chained search' );
 
-TODO: {
-    # not sure what causes an uninit warning here, please remove when the TODO starts to pass,
-    # so the real reason for the warning can be found and fixed
-    local $SIG{__WARN__} = sub { warn @_ unless $_[0] =~ /uninitialized/ };
+{
+  $rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] })->search({}, { where => \"title LIKE ?", bind => [ 'Spoon%' ] });
+  my ($sql, @bind) = @${$rs->as_query};
+  is_same_sql_bind(
+    $sql, \@bind,
+    "(SELECT me.artistid, me.name, me.rank, me.charfield FROM (SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year FROM artist a JOIN cd ON cd.artist = a.artistid WHERE cd.year = ?) WHERE title LIKE ?)",
+    [
+      [ '!!dummy' => '1999' ], 
+      [ '!!dummy' => 'Spoon%' ]
+    ],
+    'got correct SQL'
+);
 
+}
+
+TODO: {
     local $TODO = 'bind args order needs fixing (semifor)';
     $rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] })
         ->search({ 'artistid' => 1 }, {
index 69f69ef..c3df11f 100644 (file)
@@ -5,29 +5,41 @@
 use strict;
 use warnings;
 
-our @plan;
+use Test::More;
+BEGIN { plan tests => 7 }
 
-BEGIN {
-  eval "require Module::Find;";
-  @plan = $@ ? ( skip_all => 'Could not load Module::Find' )
-    : ( tests => 2 );
-}
+package DBICTest::Taint::Classes;
 
-package DBICTest::Plain;
-
-# Use the Plain test class namespace to avoid the need for a
-# new test infrastructure. If invalid classes will be introduced to
-# 't/lib/DBICTest/Plain/' someday, this has to be reworked.
+use Test::More;
+use Test::Exception;
 
 use lib qw(t/lib);
+use base qw/DBIx::Class::Schema/;
+
+lives_ok (sub {
+  __PACKAGE__->load_classes(qw/Manual/);
+  ok( __PACKAGE__->source('Manual'), 'The Classes::Manual source has been registered' );
+  __PACKAGE__->_unregister_source (qw/Manual/);
+}, 'Loading classes with explicit load_classes worked in taint mode' );
+
+lives_ok (sub {
+  __PACKAGE__->load_classes();
+  ok( __PACKAGE__->source('Auto'), 'The Classes::Auto source has been registered' );
+  ok( __PACKAGE__->source('Auto'), 'The Classes::Manual source has been re-registered' );
+}, 'Loading classes with Module::Find/load_classes worked in taint mode' );
 
-use Test::More @plan;
 
+package DBICTest::Taint::Namespaces;
+
+use Test::More;
+use Test::Exception;
+
+use lib qw(t/lib);
 use base qw/DBIx::Class::Schema/;
 
-eval{ __PACKAGE__->load_classes() };
-cmp_ok( $@, 'eq', '',
-        'Loading classes with Module::Find worked in taint mode' );
-ok( __PACKAGE__->source('Test'), 'The Plain::Test source has been registered' );
+lives_ok (sub {
+  __PACKAGE__->load_namespaces();
+  ok( __PACKAGE__->source('Test'), 'The Namespaces::Test source has been registered' );
+}, 'Loading classes with Module::Find/load_namespaces worked in taint mode' );
 
 1;
index d9d0191..de290de 100644 (file)
@@ -8,7 +8,7 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-plan tests => 90;
+plan tests => 96;
 
 eval { require DateTime::Format::MySQL };
 my $NO_DTFM = $@ ? 1 : 0;
@@ -28,7 +28,7 @@ if( $schema->storage->dbh->get_info(17) eq 'SQLite' &&
 
 my @art = $schema->resultset("Artist")->search({ }, { order_by => 'name DESC'});
 
-cmp_ok(@art, '==', 3, "Three artists returned");
+is(@art, 3, "Three artists returned");
 
 my $art = $art[0];
 
@@ -39,7 +39,7 @@ $art->name('We Are In Rehab');
 is($art->name, 'We Are In Rehab', "Accessor update ok");
 
 my %dirty = $art->get_dirty_columns();
-cmp_ok(scalar(keys(%dirty)), '==', 1, '1 dirty column');
+is(scalar(keys(%dirty)), 1, '1 dirty column');
 ok(grep($_ eq 'name', keys(%dirty)), 'name is dirty');
 
 is($art->get_column("name"), 'We Are In Rehab', 'And via get_column');
@@ -47,7 +47,7 @@ is($art->get_column("name"), 'We Are In Rehab', 'And via get_column');
 ok($art->update, 'Update run');
 
 my %not_dirty = $art->get_dirty_columns();
-cmp_ok(scalar(keys(%not_dirty)), '==', 0, 'Nothing is dirty');
+is(scalar(keys(%not_dirty)), 0, 'Nothing is dirty');
 
 eval {
   my $ret = $art->make_column_dirty('name2');
@@ -55,7 +55,7 @@ eval {
 ok(defined($@), 'Failed to make non-existent column dirty');
 $art->make_column_dirty('name');
 my %fake_dirty = $art->get_dirty_columns();
-cmp_ok(scalar(keys(%fake_dirty)), '==', 1, '1 fake dirty column');
+is(scalar(keys(%fake_dirty)), 1, '1 fake dirty column');
 ok(grep($_ eq 'name', keys(%fake_dirty)), 'name is fake dirty');
 
 my $record_jp = $schema->resultset("Artist")->search(undef, { join => 'cds' })->search(undef, { prefetch => 'cds' })->next;
@@ -68,15 +68,15 @@ ok($record_fn, "funny join is okay");
 
 @art = $schema->resultset("Artist")->search({ name => 'We Are In Rehab' });
 
-cmp_ok(@art, '==', 1, "Changed artist returned by search");
+is(@art, 1, "Changed artist returned by search");
 
-cmp_ok($art[0]->artistid, '==', 3,'Correct artist too');
+is($art[0]->artistid, 3,'Correct artist too');
 
 lives_ok (sub { $art->delete }, 'Cascading delete on Ordered has_many works' );  # real test in ordered.t
 
 @art = $schema->resultset("Artist")->search({ });
 
-cmp_ok(@art, '==', 2, 'And then there were two');
+is(@art, 2, 'And then there were two');
 
 ok(!$art->in_storage, "It knows it's dead");
 
@@ -90,15 +90,15 @@ ok($art->in_storage, "Re-created");
 
 @art = $schema->resultset("Artist")->search({ });
 
-cmp_ok(@art, '==', 3, 'And now there are three again');
+is(@art, 3, 'And now there are three again');
 
 my $new = $schema->resultset("Artist")->create({ artistid => 4 });
 
-cmp_ok($new->artistid, '==', 4, 'Create produced record ok');
+is($new->artistid, 4, 'Create produced record ok');
 
 @art = $schema->resultset("Artist")->search({ });
 
-cmp_ok(@art, '==', 4, "Oh my god! There's four of them!");
+is(@art, 4, "Oh my god! There's four of them!");
 
 $new->set_column('name' => 'Man With A Fork');
 
@@ -152,7 +152,7 @@ is($schema->resultset("Artist")->count, 4, 'count ok');
 my $cd = $schema->resultset("CD")->find(1);
 my %cols = $cd->get_columns;
 
-cmp_ok(keys %cols, '==', 6, 'get_columns number of columns ok');
+is(keys %cols, 6, 'get_columns number of columns ok');
 
 is($cols{title}, 'Spoonful of bees', 'get_columns values ok');
 
@@ -178,6 +178,18 @@ $cd = $schema->resultset("CD")->search(undef, { include_columns => [ 'artist.nam
 is($cd->title, 'Spoonful of bees', 'Correct CD returned with include');
 is($cd->get_column('name'), 'Caterwauler McCrae', 'Additional column returned');
 
+# check if new syntax +columns also works for this
+$cd = $schema->resultset("CD")->search(undef, { '+columns' => [ 'artist.name' ], join => [ 'artist' ] })->find(1);
+
+is($cd->title, 'Spoonful of bees', 'Correct CD returned with include');
+is($cd->get_column('name'), 'Caterwauler McCrae', 'Additional column returned');
+
+# check if new syntax for +columns select specifiers works for this
+$cd = $schema->resultset("CD")->search(undef, { '+columns' => [ {artist_name => 'artist.name'} ], join => [ 'artist' ] })->find(1);
+
+is($cd->title, 'Spoonful of bees', 'Correct CD returned with include');
+is($cd->get_column('artist_name'), 'Caterwauler McCrae', 'Additional column returned');
+
 # update_or_insert
 $new = $schema->resultset("Track")->new( {
   trackid => 100,
@@ -223,41 +235,58 @@ my $search = [ { 'tags.tag' => 'Cheesy' }, { 'tags.tag' => 'Blue' } ];
 my( $or_rs ) = $schema->resultset("CD")->search_rs($search, { join => 'tags',
                                                   order_by => 'cdid' });
 
-cmp_ok($or_rs->count, '==', 5, 'Search with OR ok');
+is($or_rs->count, 5, 'Search with OR ok');
 
 my $distinct_rs = $schema->resultset("CD")->search($search, { join => 'tags', distinct => 1 });
-cmp_ok($distinct_rs->all, '==', 4, 'DISTINCT search with OR ok');
+is($distinct_rs->all, 4, 'DISTINCT search with OR ok');
 
 SKIP: {
-  skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 1
+  skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 2
     if $is_broken_sqlite;
 
-  my $tcount = $schema->resultset("Track")->search(
+  my $tcount = $schema->resultset('Track')->search(
     {},
-    {       
-       select => {count => {distinct => ['position', 'title']}},
-          as => ['count']
+    {
+      select => [ qw/position title/ ],
+      distinct => 1,
     }
   );
-  cmp_ok($tcount->next->get_column('count'), '==', 13, 'multiple column COUNT DISTINCT ok');
+  is($tcount->count, 13, 'multiple column COUNT DISTINCT ok');
 
+  $tcount = $schema->resultset('Track')->search(
+    {},
+    {
+      columns => [ qw/position title/ ],
+      distinct => 1,
+    }
+  );
+  is($tcount->count, 13, 'multiple column COUNT DISTINCT ok');
+
+  $tcount = $schema->resultset('Track')->search(
+    {},
+    {
+       group_by => [ qw/position title/ ]
+    }
+  );
+  is($tcount->count, 13, 'multiple column COUNT DISTINCT using column syntax ok');  
 }
+
 my $tag_rs = $schema->resultset('Tag')->search(
                [ { 'me.tag' => 'Cheesy' }, { 'me.tag' => 'Blue' } ]);
 
 my $rel_rs = $tag_rs->search_related('cd');
 
-cmp_ok($rel_rs->count, '==', 5, 'Related search ok');
+is($rel_rs->count, 5, 'Related search ok');
 
-cmp_ok($or_rs->next->cdid, '==', $rel_rs->next->cdid, 'Related object ok');
+is($or_rs->next->cdid, $rel_rs->next->cdid, 'Related object ok');
 $or_rs->reset;
 $rel_rs->reset;
 
 my $tag = $schema->resultset('Tag')->search(
                [ { 'me.tag' => 'Blue' } ], { cols=>[qw/tagid/] } )->next;
 
-cmp_ok($tag->has_column_loaded('tagid'), '==', 1, 'Has tagid loaded');
-cmp_ok($tag->has_column_loaded('tag'), '==', 0, 'Has not tag  loaded');
+ok($tag->has_column_loaded('tagid'), 'Has tagid loaded');
+ok(!$tag->has_column_loaded('tag'), 'Has not tag loaded');
 
 ok($schema->storage(), 'Storage available');
 
@@ -289,7 +318,7 @@ ok($schema->storage(), 'Storage available');
   ok($schema->source('SourceNameArtists'), 'SourceNameArtists result source exists');
 
   my @artsn = $schema->resultset('SourceNameArtists')->search({}, { order_by => 'name DESC' });
-  cmp_ok(@artsn, '==', 4, "Four artists returned");
+  is(@artsn, 4, "Four artists returned");
   
   # make sure subclasses that don't set source_name are ok
   ok($schema->source('ArtistSubclass'), 'ArtistSubclass exists');
@@ -303,8 +332,8 @@ lives_ok (sub { my $newlink = $newbook->link}, "stringify to false value doesn't
 {
   my $art_del = $schema->resultset("Artist")->find({ artistid => 1 });
   lives_ok (sub { $art_del->delete }, 'Cascading delete on Ordered has_many works' );  # real test in ordered.t
-  cmp_ok( $schema->resultset("CD")->search({artist => 1}), '==', 0, 'Cascading through has_many top level.');
-  cmp_ok( $schema->resultset("CD_to_Producer")->search({cd => 1}), '==', 0, 'Cascading through has_many children.');
+  is( $schema->resultset("CD")->search({artist => 1}), 0, 'Cascading through has_many top level.');
+  is( $schema->resultset("CD_to_Producer")->search({cd => 1}), 0, 'Cascading through has_many children.');
 }
 
 # test column_info
index 513c6d3..e9053a3 100644 (file)
@@ -1,19 +1,14 @@
 use strict;
 use warnings;  
 
-use Test::More tests => 3;
+use Test::More tests => 2;
 use lib qw(t/lib);
 use DBICTest;
 use DBICTest::Schema;
 use DBICTest::Schema::Artist;
 
 DBICTest::Schema::Artist->source_name('MyArtist');
-{
-    my $w;
-    local $SIG{__WARN__} = sub { $w = shift };
-    DBICTest::Schema->register_class('FooA', 'DBICTest::Schema::Artist');
-    like ($w, qr/use register_extra_source/, 'Complain about using register_class on an already-registered class');
-}
+DBICTest::Schema->register_class('FooA', 'DBICTest::Schema::Artist');
 
 my $schema = DBICTest->init_schema();
 
diff --git a/t/63register_source.t b/t/63register_source.t
new file mode 100644 (file)
index 0000000..6951962
--- /dev/null
@@ -0,0 +1,18 @@
+use strict;
+use warnings;
+
+use Test::Exception tests => 1;
+use lib qw(t/lib);
+use DBICTest;
+use DBICTest::Schema;
+use DBIx::Class::ResultSource::Table;
+
+my $schema = DBICTest->init_schema();
+
+my $foo = DBIx::Class::ResultSource::Table->new({ name => "foo" });
+my $bar = DBIx::Class::ResultSource::Table->new({ name => "bar" });
+
+lives_ok {
+    $schema->register_source(foo => $foo);
+    $schema->register_source(bar => $bar);
+} 'multiple classless sources can be registered';
index 7631439..14ad6e0 100644 (file)
--- a/t/64db.t
+++ b/t/64db.t
@@ -64,6 +64,11 @@ TODO: {
       'rank' => {
           'data_type' => 'integer',
           'is_nullable' => 0,
+          'default_value' => '13',
+      },
+      'charfield' => {
+          'data_type' => 'char',
+          'is_nullable' => 1,
       },
     },
     'Correctly retrieve column info (mixed null and non-null columns)'
index 3b67e45..80c5524 100644 (file)
@@ -8,7 +8,7 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-plan tests => 69;
+plan tests => 74;
 
 # has_a test
 my $cd = $schema->resultset("CD")->find(4);
@@ -35,10 +35,15 @@ is( $cds[1]->title, 'Forkful of bees', 'search_related with abstract query ok' )
 if ($INC{'DBICTest/HelperRels.pm'}) {
   $artist->add_to_cds({ title => 'Big Flop', year => 2005 });
 } else {
-  $artist->create_related( 'cds', {
+  my $big_flop = $artist->create_related( 'cds', {
       title => 'Big Flop',
       year => 2005,
   } );
+
+ SKIP:{
+    skip "Can't fix right now", 1 if $DBIx::Class::VERSION < 0.09;
+    lives_ok { $big_flop->genre} "Don't throw exception when col is not loaded after insert";
+  };
 }
 
 my $big_flop_cd = ($artist->search_related('cds'))[3];
@@ -138,6 +143,15 @@ my $newartist = $cd->find_or_new_related( 'artist', {
 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');
 
+lives_ok( 
+    sub { 
+        my $new_bookmark = $schema->resultset("Bookmark")->new_result( {} );
+        my $new_related_link = $new_bookmark->new_related( 'link', {} );
+    },
+    'No back rel'
+);
+
+
 TODO: {
   local $TODO = "relationship checking needs fixing";
   # try to add a bogus relationship using the wrong cols
@@ -275,3 +289,14 @@ cmp_ok($relinfo->{attrs}{is_foreign_key_constraint}, '==', 1, "is_foreign_key_co
 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.");
+
+# check that relationships below left join relationships are forced to left joins 
+# when traversing multiple belongs_to
+my $cds = $schema->resultset("CD")->search({ 'me.cdid' => 5 }, { join => { single_track => 'cd' } });
+is($cds->count, 1, "subjoins under left joins force_left (string)");
+
+$cds = $schema->resultset("CD")->search({ 'me.cdid' => 5 }, { join => { single_track => ['cd'] } });
+is($cds->count, 1, "subjoins under left joins force_left (arrayref)");
+
+$cds = $schema->resultset("CD")->search({ 'me.cdid' => 5 }, { join => { single_track => { cd => {} } } });
+is($cds->count, 1, "subjoins under left joins force_left (hashref)");
index 7138989..3532900 100644 (file)
@@ -115,3 +115,12 @@ for my $index (0 .. $#hashrefinf) {
         is ($track->get_column ($col), $datahashref->{cds}{tracks}{$col}, "Correct track '$col'");
     }
 }
+
+# check for same query as above but using extended columns syntax
+$rs_hashrefinf = $schema->resultset ('Artist')->search ({ 'me.artistid' => 1}, {
+    join     => { cds => 'tracks' },
+    columns  => {name => 'name', 'cds.tracks.title' => 'tracks.title', 'cds.tracks.cd' => 'tracks.cd'},
+    order_by => [qw/cds.cdid tracks.trackid/],
+});
+$rs_hashrefinf->result_class('DBIx::Class::ResultClass::HashRefInflator');
+is_deeply [$rs_hashrefinf->all], \@hashrefinf, 'Check query using extended columns syntax';
index dbba1cd..467b4d3 100644 (file)
@@ -108,7 +108,7 @@ NULLINSEARCH: {
     => 'Created an artist resultset of undef';
     
     TODO: {
-       $TODO = "need to fix the row count =1 when select * from table where pk IS NULL problem";
+       local $TODO = "need to fix the row count =1 when select * from table where pk IS NULL problem";
            is $artist2_rs->count, 0
            => 'got no rows';           
     }
index 45e614f..2178ce0 100644 (file)
--- a/t/72pg.t
+++ b/t/72pg.t
@@ -15,10 +15,15 @@ use DBICTest;
 
   __PACKAGE__->load_components(qw/Core/);
   __PACKAGE__->table('testschema.casecheck');
-  __PACKAGE__->add_columns(qw/id name NAME uc_name/);
+  __PACKAGE__->add_columns(qw/id name NAME uc_name storecolumn/);
   __PACKAGE__->column_info_from_storage(1);
   __PACKAGE__->set_primary_key('id');
 
+  sub store_column {
+    my ($self, $name, $value) = @_;
+    $value = '#'.$value if($name eq "storecolumn");
+    $self->maybe::next::method($name, $value);
+  }
 }
 
 {
@@ -45,7 +50,7 @@ plan skip_all => 'Set $ENV{DBICTEST_PG_DSN}, _USER and _PASS to run this test '.
     unless ($dsn && $user);
 
 
-plan tests => 37;
+plan tests => 39;
 
 DBICTest::Schema->load_classes( 'Casecheck', 'ArrayTest' );
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
@@ -73,10 +78,17 @@ $schema->source("SequenceTest")->name("testschema.sequence_test");
     $dbh->do("CREATE SEQUENCE pkid1_seq START 1 MAXVALUE 999999 MINVALUE 0");
     $dbh->do("CREATE SEQUENCE pkid2_seq START 10 MAXVALUE 999999 MINVALUE 0");
     $dbh->do("CREATE SEQUENCE nonpkid_seq START 20 MAXVALUE 999999 MINVALUE 0");
-    ok ( $dbh->do('CREATE TABLE testschema.casecheck (id serial PRIMARY KEY, "name" VARCHAR(1), "NAME" VARCHAR(2), "UC_NAME" VARCHAR(3));'), 'Creation of casecheck table');
+    ok ( $dbh->do('CREATE TABLE testschema.casecheck (id serial PRIMARY KEY, "name" VARCHAR(1), "NAME" VARCHAR(2), "UC_NAME" VARCHAR(3), "storecolumn" VARCHAR(10));'), 'Creation of casecheck table');
     ok ( $dbh->do('CREATE TABLE testschema.array_test (id serial PRIMARY KEY, arrayfield INTEGER[]);'), 'Creation of array_test table');
 }
 
+# store_column is called once for create() for non sequence columns
+
+ok(my $storecolumn = $schema->resultset('Casecheck')->create({'storecolumn' => 'a'}));
+
+is($storecolumn->storecolumn, '#a'); # was '##a'
+
+
 # This is in Core now, but it's here just to test that it doesn't break
 $schema->class('Artist')->load_components('PK::Auto');
 
@@ -130,10 +142,7 @@ like($artistid_defval,
 is_deeply($type_info, $test_type_info,
           'columns_info_for - column data types');
 
-SKIP: {
-  skip "SQL::Abstract < 1.49 does not pass through arrayrefs", 4
-    if $SQL::Abstract::VERSION < 1.49;
-
+{
   lives_ok {
     $schema->resultset('ArrayTest')->create({
       arrayfield => [1, 2],
@@ -153,7 +162,7 @@ SKIP: {
   my $count;
   lives_ok {
     $count = $schema->resultset('ArrayTest')->search({
-      arrayfield => \[ '= ?' => [arrayfield => [3, 4]] ],   #TODO anything less ugly than this?
+      arrayfield => \[ '= ?' => [arrayfield => [3, 4]] ],   #Todo anything less ugly than this?
     })->count;
   } 'comparing arrayref to pg array data does not blow up';
   is($count, 1, 'comparing arrayref to pg array data gives correct result');
@@ -246,19 +255,15 @@ SKIP: {
     });
 }
 
-SKIP: {
-  skip "Oracle Auto-PK tests are broken", 16;
-
-  # test auto increment using sequences WITHOUT triggers
-  for (1..5) {
+for (1..5) {
     my $st = $schema->resultset('SequenceTest')->create({ name => 'foo' });
     is($st->pkid1, $_, "Oracle Auto-PK without trigger: First primary key");
     is($st->pkid2, $_ + 9, "Oracle Auto-PK without trigger: Second primary key");
     is($st->nonpkid, $_ + 19, "Oracle Auto-PK without trigger: Non-primary key");
-  }
-  my $st = $schema->resultset('SequenceTest')->create({ name => 'foo', pkid1 => 55 });
-  is($st->pkid1, 55, "Oracle Auto-PK without trigger: First primary key set manually");
 }
+my $st = $schema->resultset('SequenceTest')->create({ name => 'foo', pkid1 => 55 });
+is($st->pkid1, 55, "Oracle Auto-PK without trigger: First primary key set manually");
+
 
 END {
     if($dbh) {
@@ -272,4 +277,3 @@ END {
         $dbh->do("DROP SCHEMA testschema;");
     }
 }
-
index 51cc932..c5fe45a 100644 (file)
@@ -39,7 +39,7 @@ plan skip_all => 'Set $ENV{DBICTEST_ORA_DSN}, _USER and _PASS to run this test.
   ' as well as following sequences: \'pkid1_seq\', \'pkid2_seq\' and \'nonpkid_seq\''
   unless ($dsn && $user && $pass);
 
-plan tests => 24;
+plan tests => 26;
 
 DBICTest::Schema->load_classes('ArtistFQN');
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
@@ -106,15 +106,32 @@ is($tjoin->next->title, 'Track1', "ambiguous column ok");
 
 # check count distinct with multiple columns
 my $other_track = $schema->resultset('Track')->create({ trackid => 2, cd => 1, position => 1, title => 'Track2' });
+
 my $tcount = $schema->resultset('Track')->search(
-    {},
-    {
-        select => [{count => {distinct => ['position', 'title']}}],
-        as => ['count']
-    }
-  );
+  {},
+  {
+    select => [ qw/position title/ ],
+    distinct => 1,
+  }
+);
+is($tcount->count, 2, 'multiple column COUNT DISTINCT ok');
+
+$tcount = $schema->resultset('Track')->search(
+  {},
+  {
+    columns => [ qw/position title/ ],
+    distinct => 1,
+  }
+);
+is($tcount->count, 2, 'multiple column COUNT DISTINCT ok');
 
-is($tcount->next->get_column('count'), 2, "multiple column select distinct ok");
+$tcount = $schema->resultset('Track')->search(
+  {},
+  { 
+     group_by => [ qw/position title/ ]
+  }
+);
+is($tcount->count, 2, 'multiple column COUNT DISTINCT using column syntax ok');
 
 # test LIMIT support
 for (1..6) {
index 238f27a..92b3103 100644 (file)
@@ -40,7 +40,7 @@ ok($new->artistid, "Auto-PK worked");
 
 # Test LIMIT
 for (1..6) {
-    $schema->resultset('Artist')->create( { name => 'Artist ' . $_ } );
+    $schema->resultset('Artist')->create( { name => 'Artist ' . $_, rank => $_ } );
 }
 
 my $it = $schema->resultset('Artist')->search( { },
index 4325a70..40045b4 100644 (file)
@@ -5,10 +5,11 @@ use Test::More;
 use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
+use DBIC::SqlMakerTest;
 
 my $schema = DBICTest->init_schema();
 
-plan tests => 12;
+plan tests => 24;
 
 my $rs = $schema->resultset('CD')->search({},
     {
@@ -28,6 +29,16 @@ $rs = $schema->resultset('CD')->search({},
 lives_ok(sub { $rs->first->get_column('count') }, 'multiple +select/+as columns, 1st rscolumn present');
 lives_ok(sub { $rs->first->get_column('addedtitle') }, 'multiple +select/+as columns, 2nd rscolumn present');
 
+# Tests a regression in ResultSetColumn wrt +select
+$rs = $schema->resultset('CD')->search(undef,
+    {
+        '+select'   => [ \'COUNT(*) AS year_count' ],
+               order_by => 'year_count'
+       }
+);
+my @counts = $rs->get_column('cdid')->all;
+ok(scalar(@counts), 'got rows from ->all using +select');
+
 $rs = $schema->resultset('CD')->search({},
     {
         '+select'   => [ \ 'COUNT(*)', 'title' ],
@@ -63,3 +74,128 @@ is ($subsel->next->title, $cds->next->title, 'First CD title match');
 is ($subsel->next->title, $cds->next->title, 'Second CD title match');
 
 is($schema->resultset('CD')->current_source_alias, "me", '$rs->current_source_alias returns "me"');
+
+
+
+$rs = $schema->resultset('CD')->search({},
+    {
+        'join' => 'artist',
+        'columns' => ['cdid', 'title', 'artist.name'],
+    }
+);
+
+my ($sql, @bind) = @${$rs->as_query};
+is_same_sql_bind (
+  $sql,
+  \@bind,
+  '(SELECT me.cdid, me.title, artist.name FROM cd me  JOIN artist artist ON artist.artistid = me.artist)',
+  [],
+  'Use of columns attribute results in proper sql'
+);
+
+lives_ok(sub {
+  $rs->first->get_column('cdid')
+}, 'columns 1st rscolumn present');
+
+lives_ok(sub {
+  $rs->first->get_column('title')
+}, 'columns 2nd rscolumn present');
+
+lives_ok(sub {
+  $rs->first->artist->get_column('name') 
+}, 'columns 3rd rscolumn present'); 
+
+
+
+$rs = $schema->resultset('CD')->search({},
+    {  
+        'join' => 'artist',
+        '+columns' => ['cdid', 'title', 'artist.name'],
+    }
+);
+
+($sql, @bind) = @${$rs->as_query};
+is_same_sql_bind (
+  $sql,
+  \@bind,
+  '(SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track, me.cdid, me.title, artist.name FROM cd me  JOIN artist artist ON artist.artistid = me.artist)',
+  [],
+  'Use of columns attribute results in proper sql'
+);
+
+lives_ok(sub {
+  $rs->first->get_column('cdid') 
+}, 'columns 1st rscolumn present');
+
+lives_ok(sub {
+  $rs->first->get_column('title')
+}, 'columns 2nd rscolumn present');
+
+lives_ok(sub {
+  $rs->first->artist->get_column('name')
+}, 'columns 3rd rscolumn present');
+
+
+$rs = $schema->resultset('CD')->search({'tracks.position' => { -in => [2] } },
+  {
+    join => 'tracks',
+    columns => [qw/me.cdid me.title/],
+    '+select' => ['tracks.position'],
+    '+as' => ['track_position'],
+
+    # get a hashref of CD1 only (the first with a second track)
+    result_class => 'DBIx::Class::ResultClass::HashRefInflator',
+    order_by => 'cdid',
+    rows => 1,
+  }
+);
+
+is_deeply (
+  $rs->single,
+  {
+    cdid => 1,
+    track_position => 2,
+    title => 'Spoonful of bees',
+  },
+  'limited prefetch via column works on a multi-relationship',
+);
+
+my $sub_rs = $rs->search ({},
+  {
+    columns => [qw/artist tracks.trackid/],    # columns should not be merged but override $rs columns
+    '+select' => ['tracks.title'],
+    '+as' => ['tracks.title'],
+  }
+);
+
+is_deeply (
+  $sub_rs->single,
+  {
+    artist => 1,
+    track_position => 2,
+    tracks =>
+      {
+        trackid => 17,
+        title => 'Apiary',
+      },
+  },
+  'columns/select/as fold properly on sub-searches',
+);
+
+TODO: {
+  local $TODO = "Multi-collapsing still doesn't work right - HRI should be getting an arrayref, not an individual hash";
+  is_deeply (
+    $sub_rs->single,
+    {
+      artist => 1,
+      track_position => 2,
+      tracks => [
+        {
+          trackid => 17,
+          title => 'Apiary',
+        },
+      ],
+    },
+    'columns/select/as fold properly on sub-searches',
+  );
+}
diff --git a/t/77prefetch.t b/t/77prefetch.t
deleted file mode 100644 (file)
index e6273c6..0000000
+++ /dev/null
@@ -1,477 +0,0 @@
-use strict;
-use warnings;  
-
-use Test::More;
-use Test::Exception;
-use lib qw(t/lib);
-use DBICTest;
-use Data::Dumper;
-
-my $schema = DBICTest->init_schema();
-
-my $orig_debug = $schema->storage->debug;
-
-use IO::File;
-
-BEGIN {
-    eval "use DBD::SQLite";
-    plan $@
-        ? ( skip_all => 'needs DBD::SQLite for testing' )
-        : ( tests => 68 );
-}
-
-# figure out if we've got a version of sqlite that is older than 3.2.6, in
-# which case COUNT(DISTINCT()) doesn't work
-my $is_broken_sqlite = 0;
-my ($sqlite_major_ver,$sqlite_minor_ver,$sqlite_patch_ver) =
-    split /\./, $schema->storage->dbh->get_info(18);
-if( $schema->storage->dbh->get_info(17) eq 'SQLite' &&
-    ( ($sqlite_major_ver < 3) ||
-      ($sqlite_major_ver == 3 && $sqlite_minor_ver < 2) ||
-      ($sqlite_major_ver == 3 && $sqlite_minor_ver == 2 && $sqlite_patch_ver < 6) ) ) {
-    $is_broken_sqlite = 1;
-}
-
-# bug in 0.07000 caused attr (join/prefetch) to be modifed by search
-# so we check the search & attr arrays are not modified
-my $search = { 'artist.name' => 'Caterwauler McCrae' };
-my $attr = { prefetch => [ qw/artist liner_notes/ ],
-             order_by => 'me.cdid' };
-my $search_str = Dumper($search);
-my $attr_str = Dumper($attr);
-
-my $rs = $schema->resultset("CD")->search($search, $attr);
-
-is(Dumper($search), $search_str, 'Search hash untouched after search()');
-is(Dumper($attr), $attr_str, 'Attribute hash untouched after search()');
-cmp_ok($rs + 0, '==', 3, 'Correct number of records returned');
-
-# A search() with prefetch seems to pollute an already joined resultset
-# in a way that offsets future joins (adapted from a test case by Debolaz)
-{
-  my ($cd_rs, $attrs);
-
-  # test a real-life case - rs is obtained by an implicit m2m join
-  $cd_rs = $schema->resultset ('Producer')->first->cds;
-  $attrs = Dumper $cd_rs->{attrs};
-
-  $cd_rs->search ({})->all;
-  is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after a simple search');
-
-  lives_ok (sub {
-    $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
-    is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after search with prefetch');
-  }, 'first prefetching search ok');
-
-  lives_ok (sub {
-    $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
-    is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after another search with prefetch')
-  }, 'second prefetching search ok');
-
-
-  # test a regular rs with an empty seen_join injected - it should still work!
-  $cd_rs = $schema->resultset ('CD');
-  $cd_rs->{attrs}{seen_join}  = {};
-  $attrs = Dumper $cd_rs->{attrs};
-
-  $cd_rs->search ({})->all;
-  is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after a simple search');
-
-  lives_ok (sub {
-    $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
-    is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after search with prefetch');
-  }, 'first prefetching search ok');
-
-  lives_ok (sub {
-    $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
-    is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after another search with prefetch')
-  }, 'second prefetching search ok');
-}
-
-
-my $queries = 0;
-$schema->storage->debugcb(sub { $queries++; });
-$schema->storage->debug(1);
-
-my @cd = $rs->all;
-
-is($cd[0]->title, 'Spoonful of bees', 'First record returned ok');
-
-ok(!defined $cd[0]->liner_notes, 'No prefetch for NULL LEFT join');
-
-is($cd[1]->{_relationship_data}{liner_notes}->notes, 'Buy Whiskey!', 'Prefetch for present LEFT JOIN');
-
-is(ref $cd[1]->liner_notes, 'DBICTest::LinerNotes', 'Prefetch returns correct class');
-
-is($cd[2]->{_inflated_column}{artist}->name, 'Caterwauler McCrae', 'Prefetch on parent object ok');
-
-is($queries, 1, 'prefetch ran only 1 select statement');
-
-$schema->storage->debug($orig_debug);
-$schema->storage->debugobj->callback(undef);
-
-# test for partial prefetch via columns attr
-my $cd = $schema->resultset('CD')->find(1,
-    {
-      columns => [qw/title artist artist.name/], 
-      join => { 'artist' => {} }
-    }
-);
-ok(eval { $cd->artist->name eq 'Caterwauler McCrae' }, 'single related column prefetched');
-
-# start test for nested prefetch SELECT count
-$queries = 0;
-$schema->storage->debugcb(sub { $queries++ });
-$schema->storage->debug(1);
-
-$rs = $schema->resultset('Tag')->search(
-  {},
-  {
-    prefetch => { cd => 'artist' }
-  }
-);
-
-my $tag = $rs->first;
-
-is( $tag->cd->title, 'Spoonful of bees', 'step 1 ok for nested prefetch' );
-
-is( $tag->cd->artist->name, 'Caterwauler McCrae', 'step 2 ok for nested prefetch');
-
-# count the SELECTs
-#$selects++ if /SELECT(?!.*WHERE 1=0.*)/;
-is($queries, 1, 'nested prefetch ran exactly 1 select statement (excluding column_info)');
-
-$queries = 0;
-
-is($tag->search_related('cd')->search_related('artist')->first->name,
-   'Caterwauler McCrae',
-   'chained belongs_to->belongs_to search_related ok');
-
-is($queries, 0, 'chained search_related after belontgs_to->belongs_to prefetch ran no queries');
-
-$queries = 0;
-
-$cd = $schema->resultset('CD')->find(1, { prefetch => 'artist' });
-
-is($cd->{_inflated_column}{artist}->name, 'Caterwauler McCrae', 'artist prefetched correctly on find');
-
-is($queries, 1, 'find with prefetch ran exactly 1 select statement (excluding column_info)');
-
-$queries = 0;
-
-$schema->storage->debugcb(sub { $queries++; });
-
-$cd = $schema->resultset('CD')->find(1, { prefetch => { cd_to_producer => 'producer' } });
-
-is($cd->producers->first->name, 'Matt S Trout', 'many_to_many accessor ok');
-
-is($queries, 1, 'many_to_many accessor with nested prefetch ran exactly 1 query');
-
-$queries = 0;
-
-my $producers = $cd->search_related('cd_to_producer')->search_related('producer');
-
-is($producers->first->name, 'Matt S Trout', 'chained many_to_many search_related ok');
-
-is($queries, 0, 'chained search_related after many_to_many prefetch ran no queries');
-
-$schema->storage->debug($orig_debug);
-$schema->storage->debugobj->callback(undef);
-
-$rs = $schema->resultset('Tag')->search(
-  {},
-  {
-    join => { cd => 'artist' },
-    prefetch => { cd => 'artist' }
-  }
-);
-
-cmp_ok( $rs->count, '>=', 0, 'nested prefetch does not duplicate joins' );
-
-my ($artist) = $schema->resultset("Artist")->search({ 'cds.year' => 2001 },
-                 { order_by => 'artistid DESC', join => 'cds' });
-
-is($artist->name, 'Random Boy Band', "Join search by object ok");
-
-my @cds = $schema->resultset("CD")->search({ 'liner_notes.notes' => 'Buy Merch!' },
-                               { join => 'liner_notes' });
-
-cmp_ok(scalar @cds, '==', 1, "Single CD retrieved via might_have");
-
-is($cds[0]->title, "Generic Manufactured Singles", "Correct CD retrieved");
-
-my @artists = $schema->resultset("Artist")->search({ 'tags.tag' => 'Shiny' },
-                                       { join => { 'cds' => 'tags' } });
-
-cmp_ok( @artists, '==', 2, "two-join search ok" );
-
-$rs = $schema->resultset("CD")->search(
-  {},
-  { group_by => [qw/ title me.cdid /] }
-);
-
-SKIP: {
-    skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 1
-        if $is_broken_sqlite;
-    cmp_ok( $rs->count, '==', 5, "count() ok after group_by on main pk" );
-}
-
-cmp_ok( scalar $rs->all, '==', 5, "all() returns same count as count() after group_by on main pk" );
-
-$rs = $schema->resultset("CD")->search(
-  {},
-  { join => [qw/ artist /], group_by => [qw/ artist.name /] }
-);
-
-SKIP: {
-    skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 1
-        if $is_broken_sqlite;
-    cmp_ok( $rs->count, '==', 3, "count() ok after group_by on related column" );
-}
-
-$rs = $schema->resultset("Artist")->search(
-  {},
-      { join => [qw/ cds /], group_by => [qw/ me.name /], having =>{ 'MAX(cds.cdid)'=> \'< 5' } }
-);
-
-cmp_ok( $rs->all, '==', 2, "results ok after group_by on related column with a having" );
-
-$rs = $rs->search( undef, {  having =>{ 'count(*)'=> \'> 2' }});
-
-cmp_ok( $rs->all, '==', 1, "count() ok after group_by on related column with a having" );
-
-$rs = $schema->resultset("Artist")->search(
-        { 'cds.title' => 'Spoonful of bees',
-          'cds_2.title' => 'Forkful of bees' },
-        { join => [ 'cds', 'cds' ] });
-
-SKIP: {
-    skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 1
-        if $is_broken_sqlite;
-    cmp_ok($rs->count, '==', 1, "single artist returned from multi-join");
-}
-
-is($rs->next->name, 'Caterwauler McCrae', "Correct artist returned");
-
-$cd = $schema->resultset('Artist')->first->create_related('cds',
-    {
-    title   => 'Unproduced Single',
-    year    => 2007
-});
-
-my $left_join = $schema->resultset('CD')->search(
-    { 'me.cdid' => $cd->cdid },
-    { prefetch => { cd_to_producer => 'producer' } }
-);
-
-cmp_ok($left_join, '==', 1, 'prefetch with no join record present');
-
-$queries = 0;
-$schema->storage->debugcb(sub { $queries++ });
-$schema->storage->debug(1);
-
-my $tree_like =
-     $schema->resultset('TreeLike')->find(5,
-       { join     => { parent => { parent => 'parent' } },
-         prefetch => { parent => { parent => 'parent' } } });
-
-is($tree_like->name, 'quux', 'Bottom of tree ok');
-$tree_like = $tree_like->parent;
-is($tree_like->name, 'baz', 'First level up ok');
-$tree_like = $tree_like->parent;
-is($tree_like->name, 'bar', 'Second level up ok');
-$tree_like = $tree_like->parent;
-is($tree_like->name, 'foo', 'Third level up ok');
-
-$schema->storage->debug($orig_debug);
-$schema->storage->debugobj->callback(undef);
-
-cmp_ok($queries, '==', 1, 'Only one query run');
-
-$tree_like = $schema->resultset('TreeLike')->search({'me.id' => 2});
-$tree_like = $tree_like->search_related('children')->search_related('children')->search_related('children')->first;
-is($tree_like->name, 'quux', 'Tree search_related ok');
-
-$tree_like = $schema->resultset('TreeLike')->search_related('children',
-    { 'children.id' => 3, 'children_2.id' => 4 },
-    { prefetch => { children => 'children' } }
-  )->first;
-is(eval { $tree_like->children->first->children->first->name }, 'quux',
-   'Tree search_related with prefetch ok');
-
-$tree_like = eval { $schema->resultset('TreeLike')->search(
-    { 'children.id' => 3, 'children_2.id' => 6 }, 
-    { join => [qw/children children/] }
-  )->search_related('children', { 'children_4.id' => 7 }, { prefetch => 'children' }
-  )->first->children->first; };
-is(eval { $tree_like->name }, 'fong', 'Tree with multiple has_many joins ok');
-
-# test that collapsed joins don't get a _2 appended to the alias
-
-my $sql = '';
-$schema->storage->debugcb(sub { $sql = $_[1] });
-$schema->storage->debug(1);
-
-eval {
-  my $row = $schema->resultset('Artist')->search_related('cds', undef, {
-    join => 'tracks',
-    prefetch => 'tracks',
-  })->search_related('tracks')->first;
-};
-
-like( $sql, qr/^SELECT tracks_2\.trackid/, "join not collapsed for search_related" );
-
-$schema->storage->debug($orig_debug);
-$schema->storage->debugobj->callback(undef);
-
-$rs = $schema->resultset('Artist');
-$rs->create({ artistid => 4, name => 'Unknown singer-songwriter' });
-$rs->create({ artistid => 5, name => 'Emo 4ever' });
-@artists = $rs->search(undef, { prefetch => 'cds', order_by => 'artistid' });
-is(scalar @artists, 5, 'has_many prefetch with adjacent empty rows ok');
-
-# -------------
-#
-# Tests for multilevel has_many prefetch
-
-# artist resultsets - with and without prefetch
-my $art_rs = $schema->resultset('Artist');
-my $art_rs_pr = $art_rs->search(
-    {},
-    {
-        join     => [ { cds => ['tracks'] } ],
-        prefetch => [ { cds => ['tracks'] } ],
-        cache    => 1 # last test needs this
-    }
-);
-
-# This test does the same operation twice - once on a
-# set of items fetched from the db with no prefetch of has_many rels
-# The second prefetches 2 levels of has_many
-# We check things are the same by comparing the name or title
-# we build everything into a hash structure and compare the one
-# from each rs to see what differs
-
-sub make_hash_struc {
-    my $rs = shift;
-
-    my $struc = {};
-    foreach my $art ( $rs->all ) {
-        foreach my $cd ( $art->cds ) {
-            foreach my $track ( $cd->tracks ) {
-                $struc->{ $art->name }{ $cd->title }{ $track->title }++;
-            }
-        }
-    }
-    return $struc;
-}
-
-$queries = 0;
-$schema->storage->debugcb(sub { $queries++ });
-$schema->storage->debug(1);
-
-my $prefetch_result = make_hash_struc($art_rs_pr);
-
-is($queries, 1, 'nested prefetch across has_many->has_many ran exactly 1 query');
-
-my $nonpre_result   = make_hash_struc($art_rs);
-
-is_deeply( $prefetch_result, $nonpre_result,
-    'Compare 2 level prefetch result to non-prefetch result' );
-
-$queries = 0;
-
-is($art_rs_pr->search_related('cds')->search_related('tracks')->first->title,
-   'Fowlin',
-   'chained has_many->has_many search_related ok'
-  );
-
-is($queries, 0, 'chained search_related after has_many->has_many prefetch ran no queries');
-
-# once the following TODO is complete, remove the 2 warning tests immediately after the TODO block
-# (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)';
-
-    #( 1 -> M + M )
-    my $cd_rs = $schema->resultset('CD')->search ({ 'me.title' => 'Forkful of bees' });
-    my $pr_cd_rs = $cd_rs->search ({}, {
-        prefetch => [qw/tracks tags/],
-    });
-
-    my $tracks_rs = $cd_rs->first->tracks;
-    my $tracks_count = $tracks_rs->count;
-
-    my ($pr_tracks_rs, $pr_tracks_count);
-
-    $queries = 0;
-    $schema->storage->debugcb(sub { $queries++ });
-    $schema->storage->debug(1);
-
-    my $o_mm_warn;
-    {
-        local $SIG{__WARN__} = sub { $o_mm_warn = shift };
-        $pr_tracks_rs = $pr_cd_rs->first->tracks;
-    };
-    $pr_tracks_count = $pr_tracks_rs->count;
-
-    ok(! $o_mm_warn, 'no warning on attempt to prefetch several same level has_many\'s (1 -> M + M)');
-
-    is($queries, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query');
-    is($pr_tracks_count, $tracks_count, 'equal count of prefetched relations over several same level has_many\'s (1 -> M + M)');
-
-    for ($pr_tracks_rs, $tracks_rs) {
-        $_->result_class ('DBIx::Class::ResultClass::HashRefInflator');
-    }
-
-    is_deeply ([$pr_tracks_rs->all], [$tracks_rs->all], 'same structure returned with and without prefetch over several same level has_many\'s (1 -> M + M)');
-
-    #( M -> 1 -> M + M )
-    my $note_rs = $schema->resultset('LinerNotes')->search ({ notes => 'Buy Whiskey!' });
-    my $pr_note_rs = $note_rs->search ({}, {
-        prefetch => {
-            cd => [qw/tags tracks/]
-        },
-    });
-
-    my $tags_rs = $note_rs->first->cd->tags;
-    my $tags_count = $tags_rs->count;
-
-    my ($pr_tags_rs, $pr_tags_count);
-
-    $queries = 0;
-    $schema->storage->debugcb(sub { $queries++ });
-    $schema->storage->debug(1);
-
-    my $m_o_mm_warn;
-    {
-        local $SIG{__WARN__} = sub { $m_o_mm_warn = shift };
-        $pr_tags_rs = $pr_note_rs->first->cd->tags;
-    };
-    $pr_tags_count = $pr_tags_rs->count;
-
-    ok(! $m_o_mm_warn, 'no warning on attempt to prefetch several same level has_many\'s (M -> 1 -> M + M)');
-
-    is($queries, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query');
-
-    is($pr_tags_count, $tags_count, 'equal count of prefetched relations over several same level has_many\'s (M -> 1 -> M + M)');
-
-    for ($pr_tags_rs, $tags_rs) {
-        $_->result_class ('DBIx::Class::ResultClass::HashRefInflator');
-    }
-
-    is_deeply ([$pr_tags_rs->all], [$tags_rs->all], 'same structure returned with and without prefetch over several same level has_many\'s (M -> 1 -> M + M)');
-};
-
-# remove this closure once the TODO above is working
-my $w;
-{
-    local $SIG{__WARN__} = sub { $w = shift };
-
-    my $track = $schema->resultset('CD')->search ({ 'me.title' => 'Forkful of bees' }, { prefetch => [qw/tracks tags/] })->first->tracks->first;
-    like ($w, qr/will currently disrupt both the functionality of .rs->count\(\), and the amount of objects retrievable via .rs->next\(\)/,
-        'warning on attempt to prefetch several same level has_many\'s (1 -> M + M)');
-    my $tag = $schema->resultset('LinerNotes')->search ({ notes => 'Buy Whiskey!' }, { prefetch => { cd => [qw/tags tracks/] } })->first->cd->tags->first;
-    like ($w, qr/will currently disrupt both the functionality of .rs->count\(\), and the amount of objects retrievable via .rs->next\(\)/,
-        'warning on attempt to prefetch several same level has_many\'s (M -> 1 -> M + M)');
-}
index 6108f28..8bd7e2c 100644 (file)
@@ -7,7 +7,7 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-plan tests => 45;
+plan tests => 49;
 
 # Check the defined unique constraints
 is_deeply(
@@ -183,3 +183,30 @@ is($row->baz, 3, 'baz is correct');
   ok($cd->in_storage, 'find correctly grepped the key across a relationship');
   is($cd->cdid, 1, 'cdid is correct');
 }
+
+# Test update_or_new
+{
+    my $cd1 = $schema->resultset('CD')->update_or_new(
+      {
+        artist => $artistid,
+        title  => "SuperHits $$",
+        year   => 2007,
+      },
+      { key => 'cd_artist_title' }
+    );
+
+    ok(!$cd1->in_storage, 'CD is not in storage yet after update_or_new');
+    $cd1->insert;
+    ok($cd1->in_storage, 'CD got added to strage after update_or_new && insert');
+
+    my $cd2 = $schema->resultset('CD')->update_or_new(
+      {
+        artist => $artistid,
+        title  => "SuperHits $$",
+        year   => 2008,
+      },
+      { key => 'cd_artist_title' }
+    );
+    ok($cd2->in_storage, 'Updating year using update_or_new was successful');
+    is($cd2->id, $cd1->id, 'Got the same CD using update_or_new');
+}
\ No newline at end of file
index 027ba76..32d5f17 100644 (file)
@@ -272,7 +272,7 @@ $schema->storage->disconnect;
 
   ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
 
-  eval {
+  lives_ok (sub {
     my $w;
     local $SIG{__WARN__} = sub { $w = shift };
 
@@ -281,32 +281,27 @@ $schema->storage->disconnect;
     outer($schema, 0);
 
     like ($w, qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or an error/, 'Out of scope warning detected');
-  };
-
-  local $TODO = "Work out how this should work";
-  is($@, "Not sure what we want here, but something", "Rollback okay");
-
-  ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
+    ok(!$artist_rs->find({name => 'Death Cab for Cutie'}), "Artist not created");
+  }, 'rollback successful withot exception');
 
   sub outer {
     my ($schema) = @_;
-   
+
     my $guard = $schema->txn_scope_guard;
     $schema->resultset('Artist')->create({
       name => 'Death Cab for Cutie',
     });
     inner(@_);
-    $guard->commit;
   }
 
   sub inner {
     my ($schema, $fatal) = @_;
-    my $guard = $schema->txn_scope_guard;
+
+    my $inner_guard = $schema->txn_scope_guard;
+    is($schema->storage->transaction_depth, 2, "Correct transaction depth");
 
     my $artist = $artist_rs->find({ name => 'Death Cab for Cutie' });
 
-    is($schema->storage->transaction_depth, 2, "Correct transaction depth");
-    undef $@;
     eval {
       $artist->cds->create({ 
         title => 'Plans',
@@ -320,6 +315,7 @@ $schema->storage->disconnect;
       die $@;
     }
 
-    # See what happens if we dont $guard->commit;
+    # inner guard should commit without consequences
+    $inner_guard->commit;
   }
 }
index 67033db..1d9f17a 100644 (file)
@@ -42,11 +42,15 @@ foreach my $group_id (1..4) {
 my $group_3 = $employees->search({group_id=>3});
 my $to_group = 1;
 my $to_pos = undef;
-while (my $employee = $group_3->next) {
-       $employee->discard_changes;     # since we are effective shift()ing the $rs
-       $employee->move_to_group($to_group, $to_pos);
-       $to_pos++;
-       $to_group = $to_group==1 ? 2 : 1;
+# now that we have transactions we need to work around stupid sqlite
+{
+  my @empl = $group_3->all;
+  while (my $employee = shift @empl) {
+    $employee->discard_changes;     # since we are effective shift()ing the $rs while doing this
+    $employee->move_to_group($to_group, $to_pos);
+    $to_pos++;
+    $to_group = $to_group==1 ? 2 : 1;
+  }
 }
 foreach my $group_id (1..4) {
     my $group_employees = $employees->search({group_id=>$group_id});
@@ -124,12 +128,17 @@ $to_group = 1;
 my $to_group_2_base = 7;
 my $to_group_2 = 1;
 $to_pos = undef;
-while (my $employee = $group_4->next) {
-       $employee->move_to_group({group_id_2=>$to_group, group_id_3=>$to_group_2}, $to_pos);
-       $to_pos++;
+
+# now that we have transactions we need to work around stupid sqlite
+{
+  my @empl = $group_3->all;
+  while (my $employee = shift @empl) {
+    $employee->move_to_group({group_id_2=>$to_group, group_id_3=>$to_group_2}, $to_pos);
+    $to_pos++;
     $to_group = ($to_group % 3) + 1;
     $to_group_2_base++;
     $to_group_2 = (ceil($to_group_2_base/3.0) %3) +1
+  }
 }
 foreach my $group_id_2 (1..4) {
     foreach my $group_id_3 (1..4) {
index 154ddab..53930c2 100644 (file)
@@ -21,7 +21,6 @@ my $tests_per_run = 5;
 
 plan tests => $tests_per_run * @json_backends;
 
-use JSON::Any;
 for my $js (@json_backends) {
 
     eval {JSON::Any->import ($js) };
index baf8ef8..f3c0e01 100644 (file)
@@ -5,12 +5,17 @@ use Test::More;
 use lib qw(t/lib);
 use DBICTest;
 
+{
+  local $SIG{__WARN__} = sub { warn @_ if $_[0] !~ /extra \=\> .+? has been deprecated/ };
+  DBICTest::Schema->load_classes('EventTZDeprecated');
+}
+
 my $schema = DBICTest->init_schema();
 
 eval { require DateTime::Format::MySQL };
 plan skip_all => "Need DateTime::Format::MySQL for inflation tests" if $@;
 
-plan tests => 32;
+plan tests => 50;
 
 # inflation test
 my $event = $schema->resultset("Event")->find(1);
@@ -52,75 +57,78 @@ is("$created_cron", '2006-06-23T00:00:00', 'Correct date/time');
 
 
 # Test "timezone" parameter
-my $event_tz = $schema->resultset('EventTZ')->create({
-    starts_at => DateTime->new(year=>2007, month=>12, day=>31, time_zone => "America/Chicago" ),
-    created_on => DateTime->new(year=>2006, month=>1, day=>31,
-        hour => 13, minute => 34, second => 56, time_zone => "America/New_York" ),
-});
 
-is ($event_tz->starts_at->day_name, "Montag", 'Locale de_DE loaded: day_name');
-is ($event_tz->starts_at->month_name, "Dezember", 'Locale de_DE loaded: month_name');
-is ($event_tz->created_on->day_name, "Tuesday", 'Default locale loaded: day_name');
-is ($event_tz->created_on->month_name, "January", 'Default locale loaded: month_name');
-
-my $starts_at = $event_tz->starts_at;
-is("$starts_at", '2007-12-31T00:00:00', 'Correct date/time using timezone');
-
-my $created_on = $event_tz->created_on;
-is("$created_on", '2006-01-31T12:34:56', 'Correct timestamp using timezone');
-is($event_tz->created_on->time_zone->name, "America/Chicago", "Correct timezone");
-
-my $loaded_event = $schema->resultset('EventTZ')->find( $event_tz->id );
-
-isa_ok($loaded_event->starts_at, 'DateTime', 'DateTime returned');
-$starts_at = $loaded_event->starts_at;
-is("$starts_at", '2007-12-31T00:00:00', 'Loaded correct date/time using timezone');
-is($starts_at->time_zone->name, 'America/Chicago', 'Correct timezone');
-
-isa_ok($loaded_event->created_on, 'DateTime', 'DateTime returned');
-$created_on = $loaded_event->created_on;
-is("$created_on", '2006-01-31T12:34:56', 'Loaded correct timestamp using timezone');
-is($created_on->time_zone->name, 'America/Chicago', 'Correct timezone');
-
-# Test floating timezone warning
-# We expect one warning
-SKIP: {
-    skip "ENV{DBIC_FLOATING_TZ_OK} was set, skipping", 1 if $ENV{DBIC_FLOATING_TZ_OK};
-    local $SIG{__WARN__} = sub {
-        like(
-            shift,
-            qr/You're using a floating timezone, please see the documentation of DBIx::Class::InflateColumn::DateTime for an explanation/,
-            'Floating timezone warning'
-        );
-    };
-    my $event_tz_floating = $schema->resultset('EventTZ')->create({
-        starts_at => DateTime->new(year=>2007, month=>12, day=>31, ),
-        created_on => DateTime->new(year=>2006, month=>1, day=>31,
-            hour => 13, minute => 34, second => 56, ),
-    });
-    delete $SIG{__WARN__};
-};
-
-# This should fail to set
-my $prev_str = "$created_on";
-$loaded_event->update({ created_on => '0000-00-00' });
-is("$created_on", $prev_str, "Don't update invalid dates");
-
-my $invalid = $schema->resultset('Event')->create({
-    starts_at  => '0000-00-00',
-    created_on => $created_on
-});
-
-is( $invalid->get_column('starts_at'), '0000-00-00', "Invalid date stored" );
-is( $invalid->starts_at, undef, "Inflate to undef" );
-
-$invalid->created_on('0000-00-00');
-$invalid->update;
-
-{
-    local $@;
-    eval { $invalid->created_on };
-    like( $@, qr/invalid date format/i, "Invalid date format exception");
+foreach my $tbl (qw/EventTZ EventTZDeprecated/) {
+  my $event_tz = $schema->resultset($tbl)->create({
+      starts_at => DateTime->new(year=>2007, month=>12, day=>31, time_zone => "America/Chicago" ),
+      created_on => DateTime->new(year=>2006, month=>1, day=>31,
+          hour => 13, minute => 34, second => 56, time_zone => "America/New_York" ),
+  });
+
+  is ($event_tz->starts_at->day_name, "Montag", 'Locale de_DE loaded: day_name');
+  is ($event_tz->starts_at->month_name, "Dezember", 'Locale de_DE loaded: month_name');
+  is ($event_tz->created_on->day_name, "Tuesday", 'Default locale loaded: day_name');
+  is ($event_tz->created_on->month_name, "January", 'Default locale loaded: month_name');
+
+  my $starts_at = $event_tz->starts_at;
+  is("$starts_at", '2007-12-31T00:00:00', 'Correct date/time using timezone');
+
+  my $created_on = $event_tz->created_on;
+  is("$created_on", '2006-01-31T12:34:56', 'Correct timestamp using timezone');
+  is($event_tz->created_on->time_zone->name, "America/Chicago", "Correct timezone");
+
+  my $loaded_event = $schema->resultset($tbl)->find( $event_tz->id );
+
+  isa_ok($loaded_event->starts_at, 'DateTime', 'DateTime returned');
+  $starts_at = $loaded_event->starts_at;
+  is("$starts_at", '2007-12-31T00:00:00', 'Loaded correct date/time using timezone');
+  is($starts_at->time_zone->name, 'America/Chicago', 'Correct timezone');
+
+  isa_ok($loaded_event->created_on, 'DateTime', 'DateTime returned');
+  $created_on = $loaded_event->created_on;
+  is("$created_on", '2006-01-31T12:34:56', 'Loaded correct timestamp using timezone');
+  is($created_on->time_zone->name, 'America/Chicago', 'Correct timezone');
+
+  # Test floating timezone warning
+  # We expect one warning
+  SKIP: {
+      skip "ENV{DBIC_FLOATING_TZ_OK} was set, skipping", 1 if $ENV{DBIC_FLOATING_TZ_OK};
+      local $SIG{__WARN__} = sub {
+          like(
+              shift,
+              qr/You're using a floating timezone, please see the documentation of DBIx::Class::InflateColumn::DateTime for an explanation/,
+              'Floating timezone warning'
+          );
+      };
+      my $event_tz_floating = $schema->resultset($tbl)->create({
+          starts_at => DateTime->new(year=>2007, month=>12, day=>31, ),
+          created_on => DateTime->new(year=>2006, month=>1, day=>31,
+              hour => 13, minute => 34, second => 56, ),
+      });
+      delete $SIG{__WARN__};
+  };
+
+  # This should fail to set
+  my $prev_str = "$created_on";
+  $loaded_event->update({ created_on => '0000-00-00' });
+  is("$created_on", $prev_str, "Don't update invalid dates");
+
+  my $invalid = $schema->resultset('Event')->create({
+      starts_at  => '0000-00-00',
+      created_on => $created_on
+  });
+
+  is( $invalid->get_column('starts_at'), '0000-00-00', "Invalid date stored" );
+  is( $invalid->starts_at, undef, "Inflate to undef" );
+
+  $invalid->created_on('0000-00-00');
+  $invalid->update;
+
+  {
+      local $@;
+      eval { $invalid->created_on };
+      like( $@, qr/invalid date format/i, "Invalid date format exception");
+  }
 }
 
 ## varchar field using inflate_date => 1
index d940eaa..5b5514b 100644 (file)
@@ -57,7 +57,7 @@ open(STDERR, '>&STDERRCOPY');
     my @cds = $schema->resultset('CD')->search( { artist => 1, cdid => { -between => [ 1, 3 ] }, } );
     is_same_sql_bind(
         $sql, [],
-        "SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me WHERE ( artist = ? AND cdid BETWEEN ? AND ? ): '1', '1', '3'", [],
+        "SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me WHERE ( artist = ? AND (cdid BETWEEN ? AND ?) ): '1', '1', '3'", [],
         'got correct SQL with all bind parameters (debugcb)'
     );
 
@@ -66,7 +66,7 @@ open(STDERR, '>&STDERRCOPY');
     @cds = $schema->resultset('CD')->search( { artist => 1, cdid => { -between => [ 1, 3 ] }, } );
     is_same_sql_bind(
         $sql, \@bind,
-        "SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me WHERE ( artist = ? AND cdid BETWEEN ? AND ? )", ["'1'", "'1'", "'3'"],
+        "SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me WHERE ( artist = ? AND (cdid BETWEEN ? AND ?) )", ["'1'", "'1'", "'3'"],
         'got correct SQL with all bind parameters (debugobj)'
     );
 }
index b72405b..b9ea61c 100644 (file)
@@ -577,12 +577,3 @@ ok $replicated->schema->resultset('Artist')->find(1)
 
 ## Delete the old database files
 $replicated->cleanup;
-
-use Data::Dump qw/dump/;
-#warn dump $replicated->schema->storage->read_handler;
-
-
-
-
-
-
index 0f7dda8..48f66ac 100644 (file)
@@ -20,10 +20,7 @@ my $schema = DBICTest->init_schema();
 my $sql_maker = $schema->storage->sql_maker;
 
 
-SKIP: {
-  skip "SQL::Abstract < 1.49 does not pass through arrayrefs", 2
-    if $SQL::Abstract::VERSION < 1.49;
-
+{
   my ($sql, @bind) = $sql_maker->insert(
             'lottery',
             {
index 4fa987b..a7687f8 100644 (file)
@@ -110,10 +110,7 @@ is_same_sql_bind(
   'scalar ORDER BY okay (multiple values)'
 );
 
-SKIP: {
-  skip "SQL::Abstract < 1.49 does not support hashrefs in order_by", 2
-    if $SQL::Abstract::VERSION < 1.49;
-
+{
   ($sql, @bind) = $sql_maker->select(
             [
               {
@@ -236,10 +233,7 @@ is_same_sql_bind(
   'quoted table names for UPDATE'
 );
 
-SKIP: {
-  skip "select attr with star does not work in SQL::Abstract < 1.49", 1
-    if $SQL::Abstract::VERSION < 1.49;
-
+{
   ($sql, @bind) = $sql_maker->select(
         [
           {
diff --git a/t/98rows_prefetch.t b/t/98rows_prefetch.t
deleted file mode 100644 (file)
index 8619c13..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-# Test to ensure we get a consistent result set wether or not we use the
-# prefetch option in combination rows (LIMIT).
-use strict;
-use warnings;
-
-use Test::More;
-use lib qw(t/lib);
-use DBICTest;
-
-plan $@ ? (skip_all => 'needs DBD::SQLite for testing') : (tests => 2);
-
-my $schema = DBICTest->init_schema();
-my $no_prefetch = $schema->resultset('Artist')->search(
-       undef,
-       { rows => 3 }
-);
-
-my $use_prefetch = $schema->resultset('Artist')->search(
-       undef,
-       {
-               prefetch => 'cds',
-               rows     => 3
-       }
-);
-
-my $no_prefetch_count  = 0;
-my $use_prefetch_count = 0;
-
-is($no_prefetch->count, $use_prefetch->count, '$no_prefetch->count == $use_prefetch->count');
-
-TODO: {
-       local $TODO = "This is a difficult bug to fix, workaround is not to use prefetch with rows";
-       $no_prefetch_count++  while $no_prefetch->next;
-       $use_prefetch_count++ while $use_prefetch->next;
-       is(
-               $no_prefetch_count,
-               $use_prefetch_count,
-               "manual row count confirms consistency"
-               . " (\$no_prefetch_count == $no_prefetch_count, "
-               . " \$use_prefetch_count == $use_prefetch_count)"
-       );
-}
index 0a42306..9968a82 100644 (file)
@@ -7,9 +7,9 @@ use DBICTest;
 
 
 BEGIN {
-    eval "use DBD::mysql; use SQL::Translator 0.09003;";
+    eval "use SQL::Translator 0.09003;";
     if ($@) {
-        plan skip_all => 'needs DBD::mysql and SQL::Translator 0.09003 for testing';
+        plan skip_all => 'needs SQL::Translator 0.09003 for testing';
     }
 }
 
index 4166226..61c7b90 100644 (file)
@@ -1,6 +1,7 @@
 use strict;
 
 use Test::More;
+use lib 't/cdbi/testlib';
 
 BEGIN {
   eval "use DBIx::Class::CDBICompat;";
@@ -13,7 +14,7 @@ BEGIN {
 #-----------------------------------------------------------------------
 package State;
 
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 
 State->table('State');
 State->columns(Essential => qw/Abbreviation Name/);
@@ -39,7 +40,7 @@ sub Snowfall { 1 }
 
 package City;
 
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 
 City->table('City');
 City->columns(All => qw/Name State Population/);
@@ -56,7 +57,7 @@ City->columns(All => qw/Name State Population/);
 
 #-------------------------------------------------------------------------
 package CD;
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 
 CD->table('CD');
 CD->columns('All' => qw/artist title length/);
index 988951d..0e1b22e 100644 (file)
@@ -151,7 +151,7 @@ is($blrunner_dc->NumExplodingSheep, undef, 'Sheep correct');
 
 # Multi-column search
 {
-       my @films = $blrunner->search_like(title => "Bladerunner%", rating => '15');
+       my @films = $blrunner->search (title => { -like => "Bladerunner%"}, rating => '15');
        is @films, 1, "Only one Bladerunner is a 15";
 }
 
@@ -208,7 +208,7 @@ is(scalar @films, 1, ' search returns one film');
 is($films[0]->id, $gone->id, ' ... the correct one');
 
 # Find all films which were directed by Bob
-@films = Film->search_like('Director', 'Bob %');
+@films = Film->search ( { 'Director' => { -like => 'Bob %' } });
 is(scalar @films, 3, ' search_like returns 3 films');
 ok(
        eq_array(
index 72f2c54..3419cf0 100644 (file)
@@ -25,6 +25,11 @@ INIT {
     sub Class::DBI::sheep { ok 0; }
 }
 
+# Install the deprecation warning intercept here for the rest of the 08 dev cycle
+local $SIG{__WARN__} = sub {
+  warn @_ unless (DBIx::Class->VERSION < 0.09 and $_[0] =~ /Query returned more than one row/);
+};
+
 sub Film::mutator_name {
     my ($class, $col) = @_;
     return "set_sheep" if lc $col eq "numexplodingsheep";
@@ -160,9 +165,6 @@ is $@, '', "No errors";
     like $@, qr/film/, "no hasa film";
 
     eval {
-        local $SIG{__WARN__} = sub {
-            warn @_ unless $_[0] =~ /Query returned more than one row/;
-        };
         ok my $f = $ac->movie, "hasa movie";
         isa_ok $f, "Film";
         is $f->id, $bt->id, " - Bad Taste";
@@ -264,5 +266,5 @@ is $@, '', "No errors";
     my $abigail = eval { Film->create({ title => "Abigail's Party" }) };
     like $@, qr/read only/, "Or create new films";
 
-    $sandl->discard_changes;
+    $_->discard_changes for ($naked, $sandl);
 }
index 9d943e5..5267ae0 100644 (file)
@@ -12,6 +12,7 @@ if ($@) {
 eval { require Time::Piece::MySQL };
 plan skip_all => "Need Time::Piece::MySQL for this test" if $@;
 
+use lib 't/cdbi/testlib';
 eval { require 't/cdbi/testlib/Log.pm' };
 plan skip_all => "Need MySQL for this test" if $@;
 
index f9bd027..6ec7fe1 100644 (file)
@@ -1,5 +1,6 @@
 use strict;
 use Test::More;
+use lib 't/cdbi/testlib';
 
 BEGIN {
   eval "use DBIx::Class::CDBICompat;";
@@ -10,7 +11,7 @@ BEGIN {
 {
     package Thing;
 
-    use base 'DBIx::Class::Test::SQLite';
+    use base 'DBIC::Test::SQLite';
 
     Thing->columns(TEMP => qw[foo bar]);
     Thing->columns(All  => qw[thing_id yarrow flower]);
index 25eb255..a6e60ba 100644 (file)
@@ -15,7 +15,7 @@ INIT {
     package # hide from PAUSE 
         MyFilm;
 
-    use base 'DBIx::Class::Test::SQLite';
+    use base 'DBIC::Test::SQLite';
     use strict;
 
     __PACKAGE__->set_table('Movies');
index 4311456..d6a9484 100644 (file)
@@ -1,5 +1,6 @@
 use strict;
 use Test::More;
+use lib 't/cdbi/testlib';
 
 BEGIN {
   eval "use DBIx::Class::CDBICompat;";
@@ -10,7 +11,7 @@ BEGIN {
 {
     package Thing;
 
-    use base 'DBIx::Class::Test::SQLite';
+    use base 'DBIC::Test::SQLite';
 
     Thing->columns(TEMP => qw[foo bar baz]);
     Thing->columns(All  => qw[some real stuff]);
index 83cf1a2..47b0a35 100644 (file)
@@ -1,5 +1,6 @@
 use strict;
 use Test::More;
+use lib 't/cdbi/testlib';
 
 BEGIN {
   eval "use DBIx::Class::CDBICompat;";
@@ -19,7 +20,7 @@ local $SIG{__WARN__} = sub {
 {
     package Thing;
 
-    use base 'DBIx::Class::Test::SQLite';
+    use base 'DBIC::Test::SQLite';
 
     Thing->columns(All  => qw[thing_id this that date]);
 }
index 7dd17ce..45ce621 100644 (file)
@@ -1,6 +1,7 @@
 use strict;
 use Test::More;
 use Test::Exception;
+use lib 't/cdbi/testlib';
 
 BEGIN {
   eval "use DBIx::Class::CDBICompat;";
@@ -13,7 +14,7 @@ BEGIN {
 {
     package Thing;
 
-    use base 'DBIx::Class::Test::SQLite';
+    use base 'DBIC::Test::SQLite';
 
     Thing->columns(All  => qw[thing_id this that date]);
 }
index 4e27abe..2944390 100644 (file)
@@ -4,7 +4,7 @@ package # hide from PAUSE
 use strict;
 use warnings;
 
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 
 __PACKAGE__->set_table('Actor');
 
index ba38551..9e4ebe4 100644 (file)
@@ -4,7 +4,7 @@ package # hide from PAUSE
 use strict;
 use warnings;
 
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 
 __PACKAGE__->set_table( 'ActorAlias' );
 
index a112f47..7c6dfdb 100644 (file)
@@ -2,7 +2,7 @@ package # hide from PAUSE
     Blurb;
 
 use strict;
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 
 __PACKAGE__->set_table('Blurbs');
 __PACKAGE__->columns('Primary', 'Title');
index 22c6262..4367ef0 100644 (file)
@@ -2,6 +2,6 @@ package # hide from PAUSE
     CDBase;
 
 use strict;
-use base qw(DBIx::Class::Test::SQLite);
+use base qw(DBIC::Test::SQLite);
 
 1;
similarity index 97%
rename from lib/DBIx/Class/Test/SQLite.pm
rename to t/cdbi/testlib/DBIC/Test/SQLite.pm
index 3302289..3b17953 100644 (file)
@@ -1,4 +1,5 @@
-package DBIx::Class::Test::SQLite;
+package # hide from PAUSE
+    DBIC::Test::SQLite;
 
 =head1 NAME
 
index af0a453..a9dd199 100644 (file)
@@ -2,7 +2,7 @@ package # hide from PAUSE
     Director;
 
 use strict;
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 
 __PACKAGE__->set_table('Directors');
 __PACKAGE__->columns('All' => qw/ Name Birthday IsInsane /);
index 6521a59..b1f50ac 100644 (file)
@@ -1,7 +1,7 @@
 package # hide from PAUSE 
     Film;
 
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 use strict;
 
 __PACKAGE__->set_table('Movies');
index 2b2137e..5835de2 100644 (file)
@@ -1,7 +1,7 @@
 package # hide from PAUSE 
     Lazy;
 
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 use strict;
 
 __PACKAGE__->set_table("Lazy");
index eeb7cf0..5dfbfed 100644 (file)
@@ -8,9 +8,7 @@ use DBI;
 
 use vars qw/$dbh/;
 
-# temporary, might get switched to the new test framework someday
-my @connect = ("dbi:mysql:test", "", "", { PrintError => 0});
-
+my @connect = (@ENV{map { "DBICTEST_MYSQL_${_}" } qw/DSN USER PASS/}, { PrintError => 0});
 $dbh = DBI->connect(@connect) or die DBI->errstr;
 my @table;
 
index 703005d..fa1f296 100644 (file)
@@ -2,7 +2,7 @@ package # hide from PAUSE
     Order;
 
 use strict;
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 
 __PACKAGE__->set_table('orders');
 __PACKAGE__->table_alias('orders');
index 08c31ba..59fb818 100644 (file)
@@ -1,5 +1,5 @@
 package OtherThing;
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 
 OtherThing->set_table("other_thing");
 OtherThing->columns(All => qw(id));
index d71e22a..4080b66 100644 (file)
@@ -1,5 +1,5 @@
 package Thing;
-use base 'DBIx::Class::Test::SQLite';
+use base 'DBIC::Test::SQLite';
 
 Thing->set_table("thing");
 Thing->columns(All => qw(id that_thing));
diff --git a/t/count/count_distinct.t b/t/count/count_distinct.t
new file mode 100644 (file)
index 0000000..d4dd422
--- /dev/null
@@ -0,0 +1,57 @@
+use strict;
+use warnings;  
+
+use Test::More;
+
+use lib qw(t/lib);
+
+use DBICTest;
+use DBIC::SqlMakerTest;
+
+my $schema = DBICTest->init_schema();
+
+eval "use DBD::SQLite";
+plan skip_all => 'needs DBD::SQLite for testing' if $@;
+plan tests => 13;
+
+my $in_rs = $schema->resultset('Tag')->search({ tag => [ 'Blue', 'Shiny' ] });
+my $rs;
+
+$rs = $schema->resultset('Tag')->search({ tag => 'Blue' });
+is($rs->count, 4, 'Count without DISTINCT');
+
+$rs = $schema->resultset('Tag')->search({ tag => [ 'Blue', 'Shiny' ] }, { group_by => 'tag' });
+is($rs->count, 2, 'Count with single column group_by');
+
+$rs = $schema->resultset('Tag')->search({ tag => 'Blue' }, { group_by => [ qw/tag cd/ ]});
+is($rs->count, 4, 'Count with multiple column group_by');
+
+$rs = $schema->resultset('Tag')->search({ tag => 'Blue' }, { distinct => 1 });
+is($rs->count, 4, 'Count with single column distinct');
+
+$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } });
+is($rs->count, 4, 'Count with IN subquery');
+
+$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { group_by => 'tag' });
+is($rs->count, 1, 'Count with IN subquery with outside group_by');
+
+$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { distinct => 1 });
+is($rs->count, 4, 'Count with IN subquery with outside distinct');
+
+$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { distinct => 1, select => 'tag' }), 
+is($rs->count, 1, 'Count with IN subquery with outside distinct on a single column');
+
+$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->search({}, { group_by => 'tag' })->get_column('tag')->as_query } });
+is($rs->count, 4, 'Count with IN subquery with single group_by');
+
+$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->search({}, { group_by => [ qw/tag cd/ ] })->get_column('tag')->as_query } });
+is($rs->count, 4, 'Count with IN subquery with multiple group_by');
+
+$rs = $schema->resultset('Tag')->search({ tag => \"= 'Blue'" });
+is($rs->count, 4, 'Count without DISTINCT, using literal SQL');
+
+$rs = $schema->resultset('Tag')->search({ tag => \" IN ('Blue', 'Shiny')" }, { group_by => 'tag' });
+is($rs->count, 2, 'Count with literal SQL and single group_by');
+
+$rs = $schema->resultset('Tag')->search({ tag => \" IN ('Blue', 'Shiny')" }, { group_by => [ qw/tag cd/ ] });
+is($rs->count, 6, 'Count with literal SQL and multiple group_by');
diff --git a/t/count/count_joined.t b/t/count/count_joined.t
new file mode 100644 (file)
index 0000000..5f5fa33
--- /dev/null
@@ -0,0 +1,18 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib qw(t/lib);
+
+use DBICTest;
+
+plan tests => 1;
+
+my $schema = DBICTest->init_schema();
+
+TODO: {
+  local $TODO = 'TODO until we figure out what we really want';
+  my $cds = $schema->resultset("CD")->search({ cdid => 1 }, { join => { cd_to_producer => 'producer' } });
+  is($cds->count, 1, "extra joins do not explode single entity count");
+}
diff --git a/t/from_subquery.t b/t/from_subquery.t
new file mode 100644 (file)
index 0000000..5dc91d0
--- /dev/null
@@ -0,0 +1,192 @@
+use strict;
+use warnings FATAL => 'all';
+
+use Test::More;
+
+BEGIN {
+    eval "use SQL::Abstract 1.49";
+    plan $@
+        ? ( skip_all => "Needs SQLA 1.49+" )
+        : ( tests => 8 );
+}
+
+use lib qw(t/lib);
+use DBICTest;
+use DBIC::SqlMakerTest;
+
+my $schema = DBICTest->init_schema();
+my $art_rs = $schema->resultset('Artist');
+my $cdrs = $schema->resultset('CD');
+
+{
+  my $cdrs2 = $cdrs->search({
+    artist_id => { 'in' => $art_rs->search({}, { rows => 1 })->get_column( 'id' )->as_query },
+  });
+
+  my $arr = $cdrs2->as_query;
+  my ($query, @bind) = @{$$arr};
+  is_same_sql_bind(
+    $query, \@bind,
+    "(SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE artist_id IN ( SELECT id FROM artist me LIMIT 1 ))",
+    [],
+  );
+}
+
+{
+  my $rs = $art_rs->search(
+    {},
+    {
+      'select' => [
+        $cdrs->search({}, { rows => 1 })->get_column('id')->as_query,
+      ],
+    },
+  );
+
+  my $arr = $rs->as_query;
+  my ($query, @bind) = @{$$arr};
+  is_same_sql_bind(
+    $query, \@bind,
+    "(SELECT (SELECT id FROM cd me LIMIT 1) FROM artist me)",
+    [],
+  );
+}
+
+{
+  my $rs = $art_rs->search(
+    {},
+    {
+      '+select' => [
+        $cdrs->search({}, { rows => 1 })->get_column('id')->as_query,
+      ],
+    },
+  );
+
+  my $arr = $rs->as_query;
+  my ($query, @bind) = @{$$arr};
+  is_same_sql_bind(
+    $query, \@bind,
+    "(SELECT me.artistid, me.name, me.rank, me.charfield, (SELECT id FROM cd me LIMIT 1) FROM artist me)",
+    [],
+  );
+}
+
+# simple from
+{
+  my $rs = $cdrs->search(
+    {},
+    {
+      alias => 'cd2',
+      from => [
+        { cd2 => $cdrs->search({ id => { '>' => 20 } })->as_query },
+      ],
+    },
+  );
+
+  my $arr = $rs->as_query;
+  my ($query, @bind) = @{$$arr};
+  is_same_sql_bind(
+    $query, \@bind,
+    "(SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE ( id > ? ) ) cd2)",
+    [
+      [ 'id', 20 ]
+    ],
+  );
+}
+
+# nested from
+{
+  my $art_rs2 = $schema->resultset('Artist')->search({}, 
+  {
+    from => [ { 'me' => 'artist' }, 
+      [ { 'cds' => $cdrs->search({},{ 'select' => [\'me.artist as cds_artist' ]})->as_query },
+      { 'me.artistid' => 'cds_artist' } ] ]
+  });
+
+  my $arr = $art_rs2->as_query;
+  my ($query, @bind) = @{$$arr};
+  is_same_sql_bind(
+    $query, \@bind,
+    "(SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me JOIN (SELECT me.artist as cds_artist FROM cd me) cds ON me.artistid = cds_artist)",
+    []
+  );
+
+
+}
+
+# nested subquery in from
+{
+  my $rs = $cdrs->search(
+    {},
+    {
+      alias => 'cd2',
+      from => [
+        { cd2 => $cdrs->search(
+            { id => { '>' => 20 } }, 
+            { 
+                alias => 'cd3',
+                from => [ 
+                { cd3 => $cdrs->search( { id => { '<' => 40 } } )->as_query }
+                ],
+            }, )->as_query },
+      ],
+    },
+  );
+
+  my $arr = $rs->as_query;
+  my ($query, @bind) = @{$$arr};
+  is_same_sql_bind(
+    $query, \@bind,
+    "(SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track 
+      FROM 
+        (SELECT cd3.cdid,cd3.artist,cd3.title,cd3.year,cd3.genreid,cd3.single_track 
+          FROM 
+            (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track 
+              FROM cd me WHERE ( id < ? ) ) cd3
+          WHERE ( id > ? ) ) cd2)",
+    [
+      [ 'id', 40 ], 
+      [ 'id', 20 ]
+    ],
+  );
+
+}
+
+{
+  my $rs = $cdrs->search({
+    year => {
+      '=' => $cdrs->search(
+        { artistid => { '=' => \'me.artistid' } },
+        { alias => 'inner' }
+      )->get_column('year')->max_rs->as_query,
+    },
+  });
+  my $arr = $rs->as_query;
+  my ($query, @bind) = @{$$arr};
+  is_same_sql_bind(
+    $query, \@bind,
+    "(SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me WHERE year = (SELECT MAX(inner.year) FROM cd inner WHERE artistid = me.artistid))",
+    [],
+  );
+}
+
+{
+  my $rs = $cdrs->search(
+    {},
+    {
+      alias => 'cd2',
+      from => [
+        { cd2 => $cdrs->search({ title => 'Thriller' })->as_query },
+      ],
+    },
+  );
+
+  my $arr = $rs->as_query;
+  my ($query, @bind) = @{$$arr};
+  is_same_sql_bind(
+    $query, \@bind,
+    "(SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE ( title = ? ) ) cd2)",
+    [ [ 'title', 'Thriller' ] ],
+  );
+}
+
+__END__
diff --git a/t/lib/DBICNGTest/Schema.pm b/t/lib/DBICNGTest/Schema.pm
deleted file mode 100644 (file)
index 57d2d50..0000000
+++ /dev/null
@@ -1,162 +0,0 @@
-package # hide from PAUSE
- DBICNGTest::Schema;
-   
-       use Moose;
-       use Path::Class::File;
-    extends 'DBIx::Class::Schema',  'Moose::Object'; 
-
-
-=head1 NAME
-
-DBICNGTest::Schema; Schema Base For Testing Moose Roles, Traits, etc.
-
-=head1 SYNOPSIS
-
-    my $schema = DBICNGTest::Schema->connect($dsn);
-    
-    ## Do anything you would as with a normal $schema object.
-
-=head1 DESCRIPTION
-
-Defines the base case for loading DBIC Schemas.  We add in some additional
-helpful functions for administering you schemas.  This namespace is dedicated
-to integration of Moose based development practices.
-
-=head1 PACKAGE METHODS
-
-The following is a list of package methods declared with this class.
-
-=head2 load_namespaces
-
-Automatically load the classes and resultsets from their default namespaces.
-
-=cut
-
-__PACKAGE__->load_namespaces(
-    default_resultset_class => 'ResultSet',
-);
-
-
-=head1 ATTRIBUTES
-
-This class defines the following attributes.
-
-=head1 METHODS
-
-This module declares the following methods
-
-=head2 new
-
-overload new to make sure we get a good meta object and that the attributes all
-get properly setup.  This is done so that our instances properly get a L<Moose>
-meta class.
-
-=cut
-
-sub new
-{
-    my $class = shift @_;
-    my $obj = $class->SUPER::new(@_);
-  
-    return $class->meta->new_object(
-        __INSTANCE__ => $obj, @_
-    );
-}
-
-
-=head2 connect_and_setup
-
-Creates a schema, deploys a database and sets the testing data.
-
-=cut
-
-sub connect_and_setup {
-    my $class = shift @_;
-    my $db_file = shift @_;
-    
-    my ($dsn, $user, $pass) = (
-      $ENV{DBICNG_DSN} || "dbi:SQLite:${db_file}",
-      $ENV{DBICNG_USER} || '',
-      $ENV{DBICNG_PASS} || '',
-    );
-    
-    return $class
-        ->connect($dsn, $user, $pass, { AutoCommit => 1 })
-        ->setup;
-}
-
-
-=head2 setup
-
-deploy a database and populate it with the initial data
-
-=cut
-
-sub setup {
-    my $self = shift @_;
-    $self->deploy();
-    $self->initial_populate(@_);
-    
-    return $self;
-}
-
-
-=head2 initial_populate
-
-initializing the startup database information
-
-=cut
-
-sub initial_populate {
-    my $self = shift @_;
-    
-    my @genders = $self->populate('Gender' => [
-        [qw(gender_id label)],
-        [qw(1 female)],
-        [qw(2 male)],
-        [qw(3 transgender)],
-    ]);
-  
-    my @persons = $self->populate('Person' => [
-        [ qw(person_id fk_gender_id name age) ],
-        [ qw(1 1 john 25) ],
-        [ qw(2 1 dan 35) ],
-        [ qw(3 2 mary 15) ],
-        [ qw(4 2 jane 95) ],
-        [ qw(5 3 steve 40) ], 
-    ]);
-    
-    my @friends = $self->populate('FriendList' => [
-        [ qw(fk_person_id fk_friend_id) ],
-        [ qw(1 2) ],
-        [ qw(1 3) ],   
-        [ qw(2 3) ], 
-        [ qw(3 2) ],             
-    ]);
-}
-
-
-=head2 job_handler_echo
-
-This is a method to test the job handler role.
-
-=cut
-
-sub job_handler_echo {
-       my ($schema, $job, $alert) = @_;
-       return $alert;
-}
-
-
-=head1 AUTHORS
-
-See L<DBIx::Class> for more information regarding authors.
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
-
-=cut
-
-
-1;
diff --git a/t/lib/DBICNGTest/Schema/Result.pm b/t/lib/DBICNGTest/Schema/Result.pm
deleted file mode 100644 (file)
index 9d13c4a..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-package # hide from PAUSE
- DBICNGTest::Schema::Result;
-    use Moose;
-    extends 'DBIx::Class', 'Moose::Object';
-       
-=head1 NAME
-
-DBICNGTest::Schema::Result; Base Class for result and class objects
-
-=head1 SYNOPSIS
-
-    package DBICNGTest::Schema::Result::Member;
-    
-    use Moose;
-    extends 'DBICNGTest::Schema::Result';
-    
-    ## Rest of the class definition.
-
-=head1 DESCRIPTION
-
-Defines the base case for loading DBIC Schemas.  We add in some additional
-helpful functions for administering you schemas.  This namespace is dedicated
-to integration of Moose based development practices
-
-=head1 PACKAGE METHODS
-
-The following is a list of package methods declared with this class.
-
-=head2 load_components
-
-Components to preload.
-
-=cut
-
-__PACKAGE__->load_components(qw/ 
-    PK::Auto 
-    InflateColumn::DateTime
-    Core 
-/);
-
-
-=head1 ATTRIBUTES
-
-This class defines the following attributes.
-
-=head1 METHODS
-
-This module declares the following methods
-
-=head2 new
-
-overload new to make sure we get a good meta object and that the attributes all
-get properly setup.  This is done so that our instances properly get a L<Moose>
-meta class.
-
-=cut
-
-sub new
-{
-    my $class = shift @_;
-    my $attrs = shift @_;
-  
-    my $obj = $class->SUPER::new($attrs);
-
-    return $class->meta->new_object(
-        __INSTANCE__ => $obj, %$attrs
-    );
-}
-
-
-=head1 AUTHORS
-
-See L<DBIx::Class> for more information regarding authors.
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
-
-=cut
-
-
-1;
\ No newline at end of file
diff --git a/t/lib/DBICNGTest/Schema/Result/FriendList.pm b/t/lib/DBICNGTest/Schema/Result/FriendList.pm
deleted file mode 100644 (file)
index 8c87003..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-package #hide from pause
- DBICNGTest::Schema::Result::FriendList;
-
-    use Moose;
-    extends 'DBICNGTest::Schema::Result';
-
-
-=head1 NAME
-
-Zoomwit::tlib::DBIC::Schema::Result::FriendList; An example Friends Class;
-
-=head1 VERSION
-
-0.01
-
-=cut
-
-our $VERSION = '0.01';
-
-
-=head1 DESCRIPTION
-
-A Person can have zero or more friends
-A Person can't be their own friend
-A Person over 18 can't be friends with Persons under 18 and vis versa.
-A Person can have friendships that are not mutual.
-
-=head1 ATTRIBUTES
-
-This class defines the following attributes.
-
-=head1 PACKAGE METHODS
-
-This module defines the following package methods
-
-=head2 table
-
-Name of the Physical table in the database
-
-=cut
-
-__PACKAGE__
-    ->table('friend_list');
-
-
-=head2 add_columns
-
-Add columns and meta information
-
-=head3 fk_person_id
-
-ID of the person with friends
-
-=head3 fk_friend_id
-
-Who is the friend?
-
-=cut
-
-__PACKAGE__
-    ->add_columns(
-        fk_person_id => {
-            data_type=>'integer',
-        },
-        fk_friend_id => {
-            data_type=>'integer',
-        },
-);
-        
-
-=head2 primary_key
-
-Sets the Primary keys for this table
-
-=cut
-
-__PACKAGE__
-    ->set_primary_key(qw/fk_person_id fk_friend_id/);
-    
-
-=head2 befriender
-
-The person that 'owns' the friendship (list)
-
-=cut
-
-__PACKAGE__
-    ->belongs_to( befriender => 'DBICNGTest::Schema::Result::Person', {
-        'foreign.person_id' => 'self.fk_person_id' });
-
-
-=head2 friendee
-
-The actual friend that befriender is listing
-
-=cut
-
-__PACKAGE__
-    ->belongs_to( friendee => 'DBICNGTest::Schema::Result::Person', { 
-        'foreign.person_id' => 'self.fk_friend_id' });
-
-
-=head1 METHODS
-
-This module defines the following methods.
-
-=head1 AUTHORS
-
-See L<DBIx::Class> for more information regarding authors.
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
-
-=cut
-
-
-1;
diff --git a/t/lib/DBICNGTest/Schema/Result/Gender.pm b/t/lib/DBICNGTest/Schema/Result/Gender.pm
deleted file mode 100644 (file)
index a47e5dd..0000000
+++ /dev/null
@@ -1,117 +0,0 @@
-package #hide from pause
- DBICNGTest::Schema::Result::Gender;
-
-    use Moose;
-    extends 'DBICNGTest::Schema::Result';
-
-
-=head1 NAME
-
-DBICNGTest::Schema::Result::Gender; An example Gender Class;
-
-=head1 DESCRIPTION
-
-Tests for this type of FK relationship
-
-=head1 ATTRIBUTES
-
-This class defines the following attributes.
-
-=head2 label
-
-example of using an attribute to add constraints on a table insert
-
-=cut
-
-has 'label' =>(is=>'rw', required=>1, isa=>'Str');
-
-
-=head1 PACKAGE METHODS
-
-This module defines the following package methods
-
-=head2 table
-
-Name of the Physical table in the database
-
-=cut
-
-__PACKAGE__
-    ->table('gender');
-
-
-=head2 add_columns
-
-Add columns and meta information
-
-=head3 gender_id
-
-Primary Key which is an auto generated UUID
-
-=head3 label
-
-Text label of the gender (ie, 'male', 'female', 'transgender', etc.).
-
-=cut
-
-__PACKAGE__
-    ->add_columns(
-        gender_id => {
-            data_type=>'integer',
-        },
-        label => {
-            data_type=>'varchar',
-            size=>12,
-        },
-    );
-
-
-=head2 primary_key
-
-Sets the Primary keys for this table
-
-=cut
-
-__PACKAGE__
-    ->set_primary_key(qw/gender_id/);
-    
-    
-=head2 
-
-Marks the unique columns
-
-=cut
-
-__PACKAGE__
-    ->add_unique_constraint('gender_label_unique' => [ qw/label/ ]);
-
-
-=head2 people
-
-A resultset of people with this gender
-
-=cut
-
-__PACKAGE__
-    ->has_many(
-        people => 'DBICNGTest::Schema::Result::Person', 
-        {'foreign.fk_gender_id' => 'self.gender_id'}
-    );
-
-
-=head1 METHODS
-
-This module defines the following methods.
-
-=head1 AUTHORS
-
-See L<DBIx::Class> for more information regarding authors.
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
-
-=cut
-
-
-1;
diff --git a/t/lib/DBICNGTest/Schema/Result/Person.pm b/t/lib/DBICNGTest/Schema/Result/Person.pm
deleted file mode 100644 (file)
index 9547cc4..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-package #hide from pause
- DBICNGTest::Schema::Result::Person;
-
-    use Moose;
-    use DateTime;
-    extends 'DBICNGTest::Schema::Result';
-
-
-=head1 NAME
-
-DBICNGTest::Schema::Result::Person; An example Person Class;
-
-=head1 DESCRIPTION
-
-Tests for this type of FK relationship
-
-=head1 ATTRIBUTES
-
-This class defines the following attributes.
-
-=head2 created
-
-attribute for the created column
-
-=cut
-
-has 'created' => (
-    is=>'ro',
-    isa=>'DateTime',
-    required=>1,
-    default=>sub {
-       DateTime->now;
-    },
-);
-
-
-=head1 PACKAGE METHODS
-
-This module defines the following package methods
-
-=head2 table
-
-Name of the Physical table in the database
-
-=cut
-
-__PACKAGE__
-    ->table('person');
-
-
-=head2 add_columns
-
-Add columns and meta information
-
-=head3 person_id
-
-Primary Key which is an auto generated autoinc
-
-=head3 fk_gender_id
-
-foreign key to the Gender table
-
-=head3 name
-
-Just an ordinary name
-
-=head3 age
-
-The person's age
-
-head3 created
-
-When the person was added to the database
-
-=cut
-
-__PACKAGE__
-    ->add_columns(
-        person_id => {
-            data_type=>'integer',
-        },
-        fk_gender_id => {
-            data_type=>'integer',
-        },      
-        name => {
-            data_type=>'varchar',
-            size=>32,
-        },
-        age => {
-            data_type=>'integer',
-            default_value=>25,
-        },
-        created => {
-            data_type=>'datetime',
-            default_value=>'date("now")',
-        });
-
-
-=head2 primary_key
-
-Sets the Primary keys for this table
-
-=cut
-
-__PACKAGE__
-    ->set_primary_key(qw/person_id/);
-
-
-=head2 friendlist
-
-Each Person might have a resultset of friendlist 
-
-=cut
-
-__PACKAGE__
-    ->has_many( 
-        friendlist => 'DBICNGTest::Schema::Result::FriendList',
-        {'foreign.fk_person_id' => 'self.person_id'});
-    
-
-=head2 gender
-
-This person's gender
-
-=cut
-
-__PACKAGE__
-    ->belongs_to( gender => 'DBICNGTest::Schema::Result::Gender', { 
-        'foreign.gender_id' => 'self.fk_gender_id' });
-        
-
-=head2 fanlist
-
-A resultset of the people listing me as a friend (if any)
-
-=cut
-
-__PACKAGE__
-    ->belongs_to( fanlist => 'DBICNGTest::Schema::Result::FriendList', { 
-        'foreign.fk_friend_id' => 'self.person_id' });
-
-
-=head2 friends
-
-A resultset of Persons who are in my FriendList
-
-=cut
-
-__PACKAGE__
-    ->many_to_many( friends => 'friendlist', 'friend' );
-    
-
-=head2 fans
-
-A resultset of people that have me in their friendlist
-
-=cut
-
-__PACKAGE__
-    ->many_to_many( fans => 'fanlist', 'befriender' );
-
-
-=head1 METHODS
-
-This module defines the following methods.
-
-=head1 AUTHORS
-
-See L<DBIx::Class> for more information regarding authors.
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
-
-=cut
-
-
-1;
diff --git a/t/lib/DBICNGTest/Schema/ResultSet.pm b/t/lib/DBICNGTest/Schema/ResultSet.pm
deleted file mode 100644 (file)
index 7bb83c6..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-package # hide from PAUSE
- DBICNGTest::Schema::ResultSet;
-    use Moose;
-    extends 'DBIx::Class::ResultSet', 'Moose::Object';
-       
-=head1 NAME
-
-DBICNGTest::Schema::ResultSet; A base ResultSet Class
-
-=head1 SYNOPSIS
-
-    package DBICNGTest::Schema::ResultSet::Member;
-    
-    use Moose;
-    extends 'DBICNGTest::Schema::ResultSet';
-    
-    ## Rest of the class definition.
-
-=head1 DESCRIPTION
-
-All ResultSet classes will inherit from this.  This provides some base function
-for all your resultsets and it is also the default resultset if you don't
-bother to declare a custom resultset in the resultset namespace
-
-=head1 PACKAGE METHODS
-
-The following is a list of package methods declared with this class.
-
-=head1 ATTRIBUTES
-
-This class defines the following attributes.
-
-=head1 METHODS
-
-This module declares the following methods
-
-=head2 new
-
-overload new to make sure we get a good meta object and that the attributes all
-get properly setup.  This is done so that our instances properly get a L<Moose>
-meta class.
-
-=cut
-
-sub new
-{
-    my $class = shift @_;
-    my $obj = $class->SUPER::new(@_);
-  
-    return $class->meta->new_object(
-        __INSTANCE__ => $obj, @_
-    );
-}
-
-
-=head1 AUTHORS
-
-See L<DBIx::Class> for more information regarding authors.
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
-
-=cut
-
-
-1;
\ No newline at end of file
diff --git a/t/lib/DBICNGTest/Schema/ResultSet/Person.pm b/t/lib/DBICNGTest/Schema/ResultSet/Person.pm
deleted file mode 100644 (file)
index 86b4dbb..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-package # hide from pause
- DBICNGTest::Schema::ResultSet::Person;
-
-       use Moose;
-       extends 'DBICNGTest::Schema::ResultSet';
-
-
-=head1 NAME
-
-DBICNGTest::Schema::ResultSet:Person; Example Resultset
-
-=head1 VERSION
-
-0.01
-
-=cut
-
-our $VERSION = '0.01';
-
-
-=head1 SYNOPSIS
-
-    ##Example Usage
-
-See Tests for more example usage.
-
-=head1 DESCRIPTION
-
-Resultset Methods for the Person Source
-
-=head1 ATTRIBUTES
-
-This class defines the following attributes.
-
-=head2 literal
-
-a literal attribute for testing
-
-=cut
-
-has 'literal' => (is=>'ro', isa=>'Str', required=>1, lazy=>1, default=>'hi');
-
-
-=head2 available_genders
-
-A resultset of the genders people can have.  Keep in mind this get's run once
-only at the first compile, so it's only good for stuff that doesn't change
-between reboots.
-
-=cut
-
-has 'available_genders' => (
-    is=>'ro',
-    isa=>'DBICNGTest::Schema::ResultSet',
-    required=>1,
-    lazy=>1,
-    default=> sub {
-        shift
-            ->result_source
-            ->schema
-            ->resultset('Gender');
-    }
-);
-
-
-=head1 METHODS
-
-This module defines the following methods.
-
-=head2 older_than($int)
-
-Only people over a given age
-
-=cut
-
-sub older_than
-{
-    my ($self, $age) = @_;
-    
-    return $self->search({age=>{'>'=>$age}});
-}
-
-
-=head1 AUTHORS
-
-See L<DBIx::Class> for more information regarding authors.
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
-
-=cut
-
-
-1;
index f222ff9..513dd65 100644 (file)
@@ -38,7 +38,9 @@ __PACKAGE__->belongs_to( artist => 'DBICTest::Schema::Artist', undef, {
 });
 
 # in case this is a single-cd it promotes a track from another cd
-__PACKAGE__->belongs_to( single_track => 'DBICTest::Schema::Track' );
+__PACKAGE__->belongs_to( single_track => 'DBICTest::Schema::Track', 'single_track', 
+    { join_type => 'left'} 
+);
 
 __PACKAGE__->has_many( tracks => 'DBICTest::Schema::Track' );
 __PACKAGE__->has_many(
@@ -82,10 +84,4 @@ __PACKAGE__->belongs_to('genre_inefficient', 'DBICTest::Schema::Genre',
     },
 );
 
-
-#__PACKAGE__->add_relationship('genre', 'DBICTest::Schema::Genre',
-#    { 'foreign.genreid' => 'self.genreid' },
-#    { 'accessor' => 'single' }
-#);
-
 1;
index d19980c..1d5c06b 100644 (file)
@@ -10,8 +10,8 @@ __PACKAGE__->table('event');
 
 __PACKAGE__->add_columns(
   id => { data_type => 'integer', is_auto_increment => 1 },
-  starts_at => { data_type => 'datetime', extra => { timezone => "America/Chicago", locale => 'de_DE' } },
-  created_on => { data_type => 'timestamp', extra => { timezone => "America/Chicago", floating_tz_ok => 1 } },
+  starts_at => { data_type => 'datetime', timezone => "America/Chicago", locale => 'de_DE' },
+  created_on => { data_type => 'timestamp', timezone => "America/Chicago", floating_tz_ok => 1 },
 );
 
 __PACKAGE__->set_primary_key('id');
diff --git a/t/lib/DBICTest/Schema/EventTZDeprecated.pm b/t/lib/DBICTest/Schema/EventTZDeprecated.pm
new file mode 100644 (file)
index 0000000..29695dd
--- /dev/null
@@ -0,0 +1,19 @@
+package DBICTest::Schema::EventTZDeprecated;
+
+use strict;
+use warnings;
+use base qw/DBIx::Class::Core/;
+
+__PACKAGE__->load_components(qw/InflateColumn::DateTime/);
+
+__PACKAGE__->table('event');
+
+__PACKAGE__->add_columns(
+  id => { data_type => 'integer', is_auto_increment => 1 },
+  starts_at => { data_type => 'datetime', extra => { timezone => "America/Chicago", locale => 'de_DE' } },
+  created_on => { data_type => 'timestamp', extra => { timezone => "America/Chicago", floating_tz_ok => 1 } },
+);
+
+__PACKAGE__->set_primary_key('id');
+
+1;
diff --git a/t/lib/DBICTest/Taint/Classes/Auto.pm b/t/lib/DBICTest/Taint/Classes/Auto.pm
new file mode 100644 (file)
index 0000000..9a30c1a
--- /dev/null
@@ -0,0 +1,7 @@
+package # hide from PAUSE 
+    DBICTest::Taint::Classes::Auto;
+
+use base 'DBIx::Class::Core';
+__PACKAGE__->table('test');
+
+1;
diff --git a/t/lib/DBICTest/Taint/Classes/Manual.pm b/t/lib/DBICTest/Taint/Classes/Manual.pm
new file mode 100644 (file)
index 0000000..5d2109b
--- /dev/null
@@ -0,0 +1,7 @@
+package # hide from PAUSE 
+    DBICTest::Taint::Classes::Manual;
+
+use base 'DBIx::Class::Core';
+__PACKAGE__->table('test');
+
+1;
diff --git a/t/lib/DBICTest/Taint/Namespaces/Result/Test.pm b/t/lib/DBICTest/Taint/Namespaces/Result/Test.pm
new file mode 100644 (file)
index 0000000..7d57bb5
--- /dev/null
@@ -0,0 +1,7 @@
+package # hide from PAUSE 
+    DBICTest::Taint::Namespaces::Result::Test;
+
+use base 'DBIx::Class::Core';
+__PACKAGE__->table('test');
+
+1;
index 105a490..9e8aaa6 100644 (file)
@@ -1,6 +1,6 @@
 -- 
 -- Created by SQL::Translator::Producer::SQLite
--- Created on Sun Feb 22 00:15:06 2009
+-- Created on Thu Apr 30 10:04:57 2009
 -- 
 
 
diff --git a/t/prefetch/attrs_untouched.t b/t/prefetch/attrs_untouched.t
new file mode 100644 (file)
index 0000000..1742770
--- /dev/null
@@ -0,0 +1,46 @@
+use warnings;  
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBICTest;
+use Data::Dumper;
+
+my $schema = DBICTest->init_schema();
+
+my $orig_debug = $schema->storage->debug;
+
+use IO::File;
+
+BEGIN {
+    eval "use DBD::SQLite";
+    plan $@
+        ? ( skip_all => 'needs DBD::SQLite for testing' )
+        : ( tests => 3 );
+}
+
+# figure out if we've got a version of sqlite that is older than 3.2.6, in
+# which case COUNT(DISTINCT()) doesn't work
+my $is_broken_sqlite = 0;
+my ($sqlite_major_ver,$sqlite_minor_ver,$sqlite_patch_ver) =
+    split /\./, $schema->storage->dbh->get_info(18);
+if( $schema->storage->dbh->get_info(17) eq 'SQLite' &&
+    ( ($sqlite_major_ver < 3) ||
+      ($sqlite_major_ver == 3 && $sqlite_minor_ver < 2) ||
+      ($sqlite_major_ver == 3 && $sqlite_minor_ver == 2 && $sqlite_patch_ver < 6) ) ) {
+    $is_broken_sqlite = 1;
+}
+
+# bug in 0.07000 caused attr (join/prefetch) to be modifed by search
+# so we check the search & attr arrays are not modified
+my $search = { 'artist.name' => 'Caterwauler McCrae' };
+my $attr = { prefetch => [ qw/artist liner_notes/ ],
+             order_by => 'me.cdid' };
+my $search_str = Dumper($search);
+my $attr_str = Dumper($attr);
+
+my $rs = $schema->resultset("CD")->search($search, $attr);
+
+is(Dumper($search), $search_str, 'Search hash untouched after search()');
+is(Dumper($attr), $attr_str, 'Attribute hash untouched after search()');
+cmp_ok($rs + 0, '==', 3, 'Correct number of records returned');
diff --git a/t/prefetch/multiple_hasmany.t b/t/prefetch/multiple_hasmany.t
new file mode 100644 (file)
index 0000000..cee298a
--- /dev/null
@@ -0,0 +1,172 @@
+use strict;
+use warnings;  
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBICTest;
+use Data::Dumper;
+
+my $schema = DBICTest->init_schema();
+
+my $orig_debug = $schema->storage->debug;
+
+use IO::File;
+
+BEGIN {
+    eval "use DBD::SQLite";
+    plan $@
+        ? ( skip_all => 'needs DBD::SQLite for testing' )
+        : ( tests => 16 );
+}
+
+# figure out if we've got a version of sqlite that is older than 3.2.6, in
+# which case COUNT(DISTINCT()) doesn't work
+my $is_broken_sqlite = 0;
+my ($sqlite_major_ver,$sqlite_minor_ver,$sqlite_patch_ver) =
+    split /\./, $schema->storage->dbh->get_info(18);
+if( $schema->storage->dbh->get_info(17) eq 'SQLite' &&
+    ( ($sqlite_major_ver < 3) ||
+      ($sqlite_major_ver == 3 && $sqlite_minor_ver < 2) ||
+      ($sqlite_major_ver == 3 && $sqlite_minor_ver == 2 && $sqlite_patch_ver < 6) ) ) {
+    $is_broken_sqlite = 1;
+}
+
+# once the following TODO is complete, remove the 2 warning tests immediately
+# after the TODO block
+# (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)';
+
+    #( 1 -> M + M )
+    my $cd_rs = $schema->resultset('CD')->search ({ 'me.title' => 'Forkful of bees' });
+    my $pr_cd_rs = $cd_rs->search ({}, {
+        prefetch => [qw/tracks tags/],
+    });
+
+    my $tracks_rs = $cd_rs->first->tracks;
+    my $tracks_count = $tracks_rs->count;
+
+    my ($pr_tracks_rs, $pr_tracks_count);
+
+    my $queries = 0;
+    $schema->storage->debugcb(sub { $queries++ });
+    $schema->storage->debug(1);
+
+    my $o_mm_warn;
+    {
+        local $SIG{__WARN__} = sub { $o_mm_warn = shift };
+        $pr_tracks_rs = $pr_cd_rs->first->tracks;
+    };
+    $pr_tracks_count = $pr_tracks_rs->count;
+
+    ok(! $o_mm_warn, 'no warning on attempt to prefetch several same level has_many\'s (1 -> M + M)');
+
+    is($queries, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query');
+    is($pr_tracks_count, $tracks_count, 'equal count of prefetched relations over several same level has_many\'s (1 -> M + M)');
+
+    for ($pr_tracks_rs, $tracks_rs) {
+        $_->result_class ('DBIx::Class::ResultClass::HashRefInflator');
+    }
+
+    is_deeply ([$pr_tracks_rs->all], [$tracks_rs->all], 'same structure returned with and without prefetch over several same level has_many\'s (1 -> M + M)');
+
+    #( M -> 1 -> M + M )
+    my $note_rs = $schema->resultset('LinerNotes')->search ({ notes => 'Buy Whiskey!' });
+    my $pr_note_rs = $note_rs->search ({}, {
+        prefetch => {
+            cd => [qw/tags tracks/]
+        },
+    });
+
+    my $tags_rs = $note_rs->first->cd->tags;
+    my $tags_count = $tags_rs->count;
+
+    my ($pr_tags_rs, $pr_tags_count);
+
+    $queries = 0;
+    $schema->storage->debugcb(sub { $queries++ });
+    $schema->storage->debug(1);
+
+    my $m_o_mm_warn;
+    {
+        local $SIG{__WARN__} = sub { $m_o_mm_warn = shift };
+        $pr_tags_rs = $pr_note_rs->first->cd->tags;
+    };
+    $pr_tags_count = $pr_tags_rs->count;
+
+    ok(! $m_o_mm_warn, 'no warning on attempt to prefetch several same level has_many\'s (M -> 1 -> M + M)');
+
+    is($queries, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query');
+
+    is($pr_tags_count, $tags_count, 'equal count of prefetched relations over several same level has_many\'s (M -> 1 -> M + M)');
+
+    for ($pr_tags_rs, $tags_rs) {
+        $_->result_class ('DBIx::Class::ResultClass::HashRefInflator');
+    }
+
+    is_deeply ([$pr_tags_rs->all], [$tags_rs->all], 'same structure returned with and without prefetch over several same level has_many\'s (M -> 1 -> M + M)');
+}
+
+# remove this closure once the TODO above is working
+my $w;
+{
+    local $SIG{__WARN__} = sub { $w = shift };
+
+    my $rs = $schema->resultset('CD')->search ({ 'me.title' => 'Forkful of bees' }, { prefetch => [qw/tracks tags/] });
+    for (qw/all count next first/) {
+        undef $w;
+        my @stuff = $rs->search()->$_;
+        like ($w, qr/will currently disrupt both the functionality of .rs->count\(\), and the amount of objects retrievable via .rs->next\(\)/,
+            "warning on ->$_ attempt prefetching several same level has_manys (1 -> M + M)");
+    }
+    my $rs2 = $schema->resultset('LinerNotes')->search ({ notes => 'Buy Whiskey!' }, { prefetch => { cd => [qw/tags tracks/] } });
+    for (qw/all count next first/) {
+        undef $w;
+        my @stuff = $rs2->search()->$_;
+        like ($w, qr/will currently disrupt both the functionality of .rs->count\(\), and the amount of objects retrievable via .rs->next\(\)/,
+            "warning on ->$_ attempt prefetching several same level has_manys (M -> 1 -> M + M)");
+    }
+}
+
+__END__
+The solution is to rewrite ResultSet->_collapse_result() and
+ResultSource->resolve_prefetch() to focus on the final results from the collapse
+of the data. Right now, the code doesn't treat the columns from the various
+tables as grouped entities. While there is a concept of hierarchy (so that
+prefetching down relationships does work as expected), there is no idea of what
+the final product should look like and how the various columns in the row would
+play together. So, the actual prefetch datastructure from the search would be
+very useful in working through this problem. We already have access to the PKs
+and sundry for those. So, when collapsing the search result, we know we are
+looking for 1 cd object. We also know we're looking for tracks and tags records
+-independently- of each other. So, we can grab the data for tracks and data for
+tags separately, uniqueing on the PK as appropriate. Then, when we're done with
+the given cd object's datastream, we know we're good. This should work for all
+the various scenarios.
+
+My reccommendation is the row's data is preprocessed first, breaking it up into
+the data for each of the component tables. (This could be done in the single
+table case, too, but probably isn't necessary.) So, starting with something
+like:
+  my $row = {
+    t1.col1 => 1,
+    t1.col2 => 2,
+    t2.col1 => 3,
+    t2.col2 => 4,
+    t3.col1 => 5,
+    t3.col2 => 6,
+  };
+it is massaged to look something like:
+  my $row_massaged = {
+    t1 => { col1 => 1, col2 => 2 },
+    t2 => { col1 => 3, col2 => 4 },
+    t3 => { col1 => 5, col2 => 6 },
+  };
+At this point, find the stuff that's different is easy enough to do and slotting
+things into the right spot is, likewise, pretty straightforward. Instead of
+storing things in a AoH, store them in a HoH keyed on the PKs of the the table,
+then convert to an AoH after all collapsing is done.
+
+This implies that the collapse attribute can probably disappear or, at the
+least, be turned into a boolean (which is how it's used in every other place).
diff --git a/t/prefetch/pollute_already_joined.t b/t/prefetch/pollute_already_joined.t
new file mode 100644 (file)
index 0000000..035aa5b
--- /dev/null
@@ -0,0 +1,75 @@
+use strict;
+use warnings;  
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBICTest;
+use Data::Dumper;
+
+my $schema = DBICTest->init_schema();
+
+my $orig_debug = $schema->storage->debug;
+
+use IO::File;
+
+BEGIN {
+    eval "use DBD::SQLite";
+    plan $@
+        ? ( skip_all => 'needs DBD::SQLite for testing' )
+        : ( tests => 10 );
+}
+
+# figure out if we've got a version of sqlite that is older than 3.2.6, in
+# which case COUNT(DISTINCT()) doesn't work
+my $is_broken_sqlite = 0;
+my ($sqlite_major_ver,$sqlite_minor_ver,$sqlite_patch_ver) =
+    split /\./, $schema->storage->dbh->get_info(18);
+if( $schema->storage->dbh->get_info(17) eq 'SQLite' &&
+    ( ($sqlite_major_ver < 3) ||
+      ($sqlite_major_ver == 3 && $sqlite_minor_ver < 2) ||
+      ($sqlite_major_ver == 3 && $sqlite_minor_ver == 2 && $sqlite_patch_ver < 6) ) ) {
+    $is_broken_sqlite = 1;
+}
+
+# A search() with prefetch seems to pollute an already joined resultset
+# in a way that offsets future joins (adapted from a test case by Debolaz)
+{
+  my ($cd_rs, $attrs);
+
+  # test a real-life case - rs is obtained by an implicit m2m join
+  $cd_rs = $schema->resultset ('Producer')->first->cds;
+  $attrs = Dumper $cd_rs->{attrs};
+
+  $cd_rs->search ({})->all;
+  is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after a simple search');
+
+  lives_ok (sub {
+    $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
+    is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after search with prefetch');
+  }, 'first prefetching search ok');
+
+  lives_ok (sub {
+    $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
+    is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after another search with prefetch')
+  }, 'second prefetching search ok');
+
+
+  # test a regular rs with an empty seen_join injected - it should still work!
+  $cd_rs = $schema->resultset ('CD');
+  $cd_rs->{attrs}{seen_join}  = {};
+  $attrs = Dumper $cd_rs->{attrs};
+
+  $cd_rs->search ({})->all;
+  is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after a simple search');
+
+  lives_ok (sub {
+    $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
+    is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after search with prefetch');
+  }, 'first prefetching search ok');
+
+  lives_ok (sub {
+    $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
+    is (Dumper ($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after another search with prefetch')
+  }, 'second prefetching search ok');
+}
diff --git a/t/prefetch/rows_bug.t b/t/prefetch/rows_bug.t
new file mode 100644 (file)
index 0000000..1457013
--- /dev/null
@@ -0,0 +1,83 @@
+# Test to ensure we get a consistent result set wether or not we use the
+# prefetch option in combination rows (LIMIT).
+use strict;
+use warnings;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest;
+
+plan $@ ? (skip_all => 'needs DBD::SQLite for testing') : (tests => 2);
+
+my $schema = DBICTest->init_schema();
+my $no_prefetch = $schema->resultset('Artist')->search(
+  undef,
+  { rows => 3 }
+);
+
+my $use_prefetch = $schema->resultset('Artist')->search(
+  undef,
+  {
+    prefetch => 'cds',
+    rows     => 3
+  }
+);
+
+my $no_prefetch_count  = 0;
+my $use_prefetch_count = 0;
+
+is($no_prefetch->count, $use_prefetch->count, '$no_prefetch->count == $use_prefetch->count');
+
+TODO: {
+  local $TODO = "This is a difficult bug to fix, workaround is not to use prefetch with rows";
+  $no_prefetch_count++  while $no_prefetch->next;
+  $use_prefetch_count++ while $use_prefetch->next;
+  is(
+    $no_prefetch_count,
+    $use_prefetch_count,
+    "manual row count confirms consistency"
+    . " (\$no_prefetch_count == $no_prefetch_count, "
+    . " \$use_prefetch_count == $use_prefetch_count)"
+  );
+}
+
+__END__
+The fix is to, when using prefetch, take the query and put it into a subquery
+joined to the tables we're prefetching from. This might result in the same
+table being joined once in the main subquery and once in the main query. This
+may actually resolve other, unknown edgecase bugs. It is also the right way
+to do prefetching. Optimizations can come later.
+
+This means that:
+  $foo_rs->search(
+    { ... },
+    {
+      prefetch => 'bar',
+      ...
+    },
+  );
+
+becomes:
+  my $temp = $foo_rs->search(
+    { ... },
+    {
+      join => 'bar',
+      ...
+    },
+  );
+  $foo_rs->storage->schema->resultset('foo')->search(
+    undef,
+    {
+      from => [
+        { me => $temp->as_query },
+      ],
+      prefetch => 'bar',
+    },
+  );
+
+Problem:
+  * The prefetch->join change needs to happen ONLY IF there are conditions
+    that depend on bar being joined.
+  * How will this work when the $rs is further searched on? Those clauses
+    need to be added to the subquery, not the outer one. This is particularly
+    true if rows is added in the attribute later per the Pager.
diff --git a/t/prefetch/standard.t b/t/prefetch/standard.t
new file mode 100644 (file)
index 0000000..c25e010
--- /dev/null
@@ -0,0 +1,340 @@
+use strict;
+use warnings;  
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBICTest;
+use Data::Dumper;
+
+my $schema = DBICTest->init_schema();
+
+my $orig_debug = $schema->storage->debug;
+
+use IO::File;
+
+BEGIN {
+    eval "use DBD::SQLite";
+    plan $@
+        ? ( skip_all => 'needs DBD::SQLite for testing' )
+        : ( tests => 45 );
+}
+
+# figure out if we've got a version of sqlite that is older than 3.2.6, in
+# which case COUNT(DISTINCT()) doesn't work
+my $is_broken_sqlite = 0;
+my ($sqlite_major_ver,$sqlite_minor_ver,$sqlite_patch_ver) =
+    split /\./, $schema->storage->dbh->get_info(18);
+if( $schema->storage->dbh->get_info(17) eq 'SQLite' &&
+    ( ($sqlite_major_ver < 3) ||
+      ($sqlite_major_ver == 3 && $sqlite_minor_ver < 2) ||
+      ($sqlite_major_ver == 3 && $sqlite_minor_ver == 2 && $sqlite_patch_ver < 6) ) ) {
+    $is_broken_sqlite = 1;
+}
+
+my $queries = 0;
+$schema->storage->debugcb(sub { $queries++; });
+$schema->storage->debug(1);
+
+my $search = { 'artist.name' => 'Caterwauler McCrae' };
+my $attr = { prefetch => [ qw/artist liner_notes/ ],
+             order_by => 'me.cdid' };
+my $search_str = Dumper($search);
+my $attr_str = Dumper($attr);
+
+my $rs = $schema->resultset("CD")->search($search, $attr);
+my @cd = $rs->all;
+
+is($cd[0]->title, 'Spoonful of bees', 'First record returned ok');
+
+ok(!defined $cd[0]->liner_notes, 'No prefetch for NULL LEFT join');
+
+is($cd[1]->{_relationship_data}{liner_notes}->notes, 'Buy Whiskey!', 'Prefetch for present LEFT JOIN');
+
+is(ref $cd[1]->liner_notes, 'DBICTest::LinerNotes', 'Prefetch returns correct class');
+
+is($cd[2]->{_inflated_column}{artist}->name, 'Caterwauler McCrae', 'Prefetch on parent object ok');
+
+is($queries, 1, 'prefetch ran only 1 select statement');
+
+$schema->storage->debug($orig_debug);
+$schema->storage->debugobj->callback(undef);
+
+# test for partial prefetch via columns attr
+my $cd = $schema->resultset('CD')->find(1,
+    {
+      columns => [qw/title artist artist.name/], 
+      join => { 'artist' => {} }
+    }
+);
+ok(eval { $cd->artist->name eq 'Caterwauler McCrae' }, 'single related column prefetched');
+
+# start test for nested prefetch SELECT count
+$queries = 0;
+$schema->storage->debugcb(sub { $queries++ });
+$schema->storage->debug(1);
+
+$rs = $schema->resultset('Tag')->search(
+  {},
+  {
+    prefetch => { cd => 'artist' }
+  }
+);
+
+my $tag = $rs->first;
+
+is( $tag->cd->title, 'Spoonful of bees', 'step 1 ok for nested prefetch' );
+
+is( $tag->cd->artist->name, 'Caterwauler McCrae', 'step 2 ok for nested prefetch');
+
+# count the SELECTs
+#$selects++ if /SELECT(?!.*WHERE 1=0.*)/;
+is($queries, 1, 'nested prefetch ran exactly 1 select statement (excluding column_info)');
+
+$queries = 0;
+
+is($tag->search_related('cd')->search_related('artist')->first->name,
+   'Caterwauler McCrae',
+   'chained belongs_to->belongs_to search_related ok');
+
+is($queries, 0, 'chained search_related after belontgs_to->belongs_to prefetch ran no queries');
+
+$queries = 0;
+
+$cd = $schema->resultset('CD')->find(1, { prefetch => 'artist' });
+
+is($cd->{_inflated_column}{artist}->name, 'Caterwauler McCrae', 'artist prefetched correctly on find');
+
+is($queries, 1, 'find with prefetch ran exactly 1 select statement (excluding column_info)');
+
+$queries = 0;
+
+$schema->storage->debugcb(sub { $queries++; });
+
+$cd = $schema->resultset('CD')->find(1, { prefetch => { cd_to_producer => 'producer' } });
+
+is($cd->producers->first->name, 'Matt S Trout', 'many_to_many accessor ok');
+
+is($queries, 1, 'many_to_many accessor with nested prefetch ran exactly 1 query');
+
+$queries = 0;
+
+my $producers = $cd->search_related('cd_to_producer')->search_related('producer');
+
+is($producers->first->name, 'Matt S Trout', 'chained many_to_many search_related ok');
+
+is($queries, 0, 'chained search_related after many_to_many prefetch ran no queries');
+
+$schema->storage->debug($orig_debug);
+$schema->storage->debugobj->callback(undef);
+
+$rs = $schema->resultset('Tag')->search(
+  {},
+  {
+    join => { cd => 'artist' },
+    prefetch => { cd => 'artist' }
+  }
+);
+
+cmp_ok( $rs->count, '>=', 0, 'nested prefetch does not duplicate joins' );
+
+my ($artist) = $schema->resultset("Artist")->search({ 'cds.year' => 2001 },
+                 { order_by => 'artistid DESC', join => 'cds' });
+
+is($artist->name, 'Random Boy Band', "Join search by object ok");
+
+my @cds = $schema->resultset("CD")->search({ 'liner_notes.notes' => 'Buy Merch!' },
+                               { join => 'liner_notes' });
+
+cmp_ok(scalar @cds, '==', 1, "Single CD retrieved via might_have");
+
+is($cds[0]->title, "Generic Manufactured Singles", "Correct CD retrieved");
+
+my @artists = $schema->resultset("Artist")->search({ 'tags.tag' => 'Shiny' },
+                                       { join => { 'cds' => 'tags' } });
+
+cmp_ok( @artists, '==', 2, "two-join search ok" );
+
+$rs = $schema->resultset("CD")->search(
+  {},
+  { group_by => [qw/ title me.cdid /] }
+);
+
+SKIP: {
+    skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 1
+        if $is_broken_sqlite;
+    cmp_ok( $rs->count, '==', 5, "count() ok after group_by on main pk" );
+}
+
+cmp_ok( scalar $rs->all, '==', 5, "all() returns same count as count() after group_by on main pk" );
+
+$rs = $schema->resultset("CD")->search(
+  {},
+  { join => [qw/ artist /], group_by => [qw/ artist.name /] }
+);
+
+SKIP: {
+    skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 1
+        if $is_broken_sqlite;
+    cmp_ok( $rs->count, '==', 3, "count() ok after group_by on related column" );
+}
+
+$rs = $schema->resultset("Artist")->search(
+  {},
+      { join => [qw/ cds /], group_by => [qw/ me.name /], having =>{ 'MAX(cds.cdid)'=> \'< 5' } }
+);
+
+cmp_ok( $rs->all, '==', 2, "results ok after group_by on related column with a having" );
+
+$rs = $rs->search( undef, {  having =>{ 'count(*)'=> \'> 2' }});
+
+cmp_ok( $rs->all, '==', 1, "count() ok after group_by on related column with a having" );
+
+$rs = $schema->resultset("Artist")->search(
+        { 'cds.title' => 'Spoonful of bees',
+          'cds_2.title' => 'Forkful of bees' },
+        { join => [ 'cds', 'cds' ] });
+
+SKIP: {
+    skip "SQLite < 3.2.6 doesn't understand COUNT(DISTINCT())", 1
+        if $is_broken_sqlite;
+    cmp_ok($rs->count, '==', 1, "single artist returned from multi-join");
+}
+
+is($rs->next->name, 'Caterwauler McCrae', "Correct artist returned");
+
+$cd = $schema->resultset('Artist')->first->create_related('cds',
+    {
+    title   => 'Unproduced Single',
+    year    => 2007
+});
+
+my $left_join = $schema->resultset('CD')->search(
+    { 'me.cdid' => $cd->cdid },
+    { prefetch => { cd_to_producer => 'producer' } }
+);
+
+cmp_ok($left_join, '==', 1, 'prefetch with no join record present');
+
+$queries = 0;
+$schema->storage->debugcb(sub { $queries++ });
+$schema->storage->debug(1);
+
+my $tree_like =
+     $schema->resultset('TreeLike')->find(5,
+       { join     => { parent => { parent => 'parent' } },
+         prefetch => { parent => { parent => 'parent' } } });
+
+is($tree_like->name, 'quux', 'Bottom of tree ok');
+$tree_like = $tree_like->parent;
+is($tree_like->name, 'baz', 'First level up ok');
+$tree_like = $tree_like->parent;
+is($tree_like->name, 'bar', 'Second level up ok');
+$tree_like = $tree_like->parent;
+is($tree_like->name, 'foo', 'Third level up ok');
+
+$schema->storage->debug($orig_debug);
+$schema->storage->debugobj->callback(undef);
+
+cmp_ok($queries, '==', 1, 'Only one query run');
+
+$tree_like = $schema->resultset('TreeLike')->search({'me.id' => 2});
+$tree_like = $tree_like->search_related('children')->search_related('children')->search_related('children')->first;
+is($tree_like->name, 'quux', 'Tree search_related ok');
+
+$tree_like = $schema->resultset('TreeLike')->search_related('children',
+    { 'children.id' => 3, 'children_2.id' => 4 },
+    { prefetch => { children => 'children' } }
+  )->first;
+is(eval { $tree_like->children->first->children->first->name }, 'quux',
+   'Tree search_related with prefetch ok');
+
+$tree_like = eval { $schema->resultset('TreeLike')->search(
+    { 'children.id' => 3, 'children_2.id' => 6 }, 
+    { join => [qw/children children/] }
+  )->search_related('children', { 'children_4.id' => 7 }, { prefetch => 'children' }
+  )->first->children->first; };
+is(eval { $tree_like->name }, 'fong', 'Tree with multiple has_many joins ok');
+
+# test that collapsed joins don't get a _2 appended to the alias
+
+my $sql = '';
+$schema->storage->debugcb(sub { $sql = $_[1] });
+$schema->storage->debug(1);
+
+eval {
+  my $row = $schema->resultset('Artist')->search_related('cds', undef, {
+    join => 'tracks',
+    prefetch => 'tracks',
+  })->search_related('tracks')->first;
+};
+
+like( $sql, qr/^SELECT tracks_2\.trackid/, "join not collapsed for search_related" );
+
+$schema->storage->debug($orig_debug);
+$schema->storage->debugobj->callback(undef);
+
+$rs = $schema->resultset('Artist');
+$rs->create({ artistid => 4, name => 'Unknown singer-songwriter' });
+$rs->create({ artistid => 5, name => 'Emo 4ever' });
+@artists = $rs->search(undef, { prefetch => 'cds', order_by => 'artistid' });
+is(scalar @artists, 5, 'has_many prefetch with adjacent empty rows ok');
+
+# -------------
+#
+# Tests for multilevel has_many prefetch
+
+# artist resultsets - with and without prefetch
+my $art_rs = $schema->resultset('Artist');
+my $art_rs_pr = $art_rs->search(
+    {},
+    {
+        join     => [ { cds => ['tracks'] } ],
+        prefetch => [ { cds => ['tracks'] } ],
+        cache    => 1 # last test needs this
+    }
+);
+
+# This test does the same operation twice - once on a
+# set of items fetched from the db with no prefetch of has_many rels
+# The second prefetches 2 levels of has_many
+# We check things are the same by comparing the name or title
+# we build everything into a hash structure and compare the one
+# from each rs to see what differs
+
+sub make_hash_struc {
+    my $rs = shift;
+
+    my $struc = {};
+    foreach my $art ( $rs->all ) {
+        foreach my $cd ( $art->cds ) {
+            foreach my $track ( $cd->tracks ) {
+                $struc->{ $art->name }{ $cd->title }{ $track->title }++;
+            }
+        }
+    }
+    return $struc;
+}
+
+$queries = 0;
+$schema->storage->debugcb(sub { $queries++ });
+$schema->storage->debug(1);
+
+my $prefetch_result = make_hash_struc($art_rs_pr);
+
+is($queries, 1, 'nested prefetch across has_many->has_many ran exactly 1 query');
+
+my $nonpre_result   = make_hash_struc($art_rs);
+
+is_deeply( $prefetch_result, $nonpre_result,
+    'Compare 2 level prefetch result to non-prefetch result' );
+
+$queries = 0;
+
+is($art_rs_pr->search_related('cds')->search_related('tracks')->first->title,
+   'Fowlin',
+   'chained has_many->has_many search_related ok'
+  );
+
+is($queries, 0, 'chained search_related after has_many->has_many prefetch ran no queries');
+
index 5071f0c..6065307 100644 (file)
@@ -7,12 +7,7 @@ use Data::Dumper;
 
 use Test::More;
 
-BEGIN {
-    eval "use SQL::Abstract 1.49";
-    plan $@
-        ? ( skip_all => "Needs SQLA 1.49+" )
-        : ( tests => 4 );
-}
+plan ( tests => 5 );
 
 use lib qw(t/lib);
 use DBICTest;
@@ -71,4 +66,13 @@ my $rscol = $art_rs->get_column( 'charfield' );
   );
 }
 
+{
+    my $rs = $schema->resultset("CD")->search(
+        { 'artist.name' => 'Caterwauler McCrae' },
+        { join => [qw/artist/]}
+    );
+    my $subsel_rs = $schema->resultset("CD")->search( { cdid => { IN => $rs->get_column('cdid')->as_query } } );
+    cmp_ok($subsel_rs->count, '==', $rs->count, 'Subselect on PK got the same row count');
+}
+
 __END__
index b5e0548..c2a1bff 100644 (file)
@@ -7,12 +7,7 @@ use Data::Dumper;
 
 use Test::More;
 
-BEGIN {
-    eval "use SQL::Abstract 1.49";
-    plan $@
-        ? ( skip_all => "Needs SQLA 1.49+" )
-        : ( tests => 8 );
-}
+plan ( tests => 8 );
 
 use lib qw(t/lib);
 use DBICTest;
@@ -31,7 +26,7 @@ my $cdrs = $schema->resultset('CD');
   my ($query, @bind) = @{$$arr};
   is_same_sql_bind(
     $query, \@bind,
-    "SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE artist_id IN ( SELECT id FROM artist me LIMIT 1 )",
+    "( SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE artist_id IN ( SELECT id FROM artist me LIMIT 1 ) )",
     [],
   );
 }
@@ -50,7 +45,7 @@ my $cdrs = $schema->resultset('CD');
   my ($query, @bind) = @{$$arr};
   is_same_sql_bind(
     $query, \@bind,
-    "SELECT (SELECT id FROM cd me LIMIT 1) FROM artist me",
+    "( SELECT (SELECT id FROM cd me LIMIT 1) FROM artist me )",
     [],
   );
 }
@@ -69,7 +64,7 @@ my $cdrs = $schema->resultset('CD');
   my ($query, @bind) = @{$$arr};
   is_same_sql_bind(
     $query, \@bind,
-    "SELECT me.artistid, me.name, me.rank, me.charfield, (SELECT id FROM cd me LIMIT 1) FROM artist me",
+    "( SELECT me.artistid, me.name, me.rank, me.charfield, (SELECT id FROM cd me LIMIT 1) FROM artist me )",
     [],
   );
 }
@@ -90,8 +85,10 @@ my $cdrs = $schema->resultset('CD');
   my ($query, @bind) = @{$$arr};
   is_same_sql_bind(
     $query, \@bind,
-    "SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE id > 20) cd2",
-    [],
+    "( SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE id > ?) cd2 )",
+    [
+      [ 'id', 20 ]
+    ],
   );
 }
 
@@ -108,7 +105,7 @@ my $cdrs = $schema->resultset('CD');
   my ($query, @bind) = @{$$arr};
   is_same_sql_bind(
     $query, \@bind,
-    "SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me JOIN (SELECT me.artist as cds_artist FROM cd me) cds ON me.artistid = cds_artist", []
+    "( SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me JOIN (SELECT me.artist as cds_artist FROM cd me) cds ON me.artistid = cds_artist )", []
   );
 
 
@@ -137,14 +134,18 @@ my $cdrs = $schema->resultset('CD');
   my ($query, @bind) = @{$$arr};
   is_same_sql_bind(
     $query, \@bind,
-    "SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track 
+    "( SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track 
       FROM 
         (SELECT cd3.cdid,cd3.artist,cd3.title,cd3.year,cd3.genreid,cd3.single_track 
           FROM 
             (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track 
-              FROM cd me WHERE id < 40) cd3
-          WHERE id > 20) cd2",
-    [],
+              FROM cd me WHERE id < ?) cd3
+          WHERE id > ?) cd2
+    )",
+    [
+      [ 'id', 40 ], 
+      [ 'id', 20 ]
+    ],
   );
 
 }
@@ -162,7 +163,7 @@ my $cdrs = $schema->resultset('CD');
   my ($query, @bind) = @{$$arr};
   is_same_sql_bind(
     $query, \@bind,
-    "SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me WHERE year = (SELECT MAX(inner.year) FROM cd inner WHERE artistid = me.artistid)",
+    "( SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me WHERE year = (SELECT MAX(inner.year) FROM cd inner WHERE artistid = me.artistid) )",
     [],
   );
 }
@@ -182,8 +183,8 @@ my $cdrs = $schema->resultset('CD');
   my ($query, @bind) = @{$$arr};
   is_same_sql_bind(
     $query, \@bind,
-    "SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE title = 'Thriller') cd2",
-    [],
+    "(SELECT cd2.cdid, cd2.artist, cd2.title, cd2.year, cd2.genreid, cd2.single_track FROM (SELECT me.cdid,me.artist,me.title,me.year,me.genreid,me.single_track FROM cd me WHERE title = ?) cd2)",
+    ['Thriller'],
   );
 }