Switch the main dev branch back to 'master'
Peter Rabbitson [Fri, 26 Feb 2016 08:57:27 +0000 (09:57 +0100)]
Previously the idea was to have two dev branches - one for "the crowd", and
another one for the rel manager to do periodic squashes, merges, cleanups etc.
This worked rather well for about 1.5 years, but with the primary architect
outgoing it can't be sustained, and only serves to confuse everyone.

Thus a fast forward with a mini-merge of master up to the tip of current/blead
and continuing pushes to master alone from there on. Not yet deleting the old
'current/blead' and 'current/for_cpan_index' branches, as it affects PR states
due to useless "smarts" in the github logic.

459 files changed:
.dir-locals.el [new file with mode: 0644]
.mailmap
.travis.yml
AUTHORS [new file with mode: 0644]
Changes
LICENSE [new file with mode: 0644]
MANIFEST.SKIP
Makefile.PL
examples/Benchmarks/semicolon_vs_comma_rowparser/comma.src [new file with mode: 0644]
examples/Benchmarks/semicolon_vs_comma_rowparser/semicol.src [new file with mode: 0644]
examples/Benchmarks/semicolon_vs_comma_rowparser/sloppy_bench.pl [new file with mode: 0644]
examples/Schema/MyApp/Schema/Result/Artist.pm
examples/Schema/MyApp/Schema/Result/Cd.pm
examples/Schema/MyApp/Schema/Result/Track.pm
examples/Schema/insertdb.pl
examples/Schema/testdb.pl
lib/DBIx/Class.pm
lib/DBIx/Class/AccessorGroup.pm
lib/DBIx/Class/Admin.pm
lib/DBIx/Class/Admin/Usage.pm
lib/DBIx/Class/CDBICompat.pm
lib/DBIx/Class/CDBICompat/AbstractSearch.pm
lib/DBIx/Class/CDBICompat/AccessorMapping.pm
lib/DBIx/Class/CDBICompat/ColumnGroups.pm
lib/DBIx/Class/CDBICompat/ColumnsAsHash.pm
lib/DBIx/Class/CDBICompat/Constructor.pm
lib/DBIx/Class/CDBICompat/Copy.pm
lib/DBIx/Class/CDBICompat/DestroyWarning.pm
lib/DBIx/Class/CDBICompat/ImaDBI.pm
lib/DBIx/Class/CDBICompat/Iterator.pm
lib/DBIx/Class/CDBICompat/NoObjectIndex.pm
lib/DBIx/Class/CDBICompat/Pager.pm
lib/DBIx/Class/CDBICompat/Relationship.pm
lib/DBIx/Class/CDBICompat/Relationships.pm
lib/DBIx/Class/CDBICompat/Retrieve.pm
lib/DBIx/Class/CDBICompat/SQLTransformer.pm
lib/DBIx/Class/Carp.pm
lib/DBIx/Class/Core.pm
lib/DBIx/Class/Cursor.pm
lib/DBIx/Class/DB.pm
lib/DBIx/Class/Exception.pm
lib/DBIx/Class/FilterColumn.pm
lib/DBIx/Class/InflateColumn.pm
lib/DBIx/Class/InflateColumn/DateTime.pm
lib/DBIx/Class/InflateColumn/File.pm
lib/DBIx/Class/Manual.pod
lib/DBIx/Class/Manual/Component.pod
lib/DBIx/Class/Manual/Cookbook.pod
lib/DBIx/Class/Manual/DocMap.pod
lib/DBIx/Class/Manual/Example.pod
lib/DBIx/Class/Manual/FAQ.pod
lib/DBIx/Class/Manual/Features.pod
lib/DBIx/Class/Manual/Glossary.pod
lib/DBIx/Class/Manual/Intro.pod
lib/DBIx/Class/Manual/Joining.pod
lib/DBIx/Class/Manual/QuickStart.pod
lib/DBIx/Class/Manual/Reading.pod
lib/DBIx/Class/Manual/ResultClass.pod.proto
lib/DBIx/Class/Manual/Troubleshooting.pod
lib/DBIx/Class/Optional/Dependencies.pm
lib/DBIx/Class/Ordered.pm
lib/DBIx/Class/PK.pm
lib/DBIx/Class/PK/Auto.pm
lib/DBIx/Class/PK/Auto/DB2.pm
lib/DBIx/Class/PK/Auto/MSSQL.pm
lib/DBIx/Class/PK/Auto/MySQL.pm
lib/DBIx/Class/PK/Auto/Oracle.pm
lib/DBIx/Class/PK/Auto/Pg.pm
lib/DBIx/Class/PK/Auto/SQLite.pm
lib/DBIx/Class/Relationship.pm
lib/DBIx/Class/Relationship/Accessor.pm
lib/DBIx/Class/Relationship/Base.pm
lib/DBIx/Class/Relationship/BelongsTo.pm
lib/DBIx/Class/Relationship/CascadeActions.pm
lib/DBIx/Class/Relationship/HasMany.pm
lib/DBIx/Class/Relationship/HasOne.pm
lib/DBIx/Class/Relationship/ManyToMany.pm
lib/DBIx/Class/Relationship/ProxyMethods.pm
lib/DBIx/Class/ResultClass/HashRefInflator.pm
lib/DBIx/Class/ResultSet.pm
lib/DBIx/Class/ResultSet/Pager.pm
lib/DBIx/Class/ResultSetColumn.pm
lib/DBIx/Class/ResultSetManager.pm
lib/DBIx/Class/ResultSource.pm
lib/DBIx/Class/ResultSource/RowParser.pm
lib/DBIx/Class/ResultSource/RowParser/Util.pm
lib/DBIx/Class/ResultSource/Table.pm
lib/DBIx/Class/ResultSource/View.pm
lib/DBIx/Class/ResultSourceHandle.pm
lib/DBIx/Class/ResultSourceProxy.pm
lib/DBIx/Class/ResultSourceProxy/Table.pm
lib/DBIx/Class/Row.pm
lib/DBIx/Class/SQLMaker.pm
lib/DBIx/Class/SQLMaker/LimitDialects.pm
lib/DBIx/Class/SQLMaker/Oracle.pm
lib/DBIx/Class/SQLMaker/OracleJoins.pm
lib/DBIx/Class/Schema.pm
lib/DBIx/Class/Schema/Versioned.pm
lib/DBIx/Class/Serialize/Storable.pm
lib/DBIx/Class/StartupCheck.pm
lib/DBIx/Class/Storage.pm
lib/DBIx/Class/Storage/BlockRunner.pm
lib/DBIx/Class/Storage/DBI.pm
lib/DBIx/Class/Storage/DBI/ACCESS.pm
lib/DBIx/Class/Storage/DBI/ADO.pm
lib/DBIx/Class/Storage/DBI/ADO/MS_Jet.pm
lib/DBIx/Class/Storage/DBI/ADO/MS_Jet/Cursor.pm
lib/DBIx/Class/Storage/DBI/ADO/Microsoft_SQL_Server.pm
lib/DBIx/Class/Storage/DBI/ADO/Microsoft_SQL_Server/Cursor.pm
lib/DBIx/Class/Storage/DBI/AutoCast.pm
lib/DBIx/Class/Storage/DBI/Cursor.pm
lib/DBIx/Class/Storage/DBI/DB2.pm
lib/DBIx/Class/Storage/DBI/Firebird.pm
lib/DBIx/Class/Storage/DBI/Firebird/Common.pm
lib/DBIx/Class/Storage/DBI/IdentityInsert.pm
lib/DBIx/Class/Storage/DBI/Informix.pm
lib/DBIx/Class/Storage/DBI/InterBase.pm
lib/DBIx/Class/Storage/DBI/MSSQL.pm
lib/DBIx/Class/Storage/DBI/NoBindVars.pm
lib/DBIx/Class/Storage/DBI/ODBC.pm
lib/DBIx/Class/Storage/DBI/ODBC/ACCESS.pm
lib/DBIx/Class/Storage/DBI/ODBC/DB2_400_SQL.pm
lib/DBIx/Class/Storage/DBI/ODBC/Firebird.pm
lib/DBIx/Class/Storage/DBI/ODBC/Microsoft_SQL_Server.pm
lib/DBIx/Class/Storage/DBI/ODBC/SQL_Anywhere.pm
lib/DBIx/Class/Storage/DBI/Oracle.pm
lib/DBIx/Class/Storage/DBI/Oracle/Generic.pm
lib/DBIx/Class/Storage/DBI/Oracle/WhereJoins.pm
lib/DBIx/Class/Storage/DBI/Pg.pm
lib/DBIx/Class/Storage/DBI/Replicated.pm
lib/DBIx/Class/Storage/DBI/Replicated/Balancer.pm
lib/DBIx/Class/Storage/DBI/Replicated/Balancer/First.pm
lib/DBIx/Class/Storage/DBI/Replicated/Balancer/Random.pm
lib/DBIx/Class/Storage/DBI/Replicated/Introduction.pod
lib/DBIx/Class/Storage/DBI/Replicated/Pool.pm
lib/DBIx/Class/Storage/DBI/Replicated/Replicant.pm
lib/DBIx/Class/Storage/DBI/Replicated/Types.pm
lib/DBIx/Class/Storage/DBI/Replicated/WithDSN.pm
lib/DBIx/Class/Storage/DBI/SQLAnywhere.pm
lib/DBIx/Class/Storage/DBI/SQLAnywhere/Cursor.pm
lib/DBIx/Class/Storage/DBI/SQLite.pm
lib/DBIx/Class/Storage/DBI/Sybase.pm
lib/DBIx/Class/Storage/DBI/Sybase/ASE.pm
lib/DBIx/Class/Storage/DBI/Sybase/ASE/NoBindVars.pm
lib/DBIx/Class/Storage/DBI/Sybase/FreeTDS.pm
lib/DBIx/Class/Storage/DBI/Sybase/MSSQL.pm
lib/DBIx/Class/Storage/DBI/Sybase/Microsoft_SQL_Server.pm
lib/DBIx/Class/Storage/DBI/Sybase/Microsoft_SQL_Server/NoBindVars.pm
lib/DBIx/Class/Storage/DBI/UniqueIdentifier.pm
lib/DBIx/Class/Storage/DBI/mysql.pm
lib/DBIx/Class/Storage/DBIHacks.pm
lib/DBIx/Class/Storage/Statistics.pm
lib/DBIx/Class/Storage/TxnScopeGuard.pm
lib/DBIx/Class/UTF8Columns.pm
lib/DBIx/Class/_Util.pm
lib/SQL/Translator/Parser/DBIx/Class.pm
lib/SQL/Translator/Producer/DBIx/Class/File.pm
maint/Makefile.PL.inc/11_authortests.pl
maint/Makefile.PL.inc/12_authordeps.pl
maint/Makefile.PL.inc/21_meta_noindex.pl [deleted file]
maint/Makefile.PL.inc/21_set_meta.pl [new file with mode: 0644]
maint/Makefile.PL.inc/29_handle_version.pl
maint/Makefile.PL.inc/50_redefine_makefile_flow.pl
maint/Makefile.PL.inc/52_autogen_README.pl [deleted file]
maint/Makefile.PL.inc/53_autogen_pod.pl
maint/Makefile.PL.inc/54_autogen_legalese_and_README.pl [new file with mode: 0644]
maint/gen_pod_authors [new file with mode: 0755]
maint/gen_pod_inherit
maint/getstatus [new file with mode: 0755]
maint/git_config_dbic.inc [new file with mode: 0644]
maint/travis-ci_scripts/10_before_install.bash
maint/travis-ci_scripts/20_install.bash
maint/travis-ci_scripts/30_before_script.bash
maint/travis-ci_scripts/40_script.bash
maint/travis-ci_scripts/50_after_failure.bash
maint/travis-ci_scripts/50_after_success.bash
maint/travis-ci_scripts/60_after_script.bash
maint/travis-ci_scripts/common.bash
maint/travis-ci_scripts/configs/minimal_mysql_travis.cnf [new file with mode: 0644]
maint/travis-ci_scripts/lib/TAP/Harness/IgnoreNonessentialDzilAutogeneratedTests.pm [deleted file]
script/dbicadmin
t/00describe_environment.t [new file with mode: 0644]
t/100populate.t
t/101populate_rs.t
t/104view.t
t/18insert_default.t
t/33exception_wrap.t
t/34exception_action.t
t/35exception_inaction.t [new file with mode: 0644]
t/40compose_connection.t
t/46where_attribute.t
t/50fork.t
t/51threadnodb.t
t/51threads.t
t/51threadtxn.t
t/52leaks.t
t/54taint.t
t/60core.t
t/61findnot.t
t/64db.t
t/67pager.t
t/71mysql.t
t/72pg.t
t/72pg_bytea.t
t/73oracle.t
t/73oracle_blob.t
t/73oracle_hq.t
t/745db2.t
t/746db2_400.t
t/746mssql.t
t/746sybase.t
t/747mssql_ado.t
t/748informix.t
t/749sqlanywhere.t
t/74mssql.t
t/750firebird.t
t/751msaccess.t
t/752sqlite.t
t/76joins.t
t/76select.t
t/80unique.t
t/82cascade_copy.t
t/83cache.t
t/84serialize.t
t/85utf8.t
t/86might_have.t
t/86sqlt.t
t/87ordered.t
t/88result_set_column.t
t/90join_torture.t
t/93autocast.t
t/94versioning.t
t/98savepoints.t [deleted file]
t/99dbic_sqlt_parser.t
t/admin/01load.t [deleted file]
t/admin/02ddl.t
t/admin/03data.t
t/cdbi/01-columns.t
t/cdbi/02-Film.t
t/cdbi/03-subclassing.t
t/cdbi/04-lazy.t
t/cdbi/06-hasa.t
t/cdbi/08-inheritcols.t
t/cdbi/09-has_many.t
t/cdbi/11-triggers.t
t/cdbi/12-filter.t
t/cdbi/13-constraint.t
t/cdbi/14-might_have.t
t/cdbi/15-accessor.t
t/cdbi/16-reserved.t
t/cdbi/18-has_a.t
t/cdbi/19-set_sql.t
t/cdbi/21-iterator.t
t/cdbi/22-deflate_order.t
t/cdbi/22-self_referential.t
t/cdbi/23-cascade.t
t/cdbi/24-meta_info.t
t/cdbi/26-mutator.t
t/cdbi/30-pager.t
t/cdbi/68-inflate_has_a.t
t/cdbi/70_implicit_inflate.t [new file with mode: 0644]
t/cdbi/71_column_object.t [new file with mode: 0644]
t/cdbi/98-failure.t
t/cdbi/DeepAbstractSearch/01_search.t
t/cdbi/abstract/search_where.t
t/cdbi/columns_as_hashes.t
t/cdbi/columns_dont_override_custom_accessors.t
t/cdbi/construct.t
t/cdbi/copy.t
t/cdbi/early_column_heisenbug.t
t/cdbi/has_many_loads_foreign_class.t
t/cdbi/hasa_without_loading.t
t/cdbi/max_min_value_of.t
t/cdbi/mk_group_accessors.t
t/cdbi/multi_column_set.t
t/cdbi/object_cache.t
t/cdbi/retrieve_from_sql_with_limit.t
t/cdbi/set_to_undef.t
t/cdbi/set_vs_DateTime.t
t/cdbi/sweet/08pager.t
t/cdbi/testlib/ColumnObject.pm [new file with mode: 0644]
t/cdbi/testlib/DBIC/Test/SQLite.pm
t/cdbi/testlib/ImplicitInflate.pm [new file with mode: 0644]
t/cdbi/testlib/Log.pm
t/cdbi/testlib/MyBase.pm
t/cdbi/testlib/MyFoo.pm
t/count/count_rs.t
t/count/distinct.t
t/count/in_subquery.t
t/count/joined.t
t/count/prefetch.t
t/delete/cascade_missing.t
t/icdt/core.t [moved from t/inflate/core.t with 95% similarity]
t/icdt/datetime_missing_deps.t [moved from t/inflate/datetime_missing_deps.t with 100% similarity]
t/icdt/engine_specific/firebird.t [moved from t/inflate/datetime_firebird.t with 76% similarity]
t/icdt/engine_specific/informix.t [moved from t/inflate/datetime_informix.t with 68% similarity]
t/icdt/engine_specific/msaccess.t [moved from t/inflate/datetime_msaccess.t with 60% similarity]
t/icdt/engine_specific/mssql.t [moved from t/inflate/datetime_mssql.t with 75% similarity]
t/icdt/engine_specific/oracle.t [new file with mode: 0644]
t/icdt/engine_specific/sqlanywhere.t [moved from t/inflate/datetime_sqlanywhere.t with 63% similarity]
t/icdt/engine_specific/sqlite.t [new file with mode: 0644]
t/icdt/engine_specific/sybase.t [moved from t/inflate/datetime_sybase.t with 81% similarity]
t/icdt/offline_mysql.t [moved from t/inflate/datetime_mysql.t with 93% similarity]
t/icdt/offline_pg.t [moved from t/inflate/datetime_pg.t with 60% similarity]
t/inflate/datetime.t [deleted file]
t/inflate/datetime_determine_parser.t [deleted file]
t/inflate/datetime_oracle.t [deleted file]
t/inflate/hri.t
t/inflate/serialize.t
t/lib/DBIC/DebugObj.pm [deleted file]
t/lib/DBIC/SqlMakerTest.pm [deleted file]
t/lib/DBICTest.pm
t/lib/DBICTest/AntiPattern/NullObject.pm [new file with mode: 0644]
t/lib/DBICTest/AntiPattern/TrueZeroLen.pm [new file with mode: 0644]
t/lib/DBICTest/Base.pm [new file with mode: 0644]
t/lib/DBICTest/BaseResult.pm
t/lib/DBICTest/BaseResultSet.pm
t/lib/DBICTest/BaseSchema.pm
t/lib/DBICTest/RunMode.pm
t/lib/DBICTest/SQLTracerObj.pm [new file with mode: 0644]
t/lib/DBICTest/Schema.pm
t/lib/DBICTest/Schema/Artist.pm
t/lib/DBICTest/Schema/Artwork.pm
t/lib/DBICTest/Schema/Artwork_to_Artist.pm
t/lib/DBICTest/Schema/CD.pm
t/lib/DBICTest/Schema/Track.pm
t/lib/DBICTest/Schema/TwoKeys.pm
t/lib/DBICTest/Schema/Year2000CDs.pm
t/lib/DBICTest/Stats.pm [deleted file]
t/lib/DBICTest/Util.pm
t/lib/DBICTest/Util/LeakTracer.pm
t/lib/PrefetchBug.pm [deleted file]
t/lib/sqlite.sql
t/multi_create/find_or_multicreate.t [new file with mode: 0644]
t/multi_create/multilev_single_PKeqFK.t
t/multi_create/standard.t
t/ordered/unordered_movement.t
t/prefetch/correlated.t
t/prefetch/count.t
t/prefetch/double_prefetch.t
t/prefetch/false_colvalues.t
t/prefetch/grouped.t
t/prefetch/incomplete.t
t/prefetch/join_type.t
t/prefetch/manual.t
t/prefetch/multiple_hasmany.t
t/prefetch/multiple_hasmany_torture.t
t/prefetch/o2m_o2m_order_by_with_limit.t
t/prefetch/one_to_many_to_one.t
t/prefetch/refined_search_on_relation.t
t/prefetch/standard.t
t/prefetch/via_search_related.t
t/prefetch/with_limit.t
t/relationship/core.t
t/relationship/custom.t
t/relationship/custom_opaque.t [new file with mode: 0644]
t/relationship/resolve_relationship_condition.t [new file with mode: 0644]
t/relationship/update_or_create_multi.t
t/resultset/as_query.t
t/resultset/as_subselect_rs.t
t/resultset/bind_attr.t
t/resultset/create_with_rs_inherited_values.t [moved from t/96_is_deteministic_value.t with 72% similarity]
t/resultset/find_on_subquery_cond.t [new file with mode: 0644]
t/resultset/inflate_result_api.t
t/resultset/plus_select.t
t/resultset/rowparser_internals.t
t/resultset/update_delete.t
t/resultsource/bare_resultclass_exception.t [new file with mode: 0644]
t/row/copy_with_extra_selection.t [new file with mode: 0644]
t/row/filter_column.t
t/row/find_one_has_many.t
t/row/set_extra_column.t [new file with mode: 0644]
t/row/sourceless.t [new file with mode: 0644]
t/search/distinct.t
t/search/empty_attrs.t [new file with mode: 0644]
t/search/preserve_original_rs.t
t/search/related_has_many.t
t/search/related_strip_prefetch.t
t/search/select_chains.t
t/search/select_chains_unbalanced.t
t/search/stack_cond.t [new file with mode: 0644]
t/search/subquery.t
t/sqlmaker/bind_transport.t
t/sqlmaker/core.t
t/sqlmaker/core_quoted.t
t/sqlmaker/dbihacks_internals.t [new file with mode: 0644]
t/sqlmaker/hierarchical/oracle.t
t/sqlmaker/legacy_joins.t [new file with mode: 0644]
t/sqlmaker/limit_dialects/custom.t
t/sqlmaker/limit_dialects/fetch_first.t
t/sqlmaker/limit_dialects/first_skip.t
t/sqlmaker/limit_dialects/generic_subq.t
t/sqlmaker/limit_dialects/mssql_torture.t
t/sqlmaker/limit_dialects/rno.t
t/sqlmaker/limit_dialects/rownum.t
t/sqlmaker/limit_dialects/skip_first.t
t/sqlmaker/limit_dialects/toplimit.t
t/sqlmaker/limit_dialects/torture.t
t/sqlmaker/msaccess.t
t/sqlmaker/mysql.t
t/sqlmaker/nest_deprec.t
t/sqlmaker/oracle.t
t/sqlmaker/oraclejoin.t
t/sqlmaker/order_by_bindtransport.t
t/sqlmaker/order_by_func.t
t/sqlmaker/quotes.t [new file with mode: 0644]
t/sqlmaker/quotes/quotes.t [deleted file]
t/sqlmaker/quotes/quotes_newstyle.t [deleted file]
t/sqlmaker/sqlite.t
t/storage/base.t
t/storage/cursor.t
t/storage/dbh_do.t
t/storage/dbic_pretty.t
t/storage/debug.t
t/storage/deploy.t
t/storage/disable_sth_caching.t
t/storage/error.t
t/storage/global_destruction.t
t/storage/nobindvars.t
t/storage/on_connect_do.t
t/storage/ping_count.t
t/storage/prefer_stringification.t [new file with mode: 0644]
t/storage/quote_names.t
t/storage/reconnect.t
t/storage/replicated.t
t/storage/savepoints.t [new file with mode: 0644]
t/storage/txn.t
t/storage/txn_scope_guard.t
t/zzzzzzz_authors.t [new file with mode: 0644]
t/zzzzzzz_perl_perf_bug.t
xt/dist/authors.t [new file with mode: 0644]
xt/dist/loadable_standalone_testschema_resultclasses.t [moved from xt/standalone_testschema_resultclasses.t with 95% similarity]
xt/dist/pod_coverage.t [moved from xt/podcoverage.t with 94% similarity]
xt/dist/postdistdir/pod_footers.t [new file with mode: 0644]
xt/dist/postdistdir/pod_validity.t [new file with mode: 0644]
xt/dist/postdistdir/whitespace.t [moved from xt/whitespace.t with 80% similarity]
xt/dist/strictures.t [new file with mode: 0644]
xt/extra/c3_mro.t [moved from t/04_c3_mro.t with 98% similarity]
xt/extra/dbicadmin.t [moved from t/admin/10script.t with 92% similarity]
xt/extra/diagnostics/deprecated_rs_attributes.t [moved from t/search/deprecated_attributes.t with 100% similarity]
xt/extra/diagnostics/malformed_rel_declaration.t [new file with mode: 0644]
xt/extra/diagnostics/many_to_many_warning.t [moved from t/103many_to_many_warning.t with 100% similarity]
xt/extra/diagnostics/resultset_manager.t [moved from t/40resultsetmanager.t with 100% similarity]
xt/extra/diagnostics/search_in_void_ctx.t [moved from t/search/void.t with 100% similarity]
xt/extra/diagnostics/unresolvable_relationship.t [moved from t/relationship/unresolvable.t with 100% similarity]
xt/extra/internals/dbictest_unlink_guard.t [moved from xt/dbictest_unlink_guard.t with 100% similarity]
xt/extra/internals/discard_changes_in_DESTROY.t [moved from t/discard_changes_in_DESTROY.t with 100% similarity]
xt/extra/internals/ensure_class_loaded.t [moved from t/90ensure_class_loaded.t with 100% similarity]
xt/extra/internals/merge_joinpref_attr.t [moved from t/91merge_joinpref_attr.t with 85% similarity]
xt/extra/internals/namespaces_cleaned.t [moved from t/55namespaces_cleaned.t with 89% similarity]
xt/extra/internals/optional_deps.t [new file with mode: 0644]
xt/extra/internals/quote_sub.t [new file with mode: 0644]
xt/extra/lean_startup.t [moved from t/53lean_startup.t with 88% similarity]
xt/extra/multicreate_opcount.t [moved from t/multi_create/reentrance_count.t with 100% similarity]
xt/extra/sqlite_deadlock.t [moved from t/zzzzzzz_sqlite_deadlock.t with 88% similarity]
xt/extra/sqlite_view_deps.t [moved from t/105view_deps.t with 81% similarity]
xt/optional_deps.t [deleted file]
xt/pod.t [deleted file]
xt/strictures.t [deleted file]

diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644 (file)
index 0000000..14bf8b9
--- /dev/null
@@ -0,0 +1,7 @@
+((nil . ((indent-tabs-mode . nil)))
+ (cperl-mode . ((cperl-indent-level . 2)
+                (cperl-continued-statement-offset . 2)
+                (cperl-continued-brace-offset . 0)
+                (cperl-close-paren-offset . -2)
+                (cperl-indent-subs-specially . nil)
+                (indent-tabs-mode . nil))))
index 587e076..ffbbe5d 100644 (file)
--- a/.mailmap
+++ b/.mailmap
@@ -5,29 +5,43 @@
 # https://www.kernel.org/pub/software/scm/git/docs/git-shortlog.html#_mapping_authors
 
 Alexander Hartmaier <abraxxa@cpan.org>      <alexander.hartmaier@t-systems.at>
+Alexander Kuznetsov <acca@cpan.org>         <acca(at)cpan.org>
 Amiri Barksdale <amiribarksdale@gmail.com>  <amiri@metalabel.com>
 Andrew Rodland <andrew@cleverdomain.org>    <arodland@cpan.org>
 Arthur Axel "fREW" Schmidt                  <frioux@gmail.com>
-Brendan Byrd <Perl@ResonatorSoft.org>       <byrd.b@insightcom.com>
-Brendan Byrd <Perl@ResonatorSoft.org>       <GitHub@ResonatorSoft.org>
-Brendan Byrd <Perl@ResonatorSoft.org>       <perl@resonatorsoft.org>
+Ash Berlin <ash@cpan.org>                   <ash_github@firemirror.com>
+Brendan Byrd <perl@resonatorsoft.org>       <byrd.b@insightcom.com>
+Brendan Byrd <perl@resonatorsoft.org>       <GitHub@ResonatorSoft.org>
+Brendan Byrd <perl@resonatorsoft.org>       <Perl@ResonatorSoft.org>
 Brian Phillips <bphillips@cpan.org>         <bphillips@digitalriver.com>
+Christian Walde <walde.christian@gmail.com> <walde.christian@googlemail.com>
+Jess Robinson <castaway@desert-island.me.uk><no-email-available@dev.catalystframework.org>
 Dagfinn Ilmari Mannsåker <ilmari@ilmari.org><ilmari.mannsaker@net-a-porter.com>
 David Kamholz <dkamholz@cpan.org>           <davekam@pobox.com>
 David Schmidt <mail@davidschmidt.at>        <d.schmidt@tripwolf.com>
 David Schmidt <mail@davidschmidt.at>        <dt@univie.ac.at>
 David Schmidt <mail@davidschmidt.at>        <davewood@gmx.at>
 Devin Austin <dhoss@cpan.org>               <devin.austin@gmail.com>
+Duncan Garland <Duncan.Garland@motortrak.com> <duncan@duncan-laptop.(none)>
 Felix Antonius Wilhelm Ostmann <sadrak@cpan.org>  <ostmann@sadraksaemp.intern4.websuche.de>
+Fitz Elliott <fitz.elliott@gmail.com>       <felliott@fiskur.org>
 Gerda Shank <gshank@cpan.org>               <gerda.shank@gmail.com>
 Gianni Ceccarelli <dakkar@thenautilus.net>  <gianni.ceccarelli@net-a-porter.com>
 Gordon Irving <goraxe@cpan.org>             <goraxe@goraxe.me.uk>
 Hakim Cassimally <osfameron@cpan.org>       <hakim@vm-participo.(none)>
+Henry Van Styn <vanstyn@cpan.org>           <vanstyn@intellitree.com>
+Jason M. Mills <jmmills@cpan.org>           <jmmills@cpan.org>
 Jonathan Chu <milki@rescomp.berkeley.edu>   <milki@rescomp.berkeley.edu>
+Jose Luis Martinez <jlmartinez@capside.com> <jlmartinez@capside.com>
+Kent Fredric <kentnl@cpan.org>              <kentfredric@gmail.com>
 Matt Phillips <mattp@cpan.org>              <mphillips@oanda.com>
 Norbert Csongrádi <bert@cpan.org>           <bert@cpan.org>
-Roman Filippov <romanf@cpan.org>            <moltar@moltar.net>
 Peter Rabbitson <ribasushi@cpan.org>        <rabbit@viator.rabbit.us>
-Tim Bunce <Tim.Bunce@pobox.com>             <Tim.Bunce@ig.co.uk>
+Roman Filippov <romanf@cpan.org>            <moltar@moltar.net>
+Ronald J Kimball <rjk@tamias.net>           <rkimball@pangeamedia.com>
+Samuel Kaufman <sam@socialflow.com>         <sam@socialflow.com>
+Tim Bunce <tim.bunce@pobox.com>             <Tim.Bunce@ig.co.uk>
 Toby Corkindale <tjc@cpan.org>              <toby@dryft.net>
+Tommy Butler <tbutler.cpan.org@internetalias.net> <tommybutler@users.noreply.github.com>
+Ton Voon <ton.voon@opsview.com>             <ton.voon@opsera.com>
 Wallace Reis <wreis@cpan.org>               <wallace@reis.org.br>
index ed2c04e..4b3fb43 100644 (file)
@@ -6,35 +6,14 @@
 #
 # * Minimum perl officially supported by DBIC is 5.8.3. This *includes* the
 # basic depchain. On failure either attempt to fix it or bring it to the
-# attention of ribasushi. *DO NOT* disable 5.8 testing - it is here for a
-# reason
-#
-# * The matrix is built from two main modes - CLEANTEST = [true|false].
-# - In the first case we test with minimal deps available, and skip everything
-#   listed in DBIC::OptDesps. The modules are installed with classic CPAN
-#   invocations and are *fully tested*. In other words we simulate what would
-#   happen if a user tried to install on a just-compiled virgin perl
-# - Without CLEANTEST we bring the armada of RDBMS and install the maximum
-#   possible set of deps *without testing them*. This ensures we stay within
-#   a reasonable build-time and still run as many of our tests as possible
-#
-# * The perl builds and the DBIC tests run under NUMTHREADS number of threads.
-# The testing of dependencies under CLEANTEST runs single-threaded, at least
-# until we fix our entire dep-chain to safely pass under -j
-#
-# * The way .travis.yml is fed to the command controller is idiotic - it
-# makes using multiline `bash -c` statements impossible. Therefore to
-# aid readability (our travis logic is rather complex), the bulk of
-# functionality is moved to scripts. More about the problem (and the
-# WONTFIX "explanation") here: https://github.com/travis-ci/travis-ci/issues/497
+# attention of the maintainer. *DO NOT* disable 5.8 testing - it is here for
+# a very good reason
 #
+# the entire run times out after 50 minutes, or after 5 minutes without
+# console output
 
 #
 # Smoke all branches except for blocked* and wip/*
-#
-# Additionally master does not smoke with bleadperl
-# ( implemented in maint/travis-ci_scripts/10_before_install.bash )
-#
 branches:
   except:
     - /^wip\//
@@ -53,211 +32,364 @@ notifications:
   email:
     recipients:
       - ribasushi@cpan.org
-      # Temporary - if it proves to be too noisy, we'll shut it off
-      #- dbix-class-devel@lists.scsys.co.uk
     on_success: change
     on_failure: always
 
-# FIXME - This stuff is not yet available for free OSS accounts, sadpanda
-# First paragrah on http://about.travis-ci.org/docs/user/caching/
-#cache:
-#  apt: true
-#  directories:
-#    - /var/cache/apt/archives
+addons:
+  apt:
+    packages:
+      - libapp-nopaste-perl
+      - net-tools
 
+# This is probably a net-loss for setup etc - a bare 'C' will likely fare much better
 language: perl
 
-perl:
-  - "5.18"
+# Currently not trying osx: https://github.com/travis-ci/travis-ci/issues/2314
+os: linux
 
-env:
-  - CLEANTEST=false
-  - CLEANTEST=true
+# The defaults run under the more rapid container infra. The hardware is
+# actually *much* slower, but the jobs start much faster, for more info see
+# https://docs.travis-ci.com/user/ci-environment/#Virtualization-environments
+# Combined with 'fast_finish' this will result in an "uh-oh" email as early
+# as possible
+dist: precise
+sudo: false
+env: CLEANTEST=true
+
+perl:
+  - "5.8"
+  - "5.10"
+  - "5.22-extras"
 
 matrix:
+  fast_finish: true
+
   include:
-    # this particular perl is quite widespread
-    - perl: 5.8.8_thr_mb
+
+    # Same as the "master matrix" above, frozen under older dist/infrastructure
+    # In genereal it is strongly recommended to keep things on the older
+    # version indefinitely - there is little value in-depth smoking on
+    # more recent software stacks
+    - perl: "5.8"
+      sudo: required
+      dist: precise
       env:
-        - CLEANTEST=true
-        - BREWOPTS="-Duseithreads -Dusemorebits"
-        - BREWVER=5.8.8
+        - CLEANTEST=false
 
-    # so is this one (test a sane CPAN.pm)
-    - perl: 5.12.4_thr_mb
+    - perl: "5.10"
+      sudo: required
+      dist: precise
       env:
-        - CLEANTEST=true
-        - BREWOPTS="-Duseithreads -Dusemorebits"
-        - BREWVER=5.12.4
+        - CLEANTEST=false
 
-    # this is the perl suse ships
-    - perl: 5.10.0_thr_dbg
+    - perl: "5.22-extras"
+      sudo: required
+      dist: precise
       env:
-        - CLEANTEST=true
-        - BREWOPTS="-DDEBUGGING -Duseithreads"
-        - BREWVER=5.10.0
+        - CLEANTEST=false
 
-    # CLEANTEST of minimum supported
-    - perl: 5.8.3_nt_mb
+    # CLEANTEST of minimum supported with non-tracing poisoning, single thread (hence the sudo)
+    - perl: "5.8.3_nt_mb"
+      sudo: required
+      dist: precise
       env:
+        - VCPU_USE=1
         - CLEANTEST=true
+        - POISON_ENV=true
+        - DBIC_TRACE_PROFILE=console_monochrome
+        - BREWVER=5.8.3
         - BREWOPTS="-Dusemorebits"
+
+    # Full Test of minimum supported without threads with plain poisoned trace
+    - perl: "5.8.3_nt"
+      sudo: required
+      dist: precise
+      # run these under legacy - great simulation of low memory env
+      group: legacy
+      env:
+        - CLEANTEST=false
+        - POISON_ENV=true
+        - DBIC_TRACE=1
         - BREWVER=5.8.3
 
-    # Full Test of minimum supported with threads
-    - perl: 5.8.5_thr
+    # Full Test of minimum supported with threads with non-tracing poisoning
+    - perl: "5.8.5_thr"
+      sudo: required
+      dist: precise
+      # run these under legacy - great simulation of low memory env
+      group: legacy
       env:
         - CLEANTEST=false
-        - BREWOPTS="-Duseithreads"
+        - POISON_ENV=true
+        - DBIC_TRACE_PROFILE=console
         - BREWVER=5.8.5
+        - BREWOPTS="-Duseithreads"
+
+    # CLEANTEST of solaris-like perl with non-tracing poisoning
+    - perl: "5.8.4_nt"
+      sudo: false
+      dist: precise
+      env:
+        - CLEANTEST=true
+        - POISON_ENV=true
         - DBIC_TRACE_PROFILE=console
+        - BREWVER=5.8.4
 
-    # Full Test of minimum supported without threads
-    - perl: 5.8.3_nt
+    # Full test: this particular perl is quite widespread, single thread
+    - perl: "5.8.8_thr"
+      sudo: required
+      dist: precise
       env:
+        - VCPU_USE=1
         - CLEANTEST=false
-        - BREWOPTS=""
-        - BREWVER=5.8.3
-        - DBIC_TRACE_PROFILE=console_monochrome
-
-    ###
-    # some permutations of tracing and envvar poisoning
+        - BREWVER=5.8.8
+        - BREWOPTS="-Duseithreads"
 
-    - perl: 5.16.2_thr_mb
+    # CLEANTEST: this is the perl suse ships, with env poisoning
+    - perl: "5.10.0_thr_dbg"
+      sudo: false
+      dist: precise
       env:
-        - CLEANTEST=false
+        - CLEANTEST=true
         - POISON_ENV=true
-        - DBIC_TRACE=1
-        - DBIC_MULTICREATE_DEBUG=0
+        - BREWVER=5.10.0
+        - BREWOPTS="-DDEBUGGING -Duseithreads"
+
+    # CLEANTEST: this one is in a number of debian-based LTS (test a sane CPAN.pm, single thread)
+    - perl: "5.14.2_thr_mb"
+      sudo: required
+      dist: precise
+      env:
+        - VCPU_USE=1
+        - CLEANTEST=true
+        - BREWVER=5.14.2
         - BREWOPTS="-Duseithreads -Dusemorebits"
-        - BREWVER=5.16.2
 
-    - perl: 5.18
+    ###
+    # some permutations of tracing and envvar poisoning
+
+    - perl: "5.12.3_thr"
+      sudo: false
+      dist: precise
       env:
-        - CLEANTEST=false
+        - CLEANTEST=true
         - POISON_ENV=true
+        - DBIC_TRACE=1
+        - DBIC_MULTICREATE_DEBUG=1
+        - DBIC_STORAGE_RETRY_DEBUG=1
         - DBIC_TRACE_PROFILE=console
+        - BREWVER=5.12.3
+        - BREWOPTS="-Duseithreads"
 
-    - perl: 5.8
+    - perl: "5.16.3_thr_mb"
+      sudo: required
+      dist: precise
       env:
-        - CLEANTEST=true
+        - CLEANTEST=false
         - POISON_ENV=true
         - DBIC_TRACE=1
-        - DBIC_TRACE_PROFILE=console
+        - BREWVER=5.16.3
+        - BREWOPTS="-Duseithreads -Dusemorebits"
 
-    - perl: 5.18
+    - perl: "5.18-extras"
+      sudo: required
+      # explicit new infra spec preparing for a future forced upgrade
+      dist: trusty
       env:
         - CLEANTEST=false
         - POISON_ENV=true
         - DBIC_TRACE=1
         - DBIC_TRACE_PROFILE=console_monochrome
-        - DBIC_MULTICREATE_DEBUG=0
+        - DBICTEST_VIA_REPLICATED=0
+        - DBICTEST_VERSION_WARNS_INDISCRIMINATELY=1
 
     ###
     # Start of the allow_failures block
 
-    # old threaded with blead CPAN
-    - perl: devcpan_5.8.7_thr
+    # threaded oldest possible with blead CPAN
+    - perl: "devcpan_5.8.1_thr_mb"
+      sudo: false
+      dist: precise
       env:
         - CLEANTEST=true
-        - BREWOPTS="-Duseithreads"
-        - BREWVER=5.8.7
         - DEVREL_DEPS=true
+        - BREWVER=5.8.1
+        - BREWOPTS="-Duseithreads -Dusemorebits"
 
-    # 5.10.0 threaded with blead CPAN
-    - perl: devcpan_5.10.0_thr_mb
+    # oldest possible with blead CPAN with poisoning and plain trace
+    - perl: "devcpan_5.8.1"
+      sudo: false
+      dist: precise
       env:
         - CLEANTEST=true
-        - BREWOPTS="-Duseithreads -Dusemorebits"
-        - BREWVER=5.10.0
         - DEVREL_DEPS=true
+        - POISON_ENV=true
+        - DBIC_TRACE=1
+        - DBICTEST_VERSION_WARNS_INDISCRIMINATELY=1
+        - BREWVER=5.8.1
+
+    # 5.8.3 with blead CPAN
+    - perl: "devcpan_5.8.3_mb"
+      sudo: required
+      # explicit new infra spec preparing for a future forced upgrade
+      dist: trusty
+      env:
+        - CLEANTEST=false
+        - DEVREL_DEPS=true
+        - BREWVER=5.8.3
+        - BREWOPTS="-Dusemorebits"
 
-    # 5.12.2 with blead CPAN
-    - perl: devcpan_5.12.2_thr
+    # 5.8.7 threaded with blead CPAN with non-tracing poisoning
+    - perl: "devcpan_5.8.7_thr"
+      sudo: false
+      dist: precise
       env:
         - CLEANTEST=true
+        - DEVREL_DEPS=true
+        - POISON_ENV=true
+        - BREWVER=5.8.7
         - BREWOPTS="-Duseithreads"
-        - BREWVER=5.12.2
+
+    # 5.8.8 threaded MB (exercises P5#72210)
+    - perl: "devcpan_5.8.8_thr_mb"
+      sudo: false
+      dist: precise
+      env:
+        - CLEANTEST=true
+        - DBICTEST_VERSION_WARNS_INDISCRIMINATELY=1
         - DEVREL_DEPS=true
+        - BREWVER=5.8.8
+        - BREWOPTS="-Duseithreads -Dusemorebits"
 
-    # recentish threaded stable with blead CPAN
-    - perl: devcpan_5.18.2_thr_mb
+    # 5.10.0 threaded with blead CPAN
+    - perl: "devcpan_5.10.0_thr_mb"
+      sudo: false
+      dist: precise
       env:
-        - CLEANTEST=false
+        - CLEANTEST=true
+        - DEVREL_DEPS=true
+        - BREWVER=5.10.0
         - BREWOPTS="-Duseithreads -Dusemorebits"
-        - BREWVER=5.18.2
+
+    # 5.12.1 with blead CPAN
+    - perl: "devcpan_5.12.1_thr"
+      sudo: false
+      dist: precise
+      env:
+        - CLEANTEST=true
         - DEVREL_DEPS=true
+        - BREWVER=5.12.1
+        - BREWOPTS="-Duseithreads"
 
-    # bleadperl with stock CPAN, full depchain test
-    - perl: bleadperl
+    # bleadperl with stock CPAN, full depchain test with non-tracing poisoning, single thread
+    - perl: "bleadperl"
+      sudo: required
+      dist: precise
       env:
+        - VCPU_USE=1
         - CLEANTEST=true
+        - POISON_ENV=true
         - BREWVER=blead
 
-    # bleadperl with blead CPAN
-    - perl: devcpan_bleadperl_thr_mb
+    # bleadperl with blead CPAN, single thread
+    - perl: "devcpan_bleadperl_thr_mb"
+      sudo: required
+      # explicitly do not specify dist - see what the default does
       env:
+        - VCPU_USE=1
         - CLEANTEST=false
-        - BREWOPTS="-Duseithreads -Dusemorebits"
-        - BREWVER=blead
         - DEVREL_DEPS=true
+        - BREWVER=blead
+        - BREWOPTS="-Duseithreads -Dusemorebits"
 
+    # CLEANTEST of http://schplog.schmorp.de/2015-06-06-a-stable-perl.html with non-tracing poisoning
+    - perl: "schmorp_stableperl_thr_mb"
+      sudo: false
+      dist: precise
+      env:
+        - CLEANTEST=true
+        - POISON_ENV=true
+        - BREWVER=schmorp_stableperl
+        - BREWOPTS="-Duseithreads -Dusemorebits"
 
   # which ones of the above can fail
   allow_failures:
 
     # these run with various dev snapshots - allowed to fail
+    - perl: devcpan_5.8.1_thr_mb
+    - perl: devcpan_5.8.1
+    - perl: devcpan_5.8.3_mb
     - perl: devcpan_5.8.7_thr
+    - perl: devcpan_5.8.8_thr_mb
     - perl: devcpan_5.10.0_thr_mb
-    - perl: devcpan_5.12.2_thr
-    - perl: devcpan_5.18.2_thr_mb
+    - perl: devcpan_5.12.1_thr
     - perl: bleadperl
     - perl: devcpan_bleadperl_thr_mb
+    - perl: schmorp_stableperl_thr_mb
 
 
-# sourcing the files is *EXTREMELY* important - otherwise
-# no envvars will survive
-
-# the entire run times out after 50 minutes, or after 5 minutes without
-# console output
+###
+### For the following two phases -e is *set*
+###
 
 before_install:
+  # common functions for all run phases below
+  #
+  # this is an exporter - sourcing it is crucial
+  # among other things it also sets -e
+  #
+  - source maint/travis-ci_scripts/common.bash
+
   # Sets global envvars, downloads/configures debs based on CLEANTEST
   # Sets extra DBICTEST_* envvars
   #
+  # this is an exporter - sourcing it is crucial
+  #
   - source maint/travis-ci_scripts/10_before_install.bash
 
 install:
   # Build and switch to a custom perl if requested
   # Configure the perl env, preinstall some generic toolchain parts
+  # Possibly poison the environment
+  #
+  # this is an exporter - sourcing it is crucial
   #
   - source maint/travis-ci_scripts/20_install.bash
 
+###
+### From this point on -e is *unset*, rely on travis' error handling
+###
+  - set +e
+
 before_script:
   # Preinstall/install deps based on envvars/CLEANTEST
   #
-  - source maint/travis-ci_scripts/30_before_script.bash
+  # need to invoke the after_failure script manually
+  # because 'after_failure' runs only after 'script' fails
+  #
+  - maint/getstatus maint/travis-ci_scripts/30_before_script.bash
 
 script:
   # Run actual tests
   #
-  - source maint/travis-ci_scripts/40_script.bash
+  - maint/getstatus maint/travis-ci_scripts/40_script.bash
+
+###
+### Set -e back, work around https://github.com/travis-ci/travis-ci/issues/3533
+###
+  - set -e
 
 after_success:
   # Check if we can assemble a dist properly if not in CLEANTEST
   #
-  - source maint/travis-ci_scripts/50_after_success.bash
+  - maint/getstatus maint/travis-ci_scripts/50_after_success.bash
 
 after_failure:
-  # No tasks yet
+  # Final sysinfo printout on fail
   #
-  #- source maint/travis-ci_scripts/50_after_failure.bash
+  - maint/getstatus maint/travis-ci_scripts/50_after_failure.bash
 
 after_script:
   # No tasks yet
   #
-  #- source maint/travis-ci_scripts/60_after_script.bash
-
-  # if we do not unset this before we terminate the travis teardown will
-  # mark the entire job as failed
-  - set +e
+  #- maint/getstatus maint/travis-ci_scripts/60_after_script.bash
diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..086b0e0
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,227 @@
+#
+#     The list of the awesome folks behind DBIx::Class
+#
+# This utf8-encoded file lists every code author and idea contributor
+# in alphabetical order
+#
+# Entry format (all elements optional, order is mandatory):
+#   (ircnick:) (name) (<email>)
+#
+#
+#    *** EVEN THOUGH FIELDS ARE OPTIONAL, COMMITTERS ARE QUITE ***
+#   *** STRONGLY URGED TO KEEP THIS LIST AS COMPLETE AS POSSIBLE ***
+#
+#             *** IN OTHER WORDS - DO NOT BE LAZY ***
+#
+
+abraxxa: Alexander Hartmaier <abraxxa@cpan.org>
+acca: Alexander Kuznetsov <acca@cpan.org>
+aherzog: Adam Herzog <adam@herzogdesigns.com>
+Alexander Keusch <cpan@keusch.at>
+alexrj: Alessandro Ranellucci <aar@cpan.org>
+alnewkirk: Al Newkirk <github@alnewkirk.com>
+Altreus: Alastair McGowan-Douglas <alastair.mcgowan@opusvl.com>
+amiri: Amiri Barksdale <amiribarksdale@gmail.com>
+amoore: Andrew Moore <amoore@cpan.org>
+Andrew Mehta <Andrew@unitedgames.co.uk>
+andrewalker: Andre Walker <andre@andrewalker.net>
+andybev: Andrew Beverley <a.beverley@ctrlo.com>
+andyg: Andy Grundman <andy@hybridized.org>
+ank: Andres Kievsky <ank@ank.com.ar>
+arc: Aaron Crane <arc@cpan.org>
+arcanez: Justin Hunter <justin.d.hunter@gmail.com>
+ash: Ash Berlin <ash@cpan.org>
+bert: Norbert Csongrádi <bert@cpan.org>
+bfwg: Colin Newell <colin.newell@gmail.com>
+blblack: Brandon L. Black <blblack@gmail.com>
+bluefeet: Aran Deltac <bluefeet@cpan.org>
+boghead: Bryan Beeley <cpan@beeley.org>
+bphillips: Brian Phillips <bphillips@cpan.org>
+brd: Brad Davis <brd@FreeBSD.org>
+Brian Kirkbride <brian.kirkbride@deeperbydesign.com>
+bricas: Brian Cassidy <bricas@cpan.org>
+brunov: Bruno Vecchi <vecchi.b@gmail.com>
+caelum: Rafael Kitover <rkitover@cpan.org>
+caldrin: Maik Hentsche <maik.hentsche@amd.com>
+castaway: Jess Robinson <castaway@desert-island.me.uk>
+chorny: Alexandr Ciornii <alexchorny@gmail.com>
+cj: C.J. Adams-Collier <cjcollier@cpan.org>
+claco: Christopher H. Laco <claco@cpan.org>
+clkao: CL Kao <clkao@clkao.org>
+Ctrl-O http://ctrlo.com/
+da5id: David Jack Olrik <david@olrik.dk>
+dams: Damien Krotkine <dams@cpan.org>
+dandv: Dan Dascalescu <ddascalescu+github@gmail.com>
+dariusj: Darius Jokilehto <dariusjokilehto@yahoo.co.uk>
+davewood: David Schmidt <mail@davidschmidt.at>
+daxim: Lars Dɪᴇᴄᴋᴏᴡ 迪拉斯 <daxim@cpan.org>
+dduncan: Darren Duncan <darren@darrenduncan.net>
+debolaz: Anders Nor Berle <berle@cpan.org>
+dew: Dan Thomas <dan@godders.org>
+dim0xff: Dmitry Latin <dim0xff@gmail.com>
+dkubb: Dan Kubb <dan.kubb-cpan@onautopilot.com>
+dnm: Justin Wheeler <jwheeler@datademons.com>
+dpetrov: Dimitar Petrov <mitakaa@gmail.com>
+Dr^ZigMan: Robert Stone <drzigman@drzigman.com>
+dsteinbrunner: David Steinbrunner <dsteinbrunner@pobox.com>
+duncan_dmg: Duncan Garland <Duncan.Garland@motortrak.com>
+dwc: Daniel Westermann-Clark <danieltwc@cpan.org>
+dyfrgi: Michael Leuchtenburg <michael@slashhome.org>
+edenc: Eden Cardim <edencardim@gmail.com>
+Eligo http://eligo.co.uk/
+ether: Karen Etheridge <ether@cpan.org>
+evdb: Edmund von der Burg <evdb@ecclestoad.co.uk>
+faxm0dem: Fabien Wernli <cpan@faxm0dem.org>
+felliott: Fitz Elliott <fitz.elliott@gmail.com>
+freetime: Bill Moseley <moseley@hank.org>
+frew: Arthur Axel "fREW" Schmidt <frioux@gmail.com>
+gbjk: Gareth Kirwan <gbjk@thermeon.com>
+geotheve: Georgina Thevenet <geotheve@gmail.com>
+Getty: Torsten Raudssus <torsten@raudss.us>
+goraxe: Gordon Irving <goraxe@cpan.org>
+gphat: Cory G Watson <gphat@cpan.org>
+Grant Street Group http://www.grantstreet.com/
+gregoa: Gregor Herrmann <gregoa@debian.org>
+groditi: Guillermo Roditi <groditi@cpan.org>
+gshank: Gerda Shank <gshank@cpan.org>
+guacamole: Fred Steinberg <fred.steinberg@gmail.com>
+Haarg: Graham Knop <haarg@haarg.org>
+hobbs: Andrew Rodland <andrew@cleverdomain.org>
+Ian Wells <ijw@cack.org.uk>
+idn: Ian Norton <i.norton@shadowcat.co.uk>
+ilmari: Dagfinn Ilmari Mannsåker <ilmari@ilmari.org>
+ingy: Ingy döt Net <ingy@ingy.net>
+initself: Mike Baas <mike@initselftech.com>
+ironcamel: Naveed Massjouni <naveedm9@gmail.com>
+jasonmay: Jason May <jason.a.may@gmail.com>
+jawnsy: Jonathan Yu <jawnsy@cpan.org>
+jegade: Jens Gassmann <jens.gassmann@atomix.de>
+jeneric: Eric A. Miller <emiller@cpan.org>
+jesper: Jesper Krogh <jesper@krogh.cc>
+Jesse Sheidlower <jester@panix.com>
+jgoulah: John Goulah <jgoulah@cpan.org>
+jguenther: Justin Guenther <jguenther@cpan.org>
+jhannah: Jay Hannah <jay@jays.net>
+jmac: Jason McIntosh <jmac@appleseed-sc.com>
+jmmills: Jason M. Mills <jmmills@cpan.org>
+jnapiorkowski: John Napiorkowski <jjn1056@yahoo.com>
+Joe Carlson <jwcarlson@lbl.gov>
+jon: Jon Schutz <jjschutz@cpan.org>
+Jordan Metzmeier <jmetzmeier@magazines.com>
+jshirley: J. Shirley <jshirley@gmail.com>
+kaare: Kaare Rasmussen
+kd: Kieren Diment <diment@gmail.com>
+kentnl: Kent Fredric <kentnl@cpan.org>
+kkane: Kevin L. Kane <kevin.kane@gmail.com>
+konobi: Scott McWhirter <konobi@cpan.org>
+lamoz: Konstantin A. Pustovalov <konstantin.pustovalov@gmail.com>
+Lasse Makholm <lasse@unity3d.com>
+lejeunerenard: Sean Zellmer <sean@lejeunerenard.com>
+littlesavage: Alexey Illarionov <littlesavage@orionet.ru>
+lukes: Luke Saunders <luke.saunders@gmail.com>
+marcus: Marcus Ramberg <mramberg@cpan.org>
+mateu: Mateu X. Hunter <hunter@missoula.org>
+Matt LeBlanc <antirice@gmail.com>
+Matt Sickler <imMute@msk4.com>
+mattlaw: Matt Lawrence
+mattp: Matt Phillips <mattp@cpan.org>
+mdk: Mark Keating <m.keating@shadowcat.co.uk>
+melo: Pedro Melo <melo@simplicidade.org>
+metaperl: Terrence Brannon <metaperl@gmail.com>
+michaelr: Michael Reddick <michael.reddick@gmail.com>
+milki: Jonathan Chu <milki@rescomp.berkeley.edu>
+minty: Murray Walker <perl@minty.org>
+mithaldu: Christian Walde <walde.christian@gmail.com>
+mjemmeson: Michael Jemmeson <michael.jemmeson@gmail.com>
+mna: Maya
+mo: Moritz Onken <onken@netcubed.de>
+moltar: Roman Filippov <romanf@cpan.org>
+moritz: Moritz Lenz <moritz@faui2k3.org>
+mrf: Mike Francis <ungrim97@gmail.com>
+mst: Matt S. Trout <mst@shadowcat.co.uk>
+mstratman: Mark A. Stratman <stratman@gmail.com>
+ned: Neil de Carteret <n3dst4@gmail.com>
+nigel: Nigel Metheringham <nigelm@cpan.org>
+ningu: David Kamholz <dkamholz@cpan.org>
+Nniuq: Ron "Quinn" Straight" <quinnfazigu@gmail.org>
+norbi: Norbert Buchmuller <norbi@nix.hu>
+nothingmuch: Yuval Kogman <nothingmuch@woobling.org>
+nuba: Nuba Princigalli <nuba@cpan.org>
+Numa: Dan Sully <daniel@cpan.org>
+oalders: Olaf Alders <olaf@wundersolutions.com>
+Olly Betts <olly@survex.com>
+osfameron: Hakim Cassimally <osfameron@cpan.org>
+ovid: Curtis "Ovid" Poe <ovid@cpan.org>
+oyse: Øystein Torget <oystein.torget@dnv.com>
+paulm: Paul Makepeace <paulm+pause@paulm.com>
+penguin: K J Cheetham <jamie@shadowcatsystems.co.uk>
+perigrin: Chris Prather <chris@prather.org>
+Peter Siklósi <einon@einon.hu>
+Peter Valdemar Mørch <peter@morch.com>
+peter: Peter Collingbourne <peter@pcc.me.uk>
+phaylon: Robert Sedlacek <phaylon@dunkelheit.at>
+plu: Johannes Plunien <plu@cpan.org>
+Possum: Daniel LeWarne <possum@cpan.org>
+pplu: Jose Luis Martinez <jlmartinez@capside.com>
+quicksilver: Jules Bean <jules@jellybean.co.uk>
+racke: Stefan Hornburg <racke@linuxia.de>
+rafl: Florian Ragwitz <rafl@debian.org>
+rainboxx: Matthias Dietrich <perl@rb.ly>
+rbo: Robert Bohne <rbo@cpan.org>
+rbuels: Robert Buels <rmb32@cornell.edu>
+rdj: Ryan D Johnson <ryan@innerfence.com>
+Relequestual: Ben Hutton <relequestual@gmail.com>
+renormalist: Steffen Schwigon <schwigon@cpan.org>
+ribasushi: Peter Rabbitson <ribasushi@cpan.org>
+rjbs: Ricardo Signes <rjbs@cpan.org>
+Robert Krimen <rkrimen@cpan.org>
+Robert Olson <bob@rdolson.org>
+robkinyon: Rob Kinyon <rkinyon@cpan.org>
+Roman Ardern-Corris <spam_in@3legs.com>
+ruoso: Daniel Ruoso <daniel@ruoso.com>
+Sadrak: Felix Antonius Wilhelm Ostmann <sadrak@cpan.org>
+sc_: Just Another Perl Hacker
+schwern: Michael G Schwern <mschwern@cpan.org>
+Scott R. Godin <webdragon.net@gmail.com>
+scotty: Scotty Allen <scotty@scottyallen.com>
+semifor: Marc Mims <marc@questright.com>
+Simon Elliott <cpan@browsing.co.uk>
+SineSwiper: Brendan Byrd <perl@resonatorsoft.org>
+skaufman: Samuel Kaufman <sam@socialflow.com>
+solomon: Jared Johnson <jaredj@nmgi.com>
+spb: Stephen Bennett <stephen@freenode.net>
+Squeeks <squeek@cpan.org>
+srezic: Slaven Rezic <slaven@rezic.de>
+sszabo: Stephan Szabo <sszabo@bigpanda.com>
+Stephen Peters <steve@stephenpeters.me>
+stonecolddevin: Devin Austin <dhoss@cpan.org>
+talexb: Alex Beamish <talexb@gmail.com>
+tamias: Ronald J Kimball <rjk@tamias.net>
+TBSliver: Tom Bloor <t.bloor@shadowcat.co.uk>
+teejay: Aaron Trevena <teejay@cpan.org>
+theorbtwo: James Mastros <james@mastros.biz>
+Thomas Kratz <tomk@cpan.org>
+timbunce: Tim Bunce <tim.bunce@pobox.com>
+tinita: Tina Mueller <cpan2@tinita.de>
+Todd Lipcon
+Tom Hukins <tom@eborcom.com>
+tommy: Tommy Butler <tbutler.cpan.org@internetalias.net>
+tonvoon: Ton Voon <ton.voon@opsview.com>
+triode: Pete Gamache <gamache@cpan.org>
+typester: Daisuke Murase <typester@cpan.org>
+uree: Oriol Soriano <oriol.soriano@capside.com>
+uwe: Uwe Voelker <uwe@uwevoelker.de>
+Vadim Pushtaev <pushtaev.vm@gmail.com>
+vanstyn: Henry Van Styn <vanstyn@cpan.org>
+victori: Victor Igumnov <victori@cpan.org>
+wdh: Will Hawes <wdhawes@gmail.com>
+wesm: Wes Malone <wes@mitsi.com>
+willert: Sebastian Willert <willert@cpan.org>
+wintermute: Toby Corkindale <tjc@cpan.org>
+wreis: Wallace Reis <wreis@cpan.org>
+xenoterracide: Caleb Cushing <xenoterracide@gmail.com>
+xmikew: Mike Wisener <xmikew@32ths.com>
+yrlnry: Mark Jason Dominus <mjd@plover.com>
+zamolxes: Bogdan Lucaciu <bogdan@wiz.ro>
+Zefram: Andrew Main <zefram@fysh.org>
+Zoffix: Zoffix Znet <cpan@zoffix.com>
diff --git a/Changes b/Changes
index 71cd4cb..9546752 100644 (file)
--- a/Changes
+++ b/Changes
 Revision history for DBIx::Class
 
+    * Notable Changes and Deprecations
+        - Neither exception_action() nor $SIG{__DIE__} handlers are invoked
+          on recoverable errors. This ensures that the retry logic is fully
+          insulated from changes in control flow, as the handlers are only
+          invoked when an error is leaving the DBIC internals to be handled by
+          the caller (n.b. https://github.com/PerlDancer/Dancer2/issues/1125)
+        - $result->related_resultset() no longer passes extra arguments to
+          an underlying search_rs(), as by design these arguments would be
+          used only on the first call to ->related_resultset(), and ignored
+          afterwards. Instead an exception (detailing the fix) is thrown.
+        - Calling the set_* many-to-many helper with a list (instead of an
+          arrayref) now emits a deprecation warning
+
+    * New Features
+        - When using non-scalars (e.g. arrays) as literal bind values it is no
+          longer necessary to explicitly specify a bindtype (this turned out
+          to be a mostly useless overprotection)
+        - DBIx::Class::Optional::Dependencies now properly understands
+          combinations of requirements and does the right thing with e.g.
+          ->req_list_for([qw( rdbms_oracle ic_dt )]) bringing in the Oracle
+          specific DateTime::Format dependencies
+
+    * Fixes
+        - Ensure failing on_connect* / on_disconnect* are dealt with properly,
+          notably on_connect* failures now properly abort the entire connect
+        - Fix incorrect SQL generated with invalid {rows} on complex resultset
+          operations, generally more robust handling of rows/offset attrs
+        - Fix incorrect $storage state on unexpected RDBMS disconnects and
+          other failure events, preventing clean reconnection (RT#110429)
+        - Ensure leaving an exception stack via Return::MultiLevel or something
+          similar produces a large warning
+        - Make sure exception objects stringifying to '' are properly handled
+          and warned about (GH#15)
+        - Fix corner case of stringify-only overloaded objects being used in
+          create()/populate()
+        - Fix spurious ROLLBACK statements when a TxnScopeGuard fails a commit
+          of a transaction with deferred FK checks: a guard is now inactivated
+          immediately before the commit is attempted (RT#107159)
+        - Work around unreliable $sth->finish() on INSERT ... RETURNING within
+          DBD::Firebird on some compiler/driver combinations (RT#110979)
+        - Fix several corner cases with Many2Many over custom relationships
+        - Fix the Sybase ASE storage incorrectly attempting to retrieve an
+          autoinc value when inserting rows containing blobs (GH#82)
+
+    * Misc
+        - Fix invalid variable names in ResultSource::View examples
+        - Typo fixes from downstream debian packagers (RT#112007)
+        - Skip tests in a way more intelligent and speedy manner when optional
+          dependencies are missing
+        - Make the Optional::Dependencies error messages cpanm-friendly
+        - Incompatibly change values (not keys) of the hash returned by
+          Optional::Dependencies::req_group_list (no known users in the wild)
+        - Protect tests and codebase from incomplete caller() overrides, like
+          e.g. RT#32640
+        - Stop using bare $] throughout - protects the codebase from issues
+          similar (but likely not limited to) P5#72210
+        - Config::Any is no longer a core dep, but instead is migrated to a new
+          optdep group 'config_file_reader'
+
+0.082821 2016-02-11 17:58 (UTC)
+    * Fixes
+        - Fix t/52leaks.t failures on compilerless systems (RT#104429)
+        - Fix t/storage/quote_names.t failures on systems with specified Oracle
+          test credentials while missing the optional Math::Base36
+        - Fix test failures when DBICTEST_SYBASE_DSN is set (unnoticed change
+          in error message wording during 0.082800 and a bogus test)
+        - Remove largely obsolete test of SQLite view deployment (RT#111916)
+
+    * Misc
+        - Work around rare test deadlock under heavy parallelism (RT#108390)
+
+0.082820 2015-03-20 20:35 (UTC)
     * Fixes
+        - Protect destructors from rare but possible double execution, and
+          loudly warn the user whenever the problem is encountered (GH#63)
+        - Relax the 'self_result_object' argument check in the relationship
+          resolution codepath, restoring exotic uses of inflate_result
+          http://lists.scsys.co.uk/pipermail/dbix-class/2015-January/011876.html
+        - Fix updating multiple CLOB/BLOB columns on Oracle
+        - Fix exception on complex update/delete under a replicated setup
+          http://lists.scsys.co.uk/pipermail/dbix-class/2015-January/011903.html
+        - Fix uninitialized warnings on empty hashes passed to join/prefetch
+          https://github.com/vanstyn/RapidApp/commit/6f41f6e48 and
+          http://lists.scsys.co.uk/pipermail/dbix-class/2015-February/011921.html
+        - Fix hang in t/72pg.t when run against DBD::Pg 3.5.0. The ping()
+          implementation changes due to RT#100648 made an alarm() based
+          timeout lock-prone.
+
+    * Misc
+        - Remove warning about potential side effects of RT#79576 (scheduled)
+        - Various doc improvements (GH#35, GH#62, GH#66, GH#70, GH#71, GH#72)
+        - Depend on newer Moo, to benefit from a safer runtime (RT#93004)
+        - Fix intermittent failures in the LeakTracer on 5.18+
+        - Fix failures of t/54taint.t on Windows with spaces in the $^X
+          executable path (RT#101615)
+
+0.082810 2014-10-25 13:58 (UTC)
+    * Fixes
+        - Fix incorrect collapsing-parser source being generated in the
+          presence of unicode data among the collapse-points
+        - Fix endless loop on BareSourcelessResultClass->throw_exception(...)
+
+    * Misc
+        - Depend on newer SQL::Abstract (fixing overly-aggressive parenthesis
+          opener: RT#99503)
+        - Depend on newer Moo, fixing some interoperability issues:
+          http://lists.scsys.co.uk/pipermail/dbix-class/2014-October/011787.html
+
+0.082801 2014-10-05 23:55 (UTC)
+    * Known Issues
+        - Passing large amounts of objects with stringification overload
+          directly to DBIx::Class may result in strange action at a distance
+          exceptions. More info (and a workaround description) can be found
+          under "Note" at https://metacpan.org/pod/SQL::Abstract#is_plain_value
+        - The relationship condition resolution fixes come with the side effect
+          of returning more complete data, tripping up *some* users of an
+          undocumented but widely used internal function. In particular
+          https://rt.cpan.org/Ticket/Display.html?id=91375#txn-1407239
+
+    * Notable Changes and Deprecations
+        - DBIC::FilterColumn now properly bypasses \'' and \[] literals, just
+          like the rest of DBIC
+        - DBIC::FilterColumn "from_storage" handler is now invoked on NULLs
+          returned from storage
+        - find() now throws an exception if some of the supplied values are
+          managed by DBIC::FilterColumn (RT#95054)
+        - Custom condition relationships are now invoked with a slightly
+          different signature (existing coderefs will continue to work)
+        - Add extra custom condition coderef attribute 'foreign_values'
+          to allow for proper reverse-relationship-like behavior
+          (i.e. $result->set_from_related($custom_rel, $foreign_result_object)
+        - When in a transaction, DBIC::Ordered now seamlesly handles result
+          objects that went out of sync with the storage (RT#96499)
+        - CDBICompat::columns() now supports adding columns through supplied
+          Class::DBI::Column instances (GH#52)
+        - Deprecate { col1 => col2 } expressions in manual {from} structures
+          (at some point of time manual {from} will be deprecated entirely)
+
+    * Fixes
+        - Fix Resultset delete/update affecting *THE ENTIRE TABLE* in cases
+          of empty (due to conditions) resultsets with multi-column keys
         - Fix on_connect_* not always firing in some cases - a race condition
           existed between storage accessor setters and the determine_driver
           routines, triggering a connection before the set-cycle is finished
-        - Avoid unnecessary database hits when accessing prefetched related
-          resultsets with no rows.
+        - Fix collapse being ignored on single-origin selection (RT#95658)
+        - Fix incorrect behavior on custom result_class inflators altering
+          the amount of returned results
+        - Fix failure to detect stable order criteria when in iterator
+          mode of a has_many prefetch off a search_related chain
+        - Prevent erroneous database hit when accessing prefetched related
+          resultsets with no rows
+        - Proper exceptions on malformed relationship conditions (RT#92234)
+        - Fix incorrect handling of custom relationship conditions returning
+          SQLA literal expressions
+        - Fix long standing bug with populate() missing data from hashrefs with
+          different keysets: http://is.gd/2011_dbic_populate_gotcha (RT#92723)
+        - Fix multi-value literal populate not working with simplified bind
+          specifications
+        - Massively improve the implied resultset condition parsing - now all
+          applicable conditions within a resultset should be properly picked
+          up by create() and populate()
+        - Ensure definitive condition extractor handles bizarre corner cases
+          without bombing out (RT#93244)
+        - Fix set_column on non-native (+columns) selections (RT#86685)
+        - Fix set_inflated_column incorrectly handling \[] literals (GH#44)
+        - Ensure that setting a column to a literal invariably marks it dirty
+        - Fix copy() not working correctly with extra selections present
+        - Work around exception objects with broken string overloading in one
+          additional codepath (missed in 0.08260)
+        - Fix more inconsistencies of the quote_names attribute propagating
+          to SQL::Translator (partially RT#87731)
+        - Fix SQLT constraint naming when DBIC table names are fully qualified
+          (PR#48)
+        - Ensure ::Schema::Versioned connects only once by reusing the main
+          connection (GH#57)
+        - Fix inability to handle multiple consecutive transactions with
+          savepoints on DBD::SQLite < 1.39
+        - Fix CDBICompat to match Class::DBI behavior handling non-result
+          blessed has_a (implicit deflate via stringification and inflate via
+          blind new) (GH#51)
+
+    * Misc
+        - Ensure source metadata calls always take place on the result source
+          instance registered with the caller
+        - IFF DBIC_TRACE output defaults to STDERR we now silence the possible
+          wide-char warnings if the trace happens to contain unicode
 
 0.08270 2014-01-30 21:54 (PST)
     * Fixes
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..f1a0df5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,409 @@
+This is free software; you can redistribute it and/or modify it under the
+same terms as the Perl5 (v5.0.0 ~ v5.20.0) programming language system
+itself: under the terms of either:
+
+a) the "Artistic License 1.0" as published by The Perl Foundation
+   http://www.perlfoundation.org/artistic_license_1_0
+
+b) the GNU General Public License as published by the Free Software Foundation;
+   either version 1 http://www.gnu.org/licenses/gpl-1.0.html
+   or (at your option) any later version
+
+PLEASE NOTE: It is the current maintainers intention to keep the dual
+licensing intact. Until this notice is removed, releases will continue to
+be available under both the standard GPL and the less restrictive Artistic
+licenses.
+
+Verbatim copies of both licenses are included below:
+
+
+
+--- The Artistic License 1.0 ---
+
+                         The "Artistic License"
+
+                                Preamble
+
+The intent of this document is to state the conditions under which a
+Package may be copied, such that the Copyright Holder maintains some
+semblance of artistic control over the development of the package,
+while giving the users of the package the right to use and distribute
+the Package in a more-or-less customary fashion, plus the right to make
+reasonable modifications.
+
+Definitions:
+
+        "Package" refers to the collection of files distributed by the
+        Copyright Holder, and derivatives of that collection of files
+        created through textual modification.
+
+        "Standard Version" refers to such a Package if it has not been
+        modified, or has been modified in accordance with the wishes
+        of the Copyright Holder as specified below.
+
+        "Copyright Holder" is whoever is named in the copyright or
+        copyrights for the package.
+
+        "You" is you, if you're thinking about copying or distributing
+        this Package.
+
+        "Reasonable copying fee" is whatever you can justify on the
+        basis of media cost, duplication charges, time of people involved,
+        and so on.  (You will not be required to justify it to the
+        Copyright Holder, but only to the computing community at large
+        as a market that must bear the fee.)
+
+        "Freely Available" means that no fee is charged for the item
+        itself, though there may be fees involved in handling the item.
+        It also means that recipients of the item may redistribute it
+        under the same conditions they received it.
+
+1. You may make and give away verbatim copies of the source form of the
+Standard Version of this Package without restriction, provided that you
+duplicate all of the original copyright notices and associated disclaimers.
+
+2. You may apply bug fixes, portability fixes and other modifications
+derived from the Public Domain or from the Copyright Holder.  A Package
+modified in such a way shall still be considered the Standard Version.
+
+3. You may otherwise modify your copy of this Package in any way, provided
+that you insert a prominent notice in each changed file stating how and
+when you changed that file, and provided that you do at least ONE of the
+following:
+
+    a) place your modifications in the Public Domain or otherwise make them
+    Freely Available, such as by posting said modifications to Usenet or
+    an equivalent medium, or placing the modifications on a major archive
+    site such as uunet.uu.net, or by allowing the Copyright Holder to include
+    your modifications in the Standard Version of the Package.
+
+    b) use the modified Package only within your corporation or organization.
+
+    c) rename any non-standard executables so the names do not conflict
+    with standard executables, which must also be provided, and provide
+    a separate manual page for each non-standard executable that clearly
+    documents how it differs from the Standard Version.
+
+    d) make other distribution arrangements with the Copyright Holder.
+
+4. You may distribute the programs of this Package in object code or
+executable form, provided that you do at least ONE of the following:
+
+    a) distribute a Standard Version of the executables and library files,
+    together with instructions (in the manual page or equivalent) on where
+    to get the Standard Version.
+
+    b) accompany the distribution with the machine-readable source of
+    the Package with your modifications.
+
+    c) give non-standard executables non-standard names, and clearly
+    document the differences in manual pages (or equivalent), together
+    with instructions on where to get the Standard Version.
+
+    d) make other distribution arrangements with the Copyright Holder.
+
+5. You may charge a reasonable copying fee for any distribution of this
+Package.  You may charge any fee you choose for support of this
+Package.  You may not charge a fee for this Package itself.  However,
+you may distribute this Package in aggregate with other (possibly
+commercial) programs as part of a larger (possibly commercial) software
+distribution provided that you do not advertise this Package as a
+product of your own.  You may embed this Package's interpreter within
+an executable of yours (by linking); this shall be construed as a mere
+form of aggregation, provided that the complete Standard Version of the
+interpreter is so embedded.
+
+6. The scripts and library files supplied as input to or produced as
+output from the programs of this Package do not automatically fall
+under the copyright of this Package, but belong to whoever generated
+them, and may be sold commercially, and may be aggregated with this
+Package.  If such scripts or library files are aggregated with this
+Package via the so-called "undump" or "unexec" methods of producing a
+binary executable image, then distribution of such an image shall
+neither be construed as a distribution of this Package nor shall it
+fall under the restrictions of Paragraphs 3 and 4, provided that you do
+not represent such an executable image as a Standard Version of this
+Package.
+
+7. C subroutines (or comparably compiled subroutines in other
+languages) supplied by you and linked into this Package in order to
+emulate subroutines and variables of the language defined by this
+Package shall not be considered part of this Package, but are the
+equivalent of input as in Paragraph 6, provided these subroutines do
+not change the language in any way that would cause it to fail the
+regression tests for the language.
+
+8. Aggregation of this Package with a commercial distribution is always
+permitted provided that the use of this Package is embedded; that is,
+when no overt attempt is made to make this Package's interfaces visible
+to the end user of the commercial distribution.  Such use shall not be
+construed as a distribution of this Package.
+
+9. The name of the Copyright Holder may not be used to endorse or promote
+products derived from this software without specific prior written permission.
+
+10. THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
+WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+
+--- end of The Artistic License 1.0 ---
+
+
+
+
+--- The GNU General Public License, Version 1, February 1989 ---
+
+                    GNU GENERAL PUBLIC LICENSE
+                     Version 1, February 1989
+
+ Copyright (C) 1989 Free Software Foundation, Inc.
+                    51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The license agreements of most software companies try to keep users
+at the mercy of those companies.  By contrast, our General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  The
+General Public License applies to the Free Software Foundation's
+software and to any other program whose authors commit to using it.
+You can use it for your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Specifically, the General Public License is designed to make
+sure that you have the freedom to give away or sell copies of free
+software, that you receive source code or can get it if you want it,
+that you can change the software or use pieces of it in new free
+programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of a such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must tell them their rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any program or other work which
+contains a notice placed by the copyright holder saying it may be
+distributed under the terms of this General Public License.  The
+"Program", below, refers to any such program or work, and a "work based
+on the Program" means either the Program or any work containing the
+Program or a portion of it, either verbatim or with modifications.  Each
+licensee is addressed as "you".
+
+  1. You may copy and distribute verbatim copies of the Program's source
+code as you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and
+disclaimer of warranty; keep intact all the notices that refer to this
+General Public License and to the absence of any warranty; and give any
+other recipients of the Program a copy of this General Public License
+along with the Program.  You may charge a fee for the physical act of
+transferring a copy.
+
+  2. You may modify your copy or copies of the Program or any portion of
+it, and copy and distribute such modifications under the terms of Paragraph
+1 above, provided that you also do the following:
+
+    a) cause the modified files to carry prominent notices stating that
+    you changed the files and the date of any change; and
+
+    b) cause the whole of any work that you distribute or publish, that
+    in whole or in part contains the Program or any part thereof, either
+    with or without modifications, to be licensed at no charge to all
+    third parties under the terms of this General Public License (except
+    that you may choose to grant warranty protection to some or all
+    third parties, at your option).
+
+    c) If the modified program normally reads commands interactively when
+    run, you must cause it, when started running for such interactive use
+    in the simplest and most usual way, to print or display an
+    announcement including an appropriate copyright notice and a notice
+    that there is no warranty (or else, saying that you provide a
+    warranty) and that users may redistribute the program under these
+    conditions, and telling the user how to view a copy of this General
+    Public License.
+
+    d) You may charge a fee for the physical act of transferring a
+    copy, and you may at your option offer warranty protection in
+    exchange for a fee.
+
+Mere aggregation of another independent work with the Program (or its
+derivative) on a volume of a storage or distribution medium does not bring
+the other work under the scope of these terms.
+
+  3. You may copy and distribute the Program (or a portion or derivative of
+it, under Paragraph 2) in object code or executable form under the terms of
+Paragraphs 1 and 2 above provided that you also do one of the following:
+
+    a) accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of
+    Paragraphs 1 and 2 above; or,
+
+    b) accompany it with a written offer, valid for at least three
+    years, to give any third party free (except for a nominal charge
+    for the cost of distribution) a complete machine-readable copy of the
+    corresponding source code, to be distributed under the terms of
+    Paragraphs 1 and 2 above; or,
+
+    c) accompany it with the information you received as to where the
+    corresponding source code may be obtained.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form alone.)
+
+Source code for a work means the preferred form of the work for making
+modifications to it.  For an executable file, complete source code means
+all the source code for all modules it contains; but, as a special
+exception, it need not include source code for modules which are standard
+libraries that accompany the operating system on which the executable
+file runs, or for standard header files or definitions files that
+accompany that operating system.
+
+  4. You may not copy, modify, sublicense, distribute or transfer the
+Program except as expressly provided under this General Public License.
+Any attempt otherwise to copy, modify, sublicense, distribute or transfer
+the Program is void, and will automatically terminate your rights to use
+the Program under this License.  However, parties who have received
+copies, or rights to use copies, from you under this General Public
+License will not have their licenses terminated so long as such parties
+remain in full compliance.
+
+  5. By copying, distributing or modifying the Program (or any work based
+on the Program) you indicate your acceptance of this license to do so,
+and all its terms and conditions.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the original
+licensor to copy, distribute or modify the Program subject to these
+terms and conditions.  You may not impose any further restrictions on the
+recipients' exercise of the rights granted herein.
+
+  7. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of the license which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+the license, you may choose any version ever published by the Free Software
+Foundation.
+
+  8. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  9. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  10. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+        Appendix: How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to humanity, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these
+terms.
+
+  To do so, attach the following notices to the program.  It is safest to
+attach them to the start of each source file to most effectively convey
+the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 1, or (at your option)
+    any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA  02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19xx name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the
+appropriate parts of the General Public License.  Of course, the
+commands you use may be called something other than `show w' and `show
+c'; they could even be mouse-clicks or menu items--whatever suits your
+program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  program `Gnomovision' (a program to direct compilers to make passes
+  at assemblers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+That's all there is to it!
+
+--- end of The GNU General Public License, Version 1, February 1989 ---
+
+
index 5e2f3f3..64f490c 100644 (file)
@@ -1,4 +1,4 @@
-^(?!script/|examples/|lib/|inc/|t/|xt/|Makefile\.PL$|maint/|README$|MANIFEST$|Changes$|META\.(?:yml|json)$)
+^(?!script/|examples/|lib/|inc/|t/|xt/|Makefile\.PL$|maint/|README$|MANIFEST$|Changes$|AUTHORS$|LICENSE$|META\.(?:yml|json)$)
 
 # Avoid version control files.
 \bRCS\b
index 492368e..f4ac1b8 100644 (file)
@@ -3,7 +3,13 @@ use warnings;
 
 use 5.008001;
 use inc::Module::Install 1.06;
-BEGIN { makemaker_args( NORECURS => 1 ) } # needs to happen early for old EUMM
+BEGIN {
+  # needs to happen early for old EUMM
+  makemaker_args( NORECURS => 1 );
+
+  local @INC = ('lib', @INC);
+  require DBIx::Class::Optional::Dependencies;
+}
 
 ##
 ## DO NOT USE THIS HACK IN YOUR DISTS!!! (it makes #toolchain sad)
@@ -15,37 +21,15 @@ BEGIN {
   $Module::Install::AUTHOR = 0 if (grep { $ENV{"PERL5_${_}_IS_RUNNING"} } (qw/CPANM CPANPLUS CPAN/) );
 }
 
-homepage 'http://www.dbix-class.org/';
-resources 'IRC'         => 'irc://irc.perl.org/#dbix-class';
-resources 'license'     => 'http://dev.perl.org/licenses/';
-resources 'repository'  => 'https://github.com/dbsrgits/DBIx-Class';
-resources 'MailingList' => 'http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/dbix-class';
-resources 'bugtracker'  => 'http://rt.cpan.org/NoAuth/Bugs.html?Dist=DBIx-Class';
-
-name     'DBIx-Class';
+name         'DBIx-Class';
+version_from 'lib/DBIx/Class.pm';
 perl_version '5.008001';
-all_from 'lib/DBIx/Class.pm';
-Meta->{values}{x_authority} = 'cpan:RIBASUSHI';
-
-# nothing determined at runtime, except for possibly SQLT dep, see
-# comment further down
-dynamic_config 0;
-
-tests_recursive (qw|
-    t
-|);
-
-install_script (qw|
-    script/dbicadmin
-|);
 
 ###
 ### DO NOT ADD OPTIONAL DEPENDENCIES HERE, EVEN AS recommends()
 ### All of them *MUST* go to DBIx::Class::Optional::Dependencies
 ###
 my $runtime_requires = {
-  # FIXME - temporary, needs throwing out for something more efficient
-  'Data::Compare'            => '1.22',
 
   # DBI itself should be capable of installation and execution in pure-perl
   # mode. However it has never been tested yet, so consider XS for the time
@@ -69,22 +53,21 @@ my $runtime_requires = {
   'Sub::Name'                => '0.04',
 
   # pure-perl (FatPack-able) libs
-  'Class::Accessor::Grouped' => '0.10010',
+  'Class::Accessor::Grouped' => '0.10012',
   'Class::C3::Componentised' => '1.0009',
   'Class::Inspector'         => '1.24',
-  'Config::Any'              => '0.20',
   'Context::Preserve'        => '0.01',
   'Data::Dumper::Concise'    => '2.020',
   'Data::Page'               => '2.00',
   'Devel::GlobalDestruction' => '0.09',
   'Hash::Merge'              => '0.12',
-  'Moo'                      => '1.002',
+  'Moo'                      => '2.000',
   'MRO::Compat'              => '0.12',
   'Module::Find'             => '0.07',
   'namespace::clean'         => '0.24',
   'Path::Class'              => '0.18',
   'Scope::Guard'             => '0.03',
-  'SQL::Abstract'            => '1.77',
+  'SQL::Abstract'            => '1.81',
   'Try::Tiny'                => '0.07',
 
   # Technically this is not a core dependency - it is only required
@@ -103,40 +86,51 @@ my $test_requires = {
   'Test::Warn'               => '0.21',
   'Test::More'               => '0.94',
 
-  # needed for testing only, not for operation
-  # we will move away from this dep eventually, perhaps to DBD::CSV or something
-###
-### IMPORTANT - do not raise this dependency
-### even though many bugfixes are present in newer versions, the general DBIC
-### rule is to bend over backwards for available DBDs (given upgrading them is
-### often *not* easy or even possible)
-###
-  'DBD::SQLite'              => '1.29',
-
   # this is already a dep of n::c, but just in case - used by t/55namespaces_cleaned.t
   # remove and do a manual glob-collection if n::c is no longer a dep
   'Package::Stash'           => '0.28',
+
+  # needed for testing only, not for operation
+  # we will move away from this dep eventually, perhaps to DBD::CSV or something
+  %{ DBIx::Class::Optional::Dependencies->req_list_for('test_rdbms_sqlite') },
 };
 
-# if the user has this env var set and no SQLT installed, tests will fail
-# Note - this is added as test_requires *directly*, so it gets properly
+# if the user has some of these env vars set and the deps are not available,
+# tests will fail
+# Note - these are added as test_requires *directly*, so they get properly
 # excluded on META.yml cleansing (even though no dist can be created from this)
-# we force this req regarless of author_deps, worst case scenario it will
+# we force these reqs regarless of author_deps, worst case scenario they will
 # be specified twice
 #
-# also note that we *do* set dynamic_config => 0, as this is the only thing
-# that we determine dynamically, and in all fairness if someone sets the
-# envvar *and* is not running a full Makefile/make/maketest cycle - they get
+# also note that we *do* set dynamic_config => 0, as these are the only things
+# that we determine dynamically, and in all fairness if someone sets these
+# envvars *and* is not running a full Makefile/make/maketest cycle - they get
 # to keep the pieces
-if ($ENV{DBICTEST_SQLT_DEPLOY}) {
-  local @INC = ('lib', @INC);
-  require DBIx::Class::Optional::Dependencies;
-  my $dep_req = DBIx::Class::Optional::Dependencies->req_list_for('deploy');
-  for (keys %$dep_req) {
-    test_requires ($_ => $dep_req->{$_})
+if ( my @optdeps = (
+  $ENV{DBICTEST_SQLT_DEPLOY} ? 'deploy' : (),
+  $ENV{DBICTEST_VIA_REPLICATED} ? 'replicated' : (),
+)) {
+  my $extra_deps = DBIx::Class::Optional::Dependencies->req_list_for(\@optdeps);
+  for (keys %$extra_deps) {
+    test_requires ($_ => $extra_deps->{$_})
   }
 }
 
+tests_recursive ('t');
+tests_recursive ('xt') if (
+  $Module::Install::AUTHOR
+    or
+  $ENV{DBICTEST_RUN_ALL_TESTS}
+    or
+  ( $ENV{TRAVIS}||'' ) eq 'true'
+    or
+  ( $ENV{AUTOMATED_TESTING} and ! $ENV{PERL5_CPANM_IS_RUNNING} and ! $ENV{RELEASE_TESTING} )
+);
+
+install_script (qw|
+    script/dbicadmin
+|);
+
 # this is so we can order requires alphabetically
 # copies are needed for potential author requires injection
 my $reqs = {
@@ -184,10 +178,22 @@ for my $mod (sort keys %final_req) {
 # IFF we are running interactively
 auto_install();
 
-WriteAll();
+{
+  # M::I understands unicode in meta but does not write with the right
+  # layers - fhtagn!!!
+  local $SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /Wide character in print/ };
+  WriteAll();
+}
 
 exit 0;
 
+
+###
+### Nothing user-serviceable beyond this point
+### (none of this executes on regular install)
+###
+
+
 # needs to be here to keep 5.8 string eval happy
 # (the include of Makefile.PL.inc loop)
 my $mm_proto;
diff --git a/examples/Benchmarks/semicolon_vs_comma_rowparser/comma.src b/examples/Benchmarks/semicolon_vs_comma_rowparser/comma.src
new file mode 100644 (file)
index 0000000..95d4f96
--- /dev/null
@@ -0,0 +1,83 @@
+### BEGIN LITERAL STRING EVAL
+  my $rows_pos = 0;
+  my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids );
+
+  # this loop is a bit arcane - the rationale is that the passed in
+  # $_[0] will either have only one row (->next) or will have all
+  # rows already pulled in (->all and/or unordered). Given that the
+  # result can be rather large - we reuse the same already allocated
+  # array, since the collapsed prefetch is smaller by definition.
+  # At the end we cut the leftovers away and move on.
+  while ($cur_row_data = (
+    (
+      $rows_pos >= 0
+        and
+      (
+        $_[0][$rows_pos++]
+          or
+        # It may be tempting to drop the -1 and undef $rows_pos instead
+        # thus saving the >= comparison above as well
+        # However NULL-handlers and underdefined root markers both use
+        # $rows_pos as a last-resort-uniqueness marker (it either is
+        # monotonically increasing while we parse ->all, or is set at
+        # a steady -1 when we are dealing with a single root node). For
+        # the time being the complication of changing all callsites seems
+        # overkill, for what is going to be a very modest saving of ops
+        ( ($rows_pos = -1), undef )
+      )
+    )
+      or
+    ( $_[1] and $_[1]->() )
+  ) ) {
+
+    # the undef checks may or may not be there
+    # depending on whether we prune or not
+    #
+    # due to left joins some of the ids may be NULL/undef, and
+    # won't play well when used as hash lookups
+    # we also need to differentiate NULLs on per-row/per-col basis
+    # (otherwise folding of optional 1:1s will be greatly confused
+( @cur_row_ids{( 0, 1, 5, 6, 8, 10 )} = (
+@{$cur_row_data}[( 0, 1, 5, 6, 8, 10 )]
+ ) ),
+
+    # in the case of an underdefined root - calculate the virtual id (otherwise no code at all)
+
+
+    # if we were supplied a coderef - we are collapsing lazily (the set
+    # is ordered properly)
+    # as long as we have a result already and the next result is new we
+    # return the pre-read data and bail
+( $_[1] and $result_pos and ! $collapse_idx[0]{ $cur_row_ids{1} } and (unshift @{$_[2]}, $cur_row_data) and last ),
+
+    # the rel assemblers
+( $collapse_idx[0]{ $cur_row_ids{1} } //= $_[0][$result_pos++] = [ { "genreid" => $cur_row_data->[4], "latest_cd" => $cur_row_data->[7], "year" => $cur_row_data->[3] } ] ),
+( $collapse_idx[0]{ $cur_row_ids{1} }[1]{"existing_single_track"} //= $collapse_idx[1]{ $cur_row_ids{1} } = [  ] ),
+( $collapse_idx[1]{ $cur_row_ids{1} }[1]{"cd"} //= $collapse_idx[2]{ $cur_row_ids{1} } = [  ] ),
+( $collapse_idx[2]{ $cur_row_ids{1} }[1]{"artist"} //= $collapse_idx[3]{ $cur_row_ids{1} } = [ { "artistid" => $cur_row_data->[1] } ] ),
+( ( ! defined $cur_row_data->[6] )
+  ? $collapse_idx[3]{ $cur_row_ids{1} }[1]{"cds"} = []
+  : do {
+( (! $collapse_idx[4]{ $cur_row_ids{1} }{ $cur_row_ids{6} }) and push @{$collapse_idx[3]{ $cur_row_ids{1} }[1]{"cds"}}, $collapse_idx[4]{ $cur_row_ids{1} }{ $cur_row_ids{6} } = [ { "cdid" => $cur_row_data->[6], "genreid" => $cur_row_data->[9], "year" => $cur_row_data->[2] } ] ),
+( ( ! defined $cur_row_data->[8] )
+  ? $collapse_idx[4]{ $cur_row_ids{1} }{ $cur_row_ids{6} }[1]{"tracks"} = []
+  : do {
+( (! $collapse_idx[5]{ $cur_row_ids{1} }{ $cur_row_ids{6} }{ $cur_row_ids{8} }) and push @{$collapse_idx[4]{ $cur_row_ids{1} }{ $cur_row_ids{6} }[1]{"tracks"}}, $collapse_idx[5]{ $cur_row_ids{1} }{ $cur_row_ids{6} }{ $cur_row_ids{8} } = [ { "title" => $cur_row_data->[8] } ] ),
+} ),
+} ),
+( ( ! defined $cur_row_data->[5] )
+  ? $collapse_idx[0]{ $cur_row_ids{1} }[1]{"tracks"} = []
+  : do {
+( (! $collapse_idx[6]{ $cur_row_ids{1} }{ $cur_row_ids{5} }) and push @{$collapse_idx[0]{ $cur_row_ids{1} }[1]{"tracks"}}, $collapse_idx[6]{ $cur_row_ids{1} }{ $cur_row_ids{5} } = [ { "title" => $cur_row_data->[5] } ] ),
+( ( ! defined $cur_row_data->[10] )
+  ? $collapse_idx[6]{ $cur_row_ids{1} }{ $cur_row_ids{5} }[1]{"lyrics"} = []
+  : do {
+( $collapse_idx[6]{ $cur_row_ids{1} }{ $cur_row_ids{5} }[1]{"lyrics"} //= $collapse_idx[7]{ $cur_row_ids{1} }{ $cur_row_ids{5} }{ $cur_row_ids{10} } = [  ] ),
+( (! $collapse_idx[8]{ $cur_row_ids{0} }{ $cur_row_ids{1} }{ $cur_row_ids{5} }{ $cur_row_ids{10} }) and push @{$collapse_idx[7]{ $cur_row_ids{1} }{ $cur_row_ids{5} }{ $cur_row_ids{10} }[1]{"existing_lyric_versions"}}, $collapse_idx[8]{ $cur_row_ids{0} }{ $cur_row_ids{1} }{ $cur_row_ids{5} }{ $cur_row_ids{10} } = [ { "lyric_id" => $cur_row_data->[10], "text" => $cur_row_data->[0] } ] ),
+} ),
+} ),
+
+  }
+
+  $#{$_[0]} = $result_pos - 1; # truncate the passed in array to where we filled it with results
+### END LITERAL STRING EVAL
diff --git a/examples/Benchmarks/semicolon_vs_comma_rowparser/semicol.src b/examples/Benchmarks/semicolon_vs_comma_rowparser/semicol.src
new file mode 100644 (file)
index 0000000..3d33e96
--- /dev/null
@@ -0,0 +1,83 @@
+### BEGIN LITERAL STRING EVAL
+  my $rows_pos = 0;
+  my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids );
+
+  # this loop is a bit arcane - the rationale is that the passed in
+  # $_[0] will either have only one row (->next) or will have all
+  # rows already pulled in (->all and/or unordered). Given that the
+  # result can be rather large - we reuse the same already allocated
+  # array, since the collapsed prefetch is smaller by definition.
+  # At the end we cut the leftovers away and move on.
+  while ($cur_row_data = (
+    (
+      $rows_pos >= 0
+        and
+      (
+        $_[0][$rows_pos++]
+          or
+        # It may be tempting to drop the -1 and undef $rows_pos instead
+        # thus saving the >= comparison above as well
+        # However NULL-handlers and underdefined root markers both use
+        # $rows_pos as a last-resort-uniqueness marker (it either is
+        # monotonically increasing while we parse ->all, or is set at
+        # a steady -1 when we are dealing with a single root node). For
+        # the time being the complication of changing all callsites seems
+        # overkill, for what is going to be a very modest saving of ops
+        ( ($rows_pos = -1), undef )
+      )
+    )
+      or
+    ( $_[1] and $_[1]->() )
+  ) ) {
+
+    # the undef checks may or may not be there
+    # depending on whether we prune or not
+    #
+    # due to left joins some of the ids may be NULL/undef, and
+    # won't play well when used as hash lookups
+    # we also need to differentiate NULLs on per-row/per-col basis
+    # (otherwise folding of optional 1:1s will be greatly confused
+@cur_row_ids{( 0, 1, 5, 6, 8, 10 )} = (
+@{$cur_row_data}[( 0, 1, 5, 6, 8, 10 )]
+ );
+
+    # in the case of an underdefined root - calculate the virtual id (otherwise no code at all)
+
+
+    # if we were supplied a coderef - we are collapsing lazily (the set
+    # is ordered properly)
+    # as long as we have a result already and the next result is new we
+    # return the pre-read data and bail
+$_[1] and $result_pos and ! $collapse_idx[0]{ $cur_row_ids{1} } and (unshift @{$_[2]}, $cur_row_data) and last;
+
+    # the rel assemblers
+$collapse_idx[0]{ $cur_row_ids{1} } //= $_[0][$result_pos++] = [ { "genreid" => $cur_row_data->[4], "latest_cd" => $cur_row_data->[7], "year" => $cur_row_data->[3] } ];
+$collapse_idx[0]{ $cur_row_ids{1} }[1]{"existing_single_track"} //= $collapse_idx[1]{ $cur_row_ids{1} } = [  ];
+$collapse_idx[1]{ $cur_row_ids{1} }[1]{"cd"} //= $collapse_idx[2]{ $cur_row_ids{1} } = [  ];
+$collapse_idx[2]{ $cur_row_ids{1} }[1]{"artist"} //= $collapse_idx[3]{ $cur_row_ids{1} } = [ { "artistid" => $cur_row_data->[1] } ];
+( ! defined $cur_row_data->[6] )
+  ? $collapse_idx[3]{ $cur_row_ids{1} }[1]{"cds"} = []
+  : do {
+(! $collapse_idx[4]{ $cur_row_ids{1} }{ $cur_row_ids{6} }) and push @{$collapse_idx[3]{ $cur_row_ids{1} }[1]{"cds"}}, $collapse_idx[4]{ $cur_row_ids{1} }{ $cur_row_ids{6} } = [ { "cdid" => $cur_row_data->[6], "genreid" => $cur_row_data->[9], "year" => $cur_row_data->[2] } ];
+( ! defined $cur_row_data->[8] )
+  ? $collapse_idx[4]{ $cur_row_ids{1} }{ $cur_row_ids{6} }[1]{"tracks"} = []
+  : do {
+(! $collapse_idx[5]{ $cur_row_ids{1} }{ $cur_row_ids{6} }{ $cur_row_ids{8} }) and push @{$collapse_idx[4]{ $cur_row_ids{1} }{ $cur_row_ids{6} }[1]{"tracks"}}, $collapse_idx[5]{ $cur_row_ids{1} }{ $cur_row_ids{6} }{ $cur_row_ids{8} } = [ { "title" => $cur_row_data->[8] } ];
+};
+};
+( ! defined $cur_row_data->[5] )
+  ? $collapse_idx[0]{ $cur_row_ids{1} }[1]{"tracks"} = []
+  : do {
+(! $collapse_idx[6]{ $cur_row_ids{1} }{ $cur_row_ids{5} }) and push @{$collapse_idx[0]{ $cur_row_ids{1} }[1]{"tracks"}}, $collapse_idx[6]{ $cur_row_ids{1} }{ $cur_row_ids{5} } = [ { "title" => $cur_row_data->[5] } ];
+( ! defined $cur_row_data->[10] )
+  ? $collapse_idx[6]{ $cur_row_ids{1} }{ $cur_row_ids{5} }[1]{"lyrics"} = []
+  : do {
+$collapse_idx[6]{ $cur_row_ids{1} }{ $cur_row_ids{5} }[1]{"lyrics"} //= $collapse_idx[7]{ $cur_row_ids{1} }{ $cur_row_ids{5} }{ $cur_row_ids{10} } = [  ];
+(! $collapse_idx[8]{ $cur_row_ids{0} }{ $cur_row_ids{1} }{ $cur_row_ids{5} }{ $cur_row_ids{10} }) and push @{$collapse_idx[7]{ $cur_row_ids{1} }{ $cur_row_ids{5} }{ $cur_row_ids{10} }[1]{"existing_lyric_versions"}}, $collapse_idx[8]{ $cur_row_ids{0} }{ $cur_row_ids{1} }{ $cur_row_ids{5} }{ $cur_row_ids{10} } = [ { "lyric_id" => $cur_row_data->[10], "text" => $cur_row_data->[0] } ];
+};
+};
+
+  }
+
+  $#{$_[0]} = $result_pos - 1; # truncate the passed in array to where we filled it with results
+### END LITERAL STRING EVAL
diff --git a/examples/Benchmarks/semicolon_vs_comma_rowparser/sloppy_bench.pl b/examples/Benchmarks/semicolon_vs_comma_rowparser/sloppy_bench.pl
new file mode 100644 (file)
index 0000000..fb12fb8
--- /dev/null
@@ -0,0 +1,28 @@
+use warnings;
+use strict;
+
+use Benchmark qw( cmpthese :hireswallclock);
+use Sereal;
+use Devel::Dwarn;
+
+my ($semicol, $comma) = map {
+  my $src = do { local (@ARGV, $/) = $_; <> };
+  eval "sub { use strict; use warnings; use warnings FATAL => 'uninitialized'; $src }" or die $@;
+} qw( semicol.src comma.src );
+
+my $enc = Sereal::Encoder->new;
+my $dec = Sereal::Decoder->new;
+
+for my $iters ( 100, 10_000, 100_000 ) {
+  my $dataset = [];
+  push @$dataset, [ (scalar @$dataset) x 11 ]
+    while @$dataset < $iters;
+
+  my $ice = $enc->encode($dataset);
+
+  print "\nTiming $iters 'rows'...\n";
+  cmpthese( -10, {
+    semicol => sub { $semicol->($dec->decode($ice)) },
+    comma => sub { $comma->($dec->decode($ice)) },
+  })
+}
index 70074b1..3cde36c 100644 (file)
@@ -21,7 +21,7 @@ __PACKAGE__->set_primary_key('artistid');
 
 __PACKAGE__->add_unique_constraint([qw( name )]);
 
-__PACKAGE__->has_many('cds' => 'MyApp::Schema::Result::Cd');
+__PACKAGE__->has_many('cds' => 'MyApp::Schema::Result::Cd', 'artistid');
 
 1;
 
index 9b0602c..d788744 100644 (file)
@@ -12,7 +12,7 @@ __PACKAGE__->add_columns(
     data_type => 'integer',
     is_auto_increment => 1
   },
-  artist => {
+  artistid => {
     data_type => 'integer',
   },
   title => {
@@ -26,9 +26,9 @@ __PACKAGE__->add_columns(
 
 __PACKAGE__->set_primary_key('cdid');
 
-__PACKAGE__->add_unique_constraint([qw( title artist )]);
+__PACKAGE__->add_unique_constraint([qw( title artistid )]);
 
-__PACKAGE__->belongs_to('artist' => 'MyApp::Schema::Result::Artist');
-__PACKAGE__->has_many('tracks' => 'MyApp::Schema::Result::Track');
+__PACKAGE__->belongs_to('artist' => 'MyApp::Schema::Result::Artist', 'artistid');
+__PACKAGE__->has_many('tracks' => 'MyApp::Schema::Result::Track', 'cdid');
 
 1;
index dc0951a..a32a27e 100644 (file)
@@ -12,7 +12,7 @@ __PACKAGE__->add_columns(
     data_type => 'integer',
     is_auto_increment => 1
   },
-  cd => {
+  cdid => {
     data_type => 'integer',
   },
   title => {
@@ -22,8 +22,8 @@ __PACKAGE__->add_columns(
 
 __PACKAGE__->set_primary_key('trackid');
 
-__PACKAGE__->add_unique_constraint([qw( title cd )]);
+__PACKAGE__->add_unique_constraint([qw( title cdid )]);
 
-__PACKAGE__->belongs_to('cd' => 'MyApp::Schema::Result::Cd');
+__PACKAGE__->belongs_to('cd' => 'MyApp::Schema::Result::Cd', 'cdid');
 
 1;
index c57460e..ae919b3 100755 (executable)
@@ -31,7 +31,7 @@ foreach my $lp (keys %albums) {
 }
 
 $schema->populate('Cd', [
-    [qw/title artist/],
+    [qw/title artistid/],
     @cds,
 ]);
 
@@ -55,6 +55,6 @@ foreach my $track (keys %tracks) {
 }
 
 $schema->populate('Track',[
-    [qw/cd title/],
+    [qw/cdid title/],
     @tracks,
 ]);
index 2a1061a..32cbd6d 100755 (executable)
@@ -53,7 +53,8 @@ sub get_tracks_by_artist {
         }
     );
     while (my $track = $rs->next) {
-        print $track->title . "\n";
+        print $track->title . " (from the CD '" . $track->cd->title
+          . "')\n";
     }
     print "\n";
 }
@@ -70,7 +71,7 @@ sub get_cd_by_track {
         }
     );
     my $cd = $rs->first;
-    print $cd->title . "\n\n";
+    print $cd->title . " has the track '$tracktitle'.\n\n";
 }
 
 sub get_cds_by_artist {
@@ -104,7 +105,7 @@ sub get_artist_by_track {
         }
     );
     my $artist = $rs->first;
-    print $artist->name . "\n\n";
+    print $artist->name . " recorded the track '$tracktitle'.\n\n";
 }
 
 sub get_artist_by_cd {
@@ -119,5 +120,5 @@ sub get_artist_by_cd {
         }
     );
     my $artist = $rs->first;
-    print $artist->name . "\n\n";
+    print $artist->name . " recorded the CD '$cdtitle'.\n\n";
 }
index ba237a2..1adbe64 100644 (file)
@@ -11,7 +11,7 @@ our $VERSION;
 # $VERSION declaration must stay up here, ahead of any other package
 # declarations, as to not confuse various modules attempting to determine
 # this ones version, whether that be s.c.o. or Module::Metadata, etc
-$VERSION = '0.08270';
+$VERSION = '0.082899_15';
 
 $VERSION = eval $VERSION if $VERSION =~ /_/; # numify for warning-free dev releases
 
@@ -25,7 +25,19 @@ use DBIx::Class::StartupCheck;
 use DBIx::Class::Exception;
 
 __PACKAGE__->mk_group_accessors(inherited => '_skip_namespace_frames');
-__PACKAGE__->_skip_namespace_frames('^DBIx::Class|^SQL::Abstract|^Try::Tiny|^Class::Accessor::Grouped|^Context::Preserve');
+__PACKAGE__->_skip_namespace_frames('^DBIx::Class|^SQL::Abstract|^Try::Tiny|^Class::Accessor::Grouped|^Context::Preserve|^Moose::Meta::');
+
+# FIXME - this is not really necessary, and is in
+# fact going to slow things down a bit
+# However it is the right thing to do in order to get
+# various install bases to highlight their brokenness
+# Remove at some unknown point in the future
+#
+# The oddball BEGIN is there for... reason unknown
+# It does make non-segfaulty difference on pre-5.8.5 perls, so shrug
+BEGIN {
+  sub DESTROY { &DBIx::Class::_Util::detected_reinvoked_destructor };
+}
 
 sub mk_classdata {
   shift->mk_classaccessor(@_);
@@ -57,12 +69,16 @@ sub _attr_cache {
   };
 }
 
+# *DO NOT* change this URL nor the identically named =head1 below
+# it is linked throughout the ecosystem
+sub DBIx::Class::_ENV_::HELP_URL () {
+  'http://p3rl.org/DBIx::Class#GETTING_HELP/SUPPORT'
+}
+
 1;
 
 __END__
 
-=encoding UTF-8
-
 =head1 NAME
 
 DBIx::Class - Extensible and flexible object <-> relational mapper.
@@ -74,13 +90,15 @@ To get the most out of DBIx::Class with the least confusion it is strongly
 recommended to read (at the very least) the
 L<Manuals|DBIx::Class::Manual::DocMap/Manuals> in the order presented there.
 
-=head1 HOW TO GET HELP
+=cut
+
+=head1 GETTING HELP/SUPPORT
 
-Due to the complexity of its problem domain, DBIx::Class is a relatively
+Due to the sheer size of its problem domain, DBIx::Class is a relatively
 complex framework. After you start using DBIx::Class questions will inevitably
 arise. If you are stuck with a problem or have doubts about a particular
-approach do not hesitate to contact the community with your questions. The
-list below is sorted by "fastest response time":
+approach do not hesitate to contact us via any of the following options (the
+list is sorted by "fastest response time"):
 
 =over
 
@@ -196,7 +214,7 @@ Then you can use these classes in your application's code:
   my $cd = $millennium_cds_rs->next; # SELECT ... FROM cds JOIN artists ...
   my $cd_artist_name = $cd->artist->name; # Already has the data so no 2nd query
 
-  # new() makes a Result object but doesnt insert it into the DB.
+  # new() makes a Result object but doesn't insert it into the DB.
   # create() is the same as new() then insert().
   my $new_cd = $schema->resultset('CD')->new({ title => 'Spoon' });
   $new_cd->artist($cd->artist);
@@ -249,8 +267,10 @@ Contributions are always welcome, in all usable forms (we especially
 welcome documentation improvements). The delivery methods include git-
 or unified-diff formatted patches, GitHub pull requests, or plain bug
 reports either via RT or the Mailing list. Contributors are generally
-granted full access to the official repository after their first patch
-passes successful review.
+granted access to the official repository after their first several
+patches pass successful review. Don't hesitate to
+L<contact|/GETTING HELP/SUPPORT> either of the L</CAT HERDERS> with
+any further questions you may have.
 
 =for comment
 FIXME: Getty, frew and jnap need to get off their asses and finish the contrib section so we can link it here ;)
@@ -271,283 +291,48 @@ accessible at the following locations:
 =item * Travis-CI log: L<https://travis-ci.org/dbsrgits/dbix-class/builds>
 
 =for html
-&#x21AA; Stable branch CI status: <img src="https://secure.travis-ci.org/dbsrgits/dbix-class.png?branch=master"></img>
+&#x21AA; Bleeding edge dev CI status: <img src="https://secure.travis-ci.org/dbsrgits/dbix-class.png?branch=master"></img>
 
 =back
 
-=head1 AUTHOR
-
-mst: Matt S. Trout <mst@shadowcatsystems.co.uk>
-
-(I mostly consider myself "project founder" these days but the AUTHOR heading
-is traditional :)
-
-=head1 CONTRIBUTORS
-
-abraxxa: Alexander Hartmaier <abraxxa@cpan.org>
-
-acca: Alexander Kuznetsov <acca@cpan.org>
-
-aherzog: Adam Herzog <adam@herzogdesigns.com>
-
-Alexander Keusch <cpan@keusch.at>
-
-alexrj: Alessandro Ranellucci <aar@cpan.org>
-
-alnewkirk: Al Newkirk <we@ana.im>
-
-amiri: Amiri Barksdale <amiri@metalabel.com>
-
-amoore: Andrew Moore <amoore@cpan.org>
-
-andrewalker: Andre Walker <andre@andrewalker.net>
-
-andyg: Andy Grundman <andy@hybridized.org>
-
-ank: Andres Kievsky
-
-arc: Aaron Crane <arc@cpan.org>
-
-arcanez: Justin Hunter <justin.d.hunter@gmail.com>
-
-ash: Ash Berlin <ash@cpan.org>
-
-bert: Norbert Csongrádi <bert@cpan.org>
-
-blblack: Brandon L. Black <blblack@gmail.com>
-
-bluefeet: Aran Deltac <bluefeet@cpan.org>
-
-bphillips: Brian Phillips <bphillips@cpan.org>
-
-boghead: Bryan Beeley <cpan@beeley.org>
-
-brd: Brad Davis <brd@FreeBSD.org>
-
-bricas: Brian Cassidy <bricas@cpan.org>
-
-brunov: Bruno Vecchi <vecchi.b@gmail.com>
-
-caelum: Rafael Kitover <rkitover@cpan.org>
-
-caldrin: Maik Hentsche <maik.hentsche@amd.com>
-
-castaway: Jess Robinson
-
-claco: Christopher H. Laco
-
-clkao: CL Kao
-
-da5id: David Jack Olrik <djo@cpan.org>
-
-dariusj: Darius Jokilehto <dariusjokilehto@yahoo.co.uk>
-
-davewood: David Schmidt <davewood@gmx.at>
-
-daxim: Lars Dɪᴇᴄᴋᴏᴡ 迪拉斯 <daxim@cpan.org>
-
-debolaz: Anders Nor Berle <berle@cpan.org>
-
-dew: Dan Thomas <dan@godders.org>
-
-dkubb: Dan Kubb <dan.kubb-cpan@onautopilot.com>
-
-dnm: Justin Wheeler <jwheeler@datademons.com>
-
-dpetrov: Dimitar Petrov <mitakaa@gmail.com>
-
-dwc: Daniel Westermann-Clark <danieltwc@cpan.org>
-
-dyfrgi: Michael Leuchtenburg <michael@slashhome.org>
-
-edenc: Eden Cardim <edencardim@gmail.com>
-
-ether: Karen Etheridge <ether@cpan.org>
-
-felliott: Fitz Elliott <fitz.elliott@gmail.com>
-
-freetime: Bill Moseley <moseley@hank.org>
-
-frew: Arthur Axel "fREW" Schmidt <frioux@gmail.com>
-
-goraxe: Gordon Irving <goraxe@cpan.org>
-
-gphat: Cory G Watson <gphat@cpan.org>
-
-Grant Street Group L<http://www.grantstreet.com/>
-
-groditi: Guillermo Roditi <groditi@cpan.org>
-
-Haarg: Graham Knop <haarg@haarg.org>
-
-hobbs: Andrew Rodland <arodland@cpan.org>
-
-ilmari: Dagfinn Ilmari MannsE<aring>ker <ilmari@ilmari.org>
-
-initself: Mike Baas <mike@initselftech.com>
-
-ironcamel: Naveed Massjouni <naveedm9@gmail.com>
-
-jawnsy: Jonathan Yu <jawnsy@cpan.org>
-
-jasonmay: Jason May <jason.a.may@gmail.com>
-
-jesper: Jesper Krogh
-
-jgoulah: John Goulah <jgoulah@cpan.org>
-
-jguenther: Justin Guenther <jguenther@cpan.org>
+=head1 AUTHORS
 
-jhannah: Jay Hannah <jay@jays.net>
+Even though a large portion of the source I<appears> to be written by just a
+handful of people, this library continues to remain a collaborative effort -
+perhaps one of the most successful such projects on L<CPAN|http://cpan.org>.
+It is important to remember that ideas do not always result in a direct code
+contribution, but deserve acknowledgement just the same. Time and time again
+the seemingly most insignificant questions and suggestions have been shown
+to catalyze monumental improvements in consistency, accuracy and performance.
 
-jmac: Jason McIntosh <jmac@appleseed-sc.com>
+=for comment this line is replaced with the author list at dist-building time
 
-jnapiorkowski: John Napiorkowski <jjn1056@yahoo.com>
+The canonical source of authors and their details is the F<AUTHORS> file at
+the root of this distribution (or repository). The canonical source of
+per-line authorship is the L<git repository|/HOW TO CONTRIBUTE> history
+itself.
 
-jon: Jon Schutz <jjschutz@cpan.org>
+=head1 CAT HERDERS
 
-jshirley: J. Shirley <jshirley@gmail.com>
+The fine folks nudging the project in a particular direction:
 
-kaare: Kaare Rasmussen
-
-konobi: Scott McWhirter
-
-littlesavage: Alexey Illarionov <littlesavage@orionet.ru>
-
-lukes: Luke Saunders <luke.saunders@gmail.com>
-
-marcus: Marcus Ramberg <mramberg@cpan.org>
-
-mattlaw: Matt Lawrence
-
-mattp: Matt Phillips <mattp@cpan.org>
-
-michaelr: Michael Reddick <michael.reddick@gmail.com>
-
-milki: Jonathan Chu <milki@rescomp.berkeley.edu>
-
-mithaldu: Christian Walde <walde.christian@gmail.com>
-
-mjemmeson: Michael Jemmeson <michael.jemmeson@gmail.com>
-
-mstratman: Mark A. Stratman <stratman@gmail.com>
-
-ned: Neil de Carteret
-
-nigel: Nigel Metheringham <nigelm@cpan.org>
-
-ningu: David Kamholz <dkamholz@cpan.org>
-
-Nniuq: Ron "Quinn" Straight" <quinnfazigu@gmail.org>
-
-norbi: Norbert Buchmuller <norbi@nix.hu>
-
-nuba: Nuba Princigalli <nuba@cpan.org>
-
-Numa: Dan Sully <daniel@cpan.org>
-
-ovid: Curtis "Ovid" Poe <ovid@cpan.org>
-
-oyse: E<Oslash>ystein Torget <oystein.torget@dnv.com>
-
-paulm: Paul Makepeace
-
-penguin: K J Cheetham
-
-perigrin: Chris Prather <chris@prather.org>
-
-peter: Peter Collingbourne <peter@pcc.me.uk>
-
-Peter Siklósi <einon@einon.hu>
-
-Peter Valdemar ME<oslash>rch <peter@morch.com>
-
-phaylon: Robert Sedlacek <phaylon@dunkelheit.at>
-
-plu: Johannes Plunien <plu@cpan.org>
-
-Possum: Daniel LeWarne <possum@cpan.org>
-
-quicksilver: Jules Bean
-
-rafl: Florian Ragwitz <rafl@debian.org>
-
-rainboxx: Matthias Dietrich <perl@rb.ly>
-
-rbo: Robert Bohne <rbo@cpan.org>
-
-rbuels: Robert Buels <rmb32@cornell.edu>
-
-rdj: Ryan D Johnson <ryan@innerfence.com>
-
-ribasushi: Peter Rabbitson <ribasushi@cpan.org>
-
-rjbs: Ricardo Signes <rjbs@cpan.org>
-
-robkinyon: Rob Kinyon <rkinyon@cpan.org>
-
-Robert Olson <bob@rdolson.org>
-
-moltar: Roman Filippov <romanf@cpan.org>
-
-Sadrak: Felix Antonius Wilhelm Ostmann <sadrak@cpan.org>
-
-sc_: Just Another Perl Hacker
-
-scotty: Scotty Allen <scotty@scottyallen.com>
-
-semifor: Marc Mims <marc@questright.com>
-
-SineSwiper: Brendan Byrd <bbyrd@cpan.org>
-
-solomon: Jared Johnson <jaredj@nmgi.com>
-
-spb: Stephen Bennett <stephen@freenode.net>
-
-Squeeks <squeek@cpan.org>
-
-sszabo: Stephan Szabo <sszabo@bigpanda.com>
-
-talexb: Alex Beamish <talexb@gmail.com>
-
-tamias: Ronald J Kimball <rjk@tamias.net>
-
-teejay : Aaron Trevena <teejay@cpan.org>
-
-Todd Lipcon
-
-Tom Hukins
-
-tonvoon: Ton Voon <tonvoon@cpan.org>
-
-triode: Pete Gamache <gamache@cpan.org>
-
-typester: Daisuke Murase <typester@cpan.org>
-
-victori: Victor Igumnov <victori@cpan.org>
-
-wdh: Will Hawes
-
-wesm: Wes Malone <wes@mitsi.com>
-
-willert: Sebastian Willert <willert@cpan.org>
-
-wreis: Wallace Reis <wreis@cpan.org>
-
-xenoterracide: Caleb Cushing <xenoterracide@gmail.com>
+=over
 
-yrlnry: Mark Jason Dominus <mjd@plover.com>
+B<ribasushi>: Peter Rabbitson <ribasushi@cpan.org>
+(present day maintenance and controlled evolution)
 
-zamolxes: Bogdan Lucaciu <bogdan@wiz.ro>
+B<castaway>: Jess Robinson <castaway@desert-island.me.uk>
+(lions share of the reference documentation and manuals)
 
-Zefram: Andrew Main <zefram@fysh.org>
+B<mst>: Matt S Trout <mst@shadowcat.co.uk> (project founder -
+original idea, architecture and implementation)
 
-=head1 COPYRIGHT
+=back
 
-Copyright (c) 2005 - 2011 the DBIx::Class L</AUTHOR> and L</CONTRIBUTORS>
-as listed above.
+=head1 COPYRIGHT AND LICENSE
 
-=head1 LICENSE
+Copyright (c) 2005 by mst, castaway, ribasushi, and other DBIx::Class
+L</AUTHORS> as listed above and in F<AUTHORS>.
 
 This library is free software and may be distributed under the same terms
-as perl itself.
+as perl5 itself. See F<LICENSE> for the complete licensing terms.
index c999a6b..ea25e4f 100644 (file)
@@ -44,13 +44,15 @@ DBIx::Class::AccessorGroup - See Class::Accessor::Grouped
 
 This class now exists in its own right on CPAN as Class::Accessor::Grouped
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
-
index b30aa0a..60d8c9e 100644 (file)
@@ -1,10 +1,14 @@
 package DBIx::Class::Admin;
 
+use warnings;
+use strict;
+
 # check deps
 BEGIN {
-  use DBIx::Class;
-  die('The following modules are required for DBIx::Class::Admin ' . DBIx::Class::Optional::Dependencies->req_missing_for ('admin') )
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('admin');
+  require DBIx::Class::Optional::Dependencies;
+  if (my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('admin') ) {
+    die "The following extra modules are required for DBIx::Class::Admin: $missing\n";
+  }
 }
 
 use JSON::Any qw(DWIW PP JSON CPANEL XS);
@@ -14,8 +18,7 @@ use DBIx::Class::Admin::Types qw/DBICConnectInfo DBICHashRef/;
 use MooseX::Types::JSON qw(JSON);
 use MooseX::Types::Path::Class qw(Dir File);
 use MooseX::Types::LoadableClass qw(LoadableClass);
-use Try::Tiny;
-use namespace::autoclean;
+use namespace::clean;
 
 =head1 NAME
 
@@ -206,9 +209,6 @@ has config => (
 sub _build_config {
   my ($self) = @_;
 
-  try { require Config::Any }
-    catch { die ("Config::Any is required to parse the config file.\n") };
-
   my $cfg = Config::Any->load_files ( {files => [$self->config_file], use_ext =>1, flatten_to_hash=>1});
 
   # just grab the config from the config file
@@ -339,8 +339,6 @@ sub create {
   $sqlt_type ||= $self->sql_type();
 
   my $schema = $self->schema();
-  # create the dir if does not exist
-  $self->sql_dir->mkpath() if ( ! -d $self->sql_dir);
 
   $schema->create_ddl_dir( $sqlt_type, (defined $schema->schema_version ? $schema->schema_version : ""), $self->sql_dir->stringify, $preversion, $sqlt_args );
 }
@@ -451,7 +449,7 @@ sub insert {
   $rs ||= $self->resultset();
   $set ||= $self->set();
   my $resultset = $self->schema->resultset($rs);
-  my $obj = $resultset->create( $set );
+  my $obj = $resultset->new_result($set)->insert;
   print ''.ref($resultset).' ID: '.join(',',$obj->id())."\n" if (!$self->quiet);
 }
 
@@ -582,13 +580,16 @@ sub _find_stanza {
   return $cfg;
 }
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index e703ede..d479ae5 100644 (file)
@@ -41,7 +41,7 @@ sub pod_authorlic_text {
 
   return join ("\n\n",
     '=head1 AUTHORS',
-    'See L<DBIx::Class/CONTRIBUTORS>',
+    'See L<DBIx::Class/AUTHORS>',
     '=head1 LICENSE',
     'You may distribute this code under the same terms as Perl itself',
     '=cut',
index ee983fd..f3697c2 100644 (file)
@@ -2,22 +2,15 @@ package DBIx::Class::CDBICompat;
 
 use strict;
 use warnings;
-use base qw/DBIx::Class::Core DBIx::Class::DB/;
-
-# Modules CDBICompat needs that DBIx::Class does not.
-my @Extra_Modules = qw(
-    Class::Trigger
-    DBIx::ContextualFetch
-    Clone
-);
 
-my @didnt_load;
-for my $module (@Extra_Modules) {
-    push @didnt_load, $module unless eval qq{require $module};
+BEGIN {
+  require DBIx::Class::Optional::Dependencies;
+  if (my $missing = DBIx::Class::Optional::Dependencies->req_missing_for('cdbicompat')) {
+    die "The following extra modules are required for DBIx::Class::CDBICompat: $missing\n";
+  }
 }
-__PACKAGE__->throw_exception("@{[ join ', ', @didnt_load ]} are missing and are required for CDBICompat")
-    if @didnt_load;
 
+use base qw/DBIx::Class::Core DBIx::Class::DB/;
 
 __PACKAGE__->load_own_components(qw/
   Constraints
@@ -45,9 +38,10 @@ __PACKAGE__->load_own_components(qw/
   Iterator
 /);
 
-            #DBIx::Class::ObjIndexStubs
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::CDBICompat - Class::DBI Compatibility layer.
@@ -165,13 +159,13 @@ Relationships between tables (has_a, has_many...) must be declared after all tab
 
 =back
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
-
-=head1 LICENSE
+=head1 FURTHER QUESTIONS?
 
-You may distribute this code under the same terms as Perl itself.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=cut
+=head1 COPYRIGHT AND LICENSE
 
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 4192c47..8f59106 100644 (file)
@@ -34,4 +34,17 @@ sub search_where {
     return $class->resultset_instance->search($where, $attr);
 }
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
+=cut
+
 1;
index 1ea49e8..1555937 100644 (file)
@@ -4,14 +4,21 @@ package # hide from PAUSE Indexer
 use strict;
 use warnings;
 
+use Scalar::Util 'blessed';
+use namespace::clean;
+
 sub mk_group_accessors {
     my ($class, $group, @cols) = @_;
 
     foreach my $col (@cols) {
-        my($accessor, $col) = ref $col ? @$col : (undef, $col);
+        my($accessor, $col) = ref $col eq 'ARRAY' ? @$col : (undef, $col);
 
         my($ro_meth, $wo_meth);
-        if( defined $accessor and ($accessor ne $col)) {
+        if (defined blessed $col and $col->isa('Class::DBI::Column')) {
+            $ro_meth = $col->accessor;
+            $wo_meth = $col->mutator;
+        }
+        elsif (defined $accessor and ($accessor ne $col)) {
             $ro_meth = $wo_meth = $accessor;
         }
         else {
index d804d02..f4c8ac8 100644 (file)
@@ -4,7 +4,6 @@ package # hide from PAUSE
 use strict;
 use warnings;
 use Sub::Name ();
-use Storable 'dclone';
 use List::Util ();
 
 use base qw/DBIx::Class::Row/;
@@ -43,7 +42,7 @@ sub _register_column_group {
 
   # Must do a complete deep copy else column groups
   # might accidentally be shared.
-  my $groups = dclone $class->_column_groups;
+  my $groups = DBIx::Class::_Util::deep_clone( $class->_column_groups );
 
   if ($group eq 'Primary') {
     $class->set_primary_key(@cols);
index 4e47ed3..c5c1fe1 100644 (file)
@@ -102,4 +102,17 @@ sub STORE {
                 : $obj->set_column($col => shift);
 }
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
+=cut
+
 1;
index f2e78b9..65ce576 100644 (file)
@@ -1,14 +1,13 @@
 package # hide from PAUSE
     DBIx::Class::CDBICompat::Constructor;
 
-use base qw(DBIx::Class::CDBICompat::ImaDBI);
-
-use Sub::Name();
-
 use strict;
 use warnings;
 
+use base 'DBIx::Class::CDBICompat::ImaDBI';
+
 use Carp;
+use DBIx::Class::_Util qw(quote_sub perlstring);
 
 __PACKAGE__->set_sql(Retrieve => <<'');
 SELECT __ESSENTIAL__
@@ -17,17 +16,16 @@ WHERE  %s
 
 sub add_constructor {
     my ($class, $method, $fragment) = @_;
-    return croak("constructors needs a name") unless $method;
 
-    no strict 'refs';
-    my $meth = "$class\::$method";
-    return carp("$method already exists in $class")
-            if *$meth{CODE};
+    croak("constructors needs a name") unless $method;
+
+    carp("$method already exists in $class") && return
+       if $class->can($method);
 
-    *$meth = Sub::Name::subname $meth => sub {
-            my $self = shift;
-            $self->sth_to_objects($self->sql_Retrieve($fragment), \@_);
-    };
+    quote_sub "${class}::${method}" => sprintf( <<'EOC', perlstring $fragment );
+      my $self = shift;
+      $self->sth_to_objects($self->sql_Retrieve(%s), \@_);
+EOC
 }
 
 1;
index 0ab6092..77e7b5b 100644 (file)
@@ -33,4 +33,17 @@ sub copy {
     return $self->next::method({ $primary_columns[0] => $arg });
 }
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
+=cut
+
 1;
index 115bf3d..61d243c 100644 (file)
@@ -3,8 +3,12 @@ package # hide from PAUSE
 
 use strict;
 use warnings;
+use DBIx::Class::_Util 'detected_reinvoked_destructor';
+use namespace::clean;
 
 sub DESTROY {
+  return if &detected_reinvoked_destructor;
+
   my ($self) = @_;
   my $class = ref $self;
   warn "$class $self destroyed without saving changes to "
index aaa19a0..0ec6993 100644 (file)
@@ -4,66 +4,13 @@ package # hide from PAUSE
 use strict;
 use warnings;
 use DBIx::ContextualFetch;
-use Sub::Name ();
+use DBIx::Class::_Util qw(quote_sub perlstring);
 
 use base qw(Class::Data::Inheritable);
 
 __PACKAGE__->mk_classdata('sql_transformer_class' =>
                           'DBIx::Class::CDBICompat::SQLTransformer');
 
-__PACKAGE__->mk_classdata('_transform_sql_handler_order'
-                            => [ qw/TABLE ESSENTIAL JOIN IDENTIFIER/ ] );
-
-__PACKAGE__->mk_classdata('_transform_sql_handlers' =>
-  {
-    'TABLE' =>
-      sub {
-        my ($self, $class, $data) = @_;
-        return $class->result_source_instance->name unless $data;
-        my ($f_class, $alias) = split(/=/, $data);
-        $f_class ||= $class;
-        $self->{_classes}{$alias} = $f_class;
-        return $f_class->result_source_instance->name." ${alias}";
-      },
-    'ESSENTIAL' =>
-      sub {
-        my ($self, $class, $data) = @_;
-        $class = $data ? $self->{_classes}{$data} : $class;
-        return join(', ', $class->columns('Essential'));
-      },
-    'IDENTIFIER' =>
-      sub {
-        my ($self, $class, $data) = @_;
-        $class = $data ? $self->{_classes}{$data} : $class;
-        return join ' AND ', map  "$_ = ?", $class->primary_columns;
-      },
-    'JOIN' =>
-      sub {
-        my ($self, $class, $data) = @_;
-        my ($from, $to) = split(/ /, $data);
-        my ($from_class, $to_class) = @{$self->{_classes}}{$from, $to};
-        my ($rel_obj) = grep { $_->{class} && $_->{class} eq $to_class }
-                          map { $from_class->relationship_info($_) }
-                            $from_class->relationships;
-        unless ($rel_obj) {
-          ($from, $to) = ($to, $from);
-          ($from_class, $to_class) = ($to_class, $from_class);
-          ($rel_obj) = grep { $_->{class} && $_->{class} eq $to_class }
-                          map { $from_class->relationship_info($_) }
-                            $from_class->relationships;
-        }
-        $self->throw_exception( "No relationship to JOIN from ${from_class} to ${to_class}" )
-          unless $rel_obj;
-        my $join = $from_class->storage->sql_maker->_join_condition(
-          scalar $from_class->result_source_instance->_resolve_condition(
-            $rel_obj->{cond}, $to, $from
-          )
-        );
-        return $join;
-      }
-
-  } );
-
 sub db_Main {
   return $_[0]->storage->dbh;
 }
@@ -81,26 +28,20 @@ sub __driver {
 
 sub set_sql {
   my ($class, $name, $sql) = @_;
-  no strict 'refs';
-  my $sql_name = "sql_${name}";
-  my $full_sql_name = join '::', $class, $sql_name;
-  *$full_sql_name = Sub::Name::subname $full_sql_name,
-    sub {
-      my $sql = $sql;
-      my $class = shift;
-      return $class->storage->dbh_do(
-        _prepare_sth => $class->transform_sql($sql, @_)
-      );
-    };
-  if ($sql =~ /select/i) {
-    my $search_name = "search_${name}";
-    my $full_search_name = join '::', $class, $search_name;
-    *$full_search_name = Sub::Name::subname $full_search_name,
-      sub {
-        my ($class, @args) = @_;
-        my $sth = $class->$sql_name;
-        return $class->sth_to_objects($sth, \@args);
-      };
+
+  quote_sub "${class}::sql_${name}", sprintf( <<'EOC', perlstring $sql );
+    my $class = shift;
+    return $class->storage->dbh_do(
+      _prepare_sth => $class->transform_sql(%s, @_)
+    );
+EOC
+
+
+  if ($sql =~ /select/i) {  # FIXME - this should be anchore surely...?
+    quote_sub "${class}::search_${name}", sprintf( <<'EOC', "sql_$name" );
+      my ($class, @args) = @_;
+      $class->sth_to_objects( $class->%s, \@args);
+EOC
   }
 }
 
index eb60177..86a3838 100644 (file)
@@ -32,9 +32,21 @@ sub _init_result_source_instance {
   return $table;
 }
 
+=head1 FURTHER QUESTIONS?
 
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-package DBIx::Class::CDBICompat::Iterator::ResultSet;
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
+=cut
+
+package # hide
+  DBIx::Class::CDBICompat::Iterator::ResultSet;
 
 use strict;
 use warnings;
index 5dd6268..f3c472d 100644 (file)
@@ -31,4 +31,17 @@ sub remove_from_object_index {}
 
 sub clear_object_index {}
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
+=cut
+
 1;
index 36fbce9..203b598 100644 (file)
@@ -2,6 +2,10 @@ package # hide from PAUSE
     DBIx::Class::CDBICompat::Pager;
 
 use strict;
+
+# even though fatalization has been proven over and over to be a universally
+# bad idea, this line has been part of the code from the beginning
+# leaving the compat layer as-is, something may in fact depend on that
 use warnings FATAL => 'all';
 
 *pager = \&page;
index b0c10fa..95e414d 100644 (file)
@@ -3,7 +3,8 @@ package
 
 use strict;
 use warnings;
-use Sub::Name ();
+
+use DBIx::Class::_Util 'quote_sub';
 
 =head1 NAME
 
@@ -23,20 +24,26 @@ my %method2key = (
     args            => 'args',
 );
 
+quote_sub __PACKAGE__ . "::$_" => "\$_[0]->{$method2key{$_}}"
+  for keys %method2key;
+
 sub new {
     my($class, $args) = @_;
 
     return bless $args, $class;
 }
 
-for my $method (keys %method2key) {
-    my $key = $method2key{$method};
-    my $code = sub {
-        $_[0]->{$key};
-    };
+=head1 FURTHER QUESTIONS?
 
-    no strict 'refs';
-    *{$method} = Sub::Name::subname $method, $code;
-}
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
+=cut
 
 1;
index 58b29e0..8d923b3 100644 (file)
@@ -3,11 +3,12 @@ package # hide from PAUSE
 
 use strict;
 use warnings;
-use Sub::Name ();
-use base qw/Class::Data::Inheritable/;
+use base 'Class::Data::Inheritable';
 
 use Clone;
 use DBIx::Class::CDBICompat::Relationship;
+use Scalar::Util 'blessed';
+use DBIx::Class::_Util qw(quote_sub perlstring);
 
 __PACKAGE__->mk_classdata('__meta_info' => {});
 
@@ -40,6 +41,13 @@ sub _declare_has_a {
 
   my $rel_info;
 
+  # Class::DBI allows Non database has_a with implicit deflate and inflate
+  # Hopefully the following will catch Non-database tables.
+  if( !$f_class->isa('DBIx::Class::Row') and !$f_class->isa('Class::DBI::Row') ) {
+    $args{'inflate'} ||= sub { $f_class->new(shift) }; # implicit inflate by calling new
+    $args{'deflate'} ||= sub { shift() . '' }; # implicit deflate by stringification
+  }
+
   if ($args{'inflate'} || $args{'deflate'}) { # Non-database has_a
     if (!ref $args{'inflate'}) {
       my $meth = $args{'inflate'};
@@ -119,19 +127,14 @@ sub has_many {
   );
 
   if (@f_method) {
-    no strict 'refs';
-    no warnings 'redefine';
-    my $post_proc = sub { my $o = shift; $o = $o->$_ for @f_method; $o; };
-    my $name = join '::', $class, $rel;
-    *$name = Sub::Name::subname $name,
-      sub {
-        my $rs = shift->search_related($rel => @_);
-        $rs->{attrs}{record_filter} = $post_proc;
-        return (wantarray ? $rs->all : $rs);
-      };
+    quote_sub "${class}::${rel}", sprintf( <<'EOC', perlstring $rel), { '$rf' => \sub { my $o = shift; $o = $o->$_ for @f_method; $o } };
+      my $rs = shift->search_related( %s => @_);
+      $rs->{attrs}{record_filter} = $rf;
+      return (wantarray ? $rs->all : $rs);
+EOC
+
     return 1;
   }
-
 }
 
 
@@ -160,14 +163,19 @@ sub might_have {
 
 sub _extend_meta {
     my ($class, $type, $rel, $val) = @_;
-    my %hash = %{ Clone::clone($class->__meta_info || {}) };
+
+### Explicitly not using the deep cloner as Clone exhibits specific behavior
+### wrt CODE references - it simply passes them as-is to the new structure
+### (without deparse/eval cycles). There likely is code that relies on this
+### so we just let sleeping dogs lie.
+    my $hash = Clone::clone($class->__meta_info || {});
 
     $val->{self_class} = $class;
     $val->{type}       = $type;
     $val->{accessor}   = $rel;
 
-    $hash{$type}{$rel} = DBIx::Class::CDBICompat::Relationship->new($val);
-    $class->__meta_info(\%hash);
+    $hash->{$type}{$rel} = DBIx::Class::CDBICompat::Relationship->new($val);
+    $class->__meta_info($hash);
 }
 
 
@@ -193,11 +201,31 @@ sub search {
                   : undef());
   if (ref $where eq 'HASH') {
     foreach my $key (keys %$where) { # has_a deflation hack
-      $where->{$key} = ''.$where->{$key}
-        if eval { $where->{$key}->isa('DBIx::Class') };
+      $where->{$key} = ''.$where->{$key} if (
+        defined blessed $where->{$key}
+          and
+        $where->{$key}->isa('DBIx::Class')
+      );
     }
   }
   $self->next::method($where, $attrs);
 }
 
+sub new_related {
+  return shift->search_related(shift)->new_result(shift);
+}
+
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
+=cut
+
 1;
index 34be5f3..87f5318 100644 (file)
@@ -2,8 +2,11 @@ package # hide from PAUSE
     DBIx::Class::CDBICompat::Retrieve;
 
 use strict;
-use warnings FATAL => 'all';
 
+# even though fatalization has been proven over and over to be a universally
+# bad idea, this line has been part of the code from the beginning
+# leaving the compat layer as-is, something may in fact depend on that
+use warnings FATAL => 'all';
 
 sub retrieve {
   my $self = shift;
index 711c464..fd54b7e 100644 (file)
@@ -100,5 +100,17 @@ sub _do_transformation {
     return 1;
 }
 
-1;
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
+=cut
+
+1;
index 69eab47..fbd37e5 100644 (file)
@@ -21,7 +21,7 @@ sub __find_caller {
   my $fr_num = 1; # skip us and the calling carp*
 
   my (@f, $origin);
-  while (@f = caller($fr_num++)) {
+  while (@f = CORE::caller($fr_num++)) {
 
     next if
       ( $f[3] eq '(eval)' or $f[3] =~ /::__ANON__$/ );
@@ -33,9 +33,9 @@ sub __find_caller {
         and
 #############################
 # Need a way to parameterize this for Carp::Skip
-      $1 !~ /^(?: DBIx::Class::Storage::BlockRunner | Context::Preserve | Try::Tiny | Class::Accessor::Grouped | Class::C3::Componentised | Module::Runtime )$/x
+      $1 !~ /^(?: DBIx::Class::Storage::BlockRunner | Context::Preserve | Try::Tiny | Class::Accessor::Grouped | Class::C3::Componentised | Module::Runtime | Sub::Uplevel )$/x
         and
-      $2 !~ /^(?: throw_exception | carp | carp_unique | carp_once | dbh_do | txn_do | with_deferred_fk_checks)$/x
+      $2 !~ /^(?: throw_exception | carp | carp_unique | carp_once | dbh_do | txn_do | with_deferred_fk_checks | __delicate_rollback | dbic_internal_try )$/x
 #############################
     ) ? $f[3] : undef;
 
@@ -54,11 +54,15 @@ sub __find_caller {
     ? "at $f[1] line $f[2]"
     : Carp::longmess()
   ;
-  $origin ||= '{UNKNOWN}';
 
   return (
     $site,
-    $origin =~ /::/ ? "$origin(): " : "$origin: ", # cargo-cult from Carp::Clan
+    (
+      # cargo-cult from Carp::Clan
+      ! defined $origin   ? ''
+    : $origin =~ /::/     ? "$origin(): "
+                          : "$origin: "
+    ),
   );
 };
 
@@ -127,6 +131,8 @@ sub unimport {
 
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::Carp - Provides advanced Carp::Clan-like warning functions for DBIx::Class internals
@@ -179,4 +185,15 @@ same ruleset as L</carp>).
 Like L</carp> but warns only once for the life of the perl interpreter
 (regardless of callsite).
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
 =cut
index 39407dc..9c6b306 100644 (file)
@@ -16,6 +16,8 @@ __PACKAGE__->load_components(qw/
 
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::Core - Core set of DBIx::Class modules
@@ -51,12 +53,13 @@ The core modules currently are:
 A better overview of the methods found in a Result class can be found
 in L<DBIx::Class::Manual::ResultClass>.
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 2031ac4..95cbe55 100644 (file)
@@ -81,4 +81,17 @@ sub all {
   return @all;
 }
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
+=cut
+
 1;
index 9f12a98..b7e5394 100644 (file)
@@ -267,13 +267,16 @@ Alias for L</txn_rollback>
 
 =end hidden
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 07f587d..a5e9945 100644 (file)
@@ -40,8 +40,8 @@ overload fallback to give natural boolean/numeric values.
 This is meant for internal use by L<DBIx::Class>'s C<throw_exception>
 code, and shouldn't be used directly elsewhere.
 
-Expects a scalar exception message.  The optional argument
-C<$stacktrace> tells it to output a full trace similar to L<Carp/confess>.
+Expects a scalar exception message. The optional boolean C<$stacktrace>
+causes it to output a full trace similar to L<confess|Carp/DESCRIPTION>.
 
   DBIx::Class::Exception->throw('Foo');
   try { ... } catch { DBIx::Class::Exception->throw(shift) }
@@ -61,7 +61,7 @@ sub throw {
         # skip all frames that match the original caller, or any of
         # the dbic-wide classdata patterns
         my ($ln, $calling) = DBIx::Class::Carp::__find_caller(
-          '^' . caller() . '$',
+          '^' . CORE::caller() . '$',
           'DBIx::Class',
         );
 
@@ -88,13 +88,16 @@ sub rethrow {
     die shift;
 }
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index cee647e..fedbf79 100644 (file)
@@ -2,16 +2,17 @@ package DBIx::Class::FilterColumn;
 use strict;
 use warnings;
 
-use base qw/DBIx::Class::Row/;
+use base 'DBIx::Class::Row';
+use SQL::Abstract 'is_literal_value';
+use namespace::clean;
 
 sub filter_column {
   my ($self, $col, $attrs) = @_;
 
   my $colinfo = $self->column_info($col);
 
-  $self->throw_exception('FilterColumn does not work with InflateColumn')
-    if $self->isa('DBIx::Class::InflateColumn') &&
-      defined $colinfo->{_inflate_info};
+  $self->throw_exception("FilterColumn can not be used on a column with a declared InflateColumn inflator")
+    if defined $colinfo->{_inflate_info} and $self->isa('DBIx::Class::InflateColumn');
 
   $self->throw_exception("No such column $col to filter")
     unless $self->has_column($col);
@@ -31,9 +32,9 @@ sub filter_column {
 sub _column_from_storage {
   my ($self, $col, $value) = @_;
 
-  return $value unless defined $value;
+  return $value if is_literal_value($value);
 
-  my $info = $self->column_info($col)
+  my $info = $self->result_source->column_info($col)
     or $self->throw_exception("No column info for $col");
 
   return $value unless exists $info->{_filter_info};
@@ -46,7 +47,9 @@ sub _column_from_storage {
 sub _column_to_storage {
   my ($self, $col, $value) = @_;
 
-  my $info = $self->column_info($col) or
+  return $value if is_literal_value($value);
+
+  my $info = $self->result_source->column_info($col) or
     $self->throw_exception("No column info for $col");
 
   return $value unless exists $info->{_filter_info};
@@ -60,20 +63,25 @@ sub get_filtered_column {
   my ($self, $col) = @_;
 
   $self->throw_exception("$col is not a filtered column")
-    unless exists $self->column_info($col)->{_filter_info};
+    unless exists $self->result_source->column_info($col)->{_filter_info};
 
   return $self->{_filtered_column}{$col}
     if exists $self->{_filtered_column}{$col};
 
   my $val = $self->get_column($col);
 
-  return $self->{_filtered_column}{$col} = $self->_column_from_storage($col, $val);
+  return $self->{_filtered_column}{$col} = $self->_column_from_storage(
+    $col, $val
+  );
 }
 
 sub get_column {
   my ($self, $col) = @_;
+
   if (exists $self->{_filtered_column}{$col}) {
-    return $self->{_column_data}{$col} ||= $self->_column_to_storage ($col, $self->{_filtered_column}{$col});
+    return $self->{_column_data}{$col} ||= $self->_column_to_storage (
+      $col, $self->{_filtered_column}{$col}
+    );
   }
 
   return $self->next::method ($col);
@@ -83,10 +91,12 @@ sub get_column {
 sub get_columns {
   my $self = shift;
 
-  foreach my $col (keys %{$self->{_filtered_column}||{}}) {
-    $self->{_column_data}{$col} ||= $self->_column_to_storage ($col, $self->{_filtered_column}{$col})
-      if exists $self->{_filtered_column}{$col};
-  }
+  $self->{_column_data}{$_} = $self->_column_to_storage (
+    $_, $self->{_filtered_column}{$_}
+  ) for grep
+    { ! exists $self->{_column_data}{$_} }
+    keys %{$self->{_filtered_column}||{}}
+  ;
 
   $self->next::method (@_);
 }
@@ -100,54 +110,65 @@ sub store_column {
   $self->next::method(@_);
 }
 
+sub has_column_loaded {
+  my ($self, $col) = @_;
+  return 1 if exists $self->{_filtered_column}{$col};
+  return $self->next::method($col);
+}
+
 sub set_filtered_column {
   my ($self, $col, $filtered) = @_;
 
-  # do not blow up the cache via set_column unless necessary
-  # (filtering may be expensive!)
-  if (exists $self->{_filtered_column}{$col}) {
-    return $filtered
-      if ($self->_eq_column_values ($col, $filtered, $self->{_filtered_column}{$col} ) );
-
-    $self->make_column_dirty ($col); # so the comparison won't run again
+  # unlike IC, FC does not need to deal with the 'filter' abomination
+  # thus we can short-curcuit filtering entirely and never call set_column
+  # in case this is already a dirty change OR the row never touched storage
+  if (
+    ! $self->in_storage
+      or
+    $self->is_column_changed($col)
+  ) {
+    $self->make_column_dirty($col);
+    delete $self->{_column_data}{$col};
   }
-
-  $self->set_column($col, $self->_column_to_storage($col, $filtered));
+  else {
+    $self->set_column($col, $self->_column_to_storage($col, $filtered));
+  };
 
   return $self->{_filtered_column}{$col} = $filtered;
 }
 
 sub update {
-  my ($self, $attrs, @rest) = @_;
+  my ($self, $data, @rest) = @_;
 
-  foreach my $key (keys %{$attrs||{}}) {
-    if (
-      $self->has_column($key)
-        &&
-      exists $self->column_info($key)->{_filter_info}
-    ) {
-      $self->set_filtered_column($key, delete $attrs->{$key});
+  my $colinfos = $self->result_source->columns_info;
+
+  foreach my $col (keys %{$data||{}}) {
+    if ( exists $colinfos->{$col}{_filter_info} ) {
+      $self->set_filtered_column($col, delete $data->{$col});
 
       # FIXME update() reaches directly into the object-hash
       # and we may *not* have a filtered value there - thus
       # the void-ctx filter-trigger
-      $self->get_column($key) unless exists $self->{_column_data}{$key};
+      $self->get_column($col) unless exists $self->{_column_data}{$col};
     }
   }
 
-  return $self->next::method($attrs, @rest);
+  return $self->next::method($data, @rest);
 }
 
 sub new {
-  my ($class, $attrs, @rest) = @_;
-  my $source = $attrs->{-result_source}
+  my ($class, $data, @rest) = @_;
+
+  my $rsrc = $data->{-result_source}
     or $class->throw_exception('Sourceless rows are not supported with DBIx::Class::FilterColumn');
 
-  my $obj = $class->next::method($attrs, @rest);
-  foreach my $key (keys %{$attrs||{}}) {
-    if ($obj->has_column($key) &&
-          exists $obj->column_info($key)->{_filter_info} ) {
-      $obj->set_filtered_column($key, $attrs->{$key});
+  my $obj = $class->next::method($data, @rest);
+
+  my $colinfos = $rsrc->columns_info;
+
+  foreach my $col (keys %{$data||{}}) {
+    if (exists $colinfos->{$col}{_filter_info} ) {
+      $obj->set_filtered_column($col, $data->{$col});
     }
   }
 
@@ -156,6 +177,8 @@ sub new {
 
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::FilterColumn - Automatically convert column data
@@ -240,3 +263,14 @@ and one, using code like this:-
 
 In this case the C<filter_from_storage> is not required, as just
 passing the database value through to perl does the right thing.
+
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 9214582..27bde58 100644 (file)
@@ -3,7 +3,9 @@ package DBIx::Class::InflateColumn;
 use strict;
 use warnings;
 
-use base qw/DBIx::Class::Row/;
+use base 'DBIx::Class::Row';
+use SQL::Abstract 'is_literal_value';
+use namespace::clean;
 
 =head1 NAME
 
@@ -87,9 +89,8 @@ sub inflate_column {
 
   my $colinfo = $self->column_info($col);
 
-  $self->throw_exception("InflateColumn does not work with FilterColumn")
-    if $self->isa('DBIx::Class::FilterColumn') &&
-      defined $colinfo->{_filter_info};
+  $self->throw_exception("InflateColumn can not be used on a column with a declared FilterColumn filter")
+    if defined $colinfo->{_filter_info} and $self->isa('DBIx::Class::FilterColumn');
 
   $self->throw_exception("No such column $col to inflate")
     unless $self->has_column($col);
@@ -103,26 +104,45 @@ sub inflate_column {
 
 sub _inflated_column {
   my ($self, $col, $value) = @_;
-  return $value unless defined $value; # NULL is NULL is NULL
-  my $info = $self->column_info($col)
+
+  return $value if (
+    ! defined $value # NULL is NULL is NULL
+      or
+    is_literal_value($value) #that would be a not-yet-reloaded literal update
+  );
+
+  my $info = $self->result_source->column_info($col)
     or $self->throw_exception("No column info for $col");
+
   return $value unless exists $info->{_inflate_info};
-  my $inflate = $info->{_inflate_info}{inflate};
-  $self->throw_exception("No inflator for $col") unless defined $inflate;
-  return $inflate->($value, $self);
+
+  return (
+    $info->{_inflate_info}{inflate}
+      ||
+    $self->throw_exception("No inflator found for '$col'")
+  )->($value, $self);
 }
 
 sub _deflated_column {
   my ($self, $col, $value) = @_;
-#  return $value unless ref $value && blessed($value); # If it's not an object, don't touch it
-  ## Leave scalar refs (ala SQL::Abstract literal SQL), untouched, deflate all other refs
-  return $value unless (ref $value && ref($value) ne 'SCALAR');
-  my $info = $self->column_info($col) or
+
+  ## Deflate any refs except for literals, pass through plain values
+  return $value if (
+    ! length ref $value
+      or
+    is_literal_value($value)
+  );
+
+  my $info = $self->result_source->column_info($col) or
     $self->throw_exception("No column info for $col");
+
   return $value unless exists $info->{_inflate_info};
-  my $deflate = $info->{_inflate_info}{deflate};
-  $self->throw_exception("No deflator for $col") unless defined $deflate;
-  return $deflate->($value, $self);
+
+  return (
+    $info->{_inflate_info}{deflate}
+      ||
+    $self->throw_exception("No deflator found for '$col'")
+  )->($value, $self);
 }
 
 =head2 get_inflated_column
@@ -138,13 +158,15 @@ Throws an exception if the column requested is not an inflated column.
 
 sub get_inflated_column {
   my ($self, $col) = @_;
+
   $self->throw_exception("$col is not an inflated column")
-    unless exists $self->column_info($col)->{_inflate_info};
+    unless exists $self->result_source->column_info($col)->{_inflate_info};
+
+  # we take care of keeping things in sync
   return $self->{_inflated_column}{$col}
     if exists $self->{_inflated_column}{$col};
 
   my $val = $self->get_column($col);
-  return $val if ref $val eq 'SCALAR';  #that would be a not-yet-reloaded sclarref update
 
   return $self->{_inflated_column}{$col} = $self->_inflated_column($col, $val);
 }
@@ -159,15 +181,22 @@ analogous to L<DBIx::Class::Row/set_column>.
 =cut
 
 sub set_inflated_column {
-  my ($self, $col, $inflated) = @_;
-  $self->set_column($col, $self->_deflated_column($col, $inflated));
-#  if (blessed $inflated) {
-  if (ref $inflated && ref($inflated) ne 'SCALAR') {
-    $self->{_inflated_column}{$col} = $inflated;
-  } else {
+  my ($self, $col, $value) = @_;
+
+  # pass through deflated stuff
+  if (! length ref $value or is_literal_value($value)) {
+    $self->set_column($col, $value);
     delete $self->{_inflated_column}{$col};
   }
-  return $inflated;
+  # need to call set_column with the deflate cycle so that
+  # relationship caches are nuked if any
+  # also does the compare-for-dirtyness and change tracking dance
+  else {
+    $self->set_column($col, $self->_deflated_column($col, $value));
+    $self->{_inflated_column}{$col} = $value;
+  }
+
+  return $value;
 }
 
 =head2 store_inflated_column
@@ -180,15 +209,18 @@ as dirty. This is directly analogous to L<DBIx::Class::Row/store_column>.
 =cut
 
 sub store_inflated_column {
-  my ($self, $col, $inflated) = @_;
-#  unless (blessed $inflated) {
-  unless (ref $inflated && ref($inflated) ne 'SCALAR') {
-      delete $self->{_inflated_column}{$col};
-      $self->store_column($col => $inflated);
-      return $inflated;
+  my ($self, $col, $value) = @_;
+
+  if (! length ref $value or is_literal_value($value)) {
+    delete $self->{_inflated_column}{$col};
+    $self->store_column($col => $value);
   }
-  delete $self->{_column_data}{$col};
-  return $self->{_inflated_column}{$col} = $inflated;
+  else {
+    delete $self->{_column_data}{$col};
+    $self->{_inflated_column}{$col} = $value;
+  }
+
+  return $value;
 }
 
 =head1 SEE ALSO
@@ -201,19 +233,16 @@ sub store_inflated_column {
 
 =back
 
-=head1 AUTHOR
-
-Matt S. Trout <mst@shadowcatsystems.co.uk>
-
-=head1 CONTRIBUTORS
-
-Daniel Westermann-Clark <danieltwc@cpan.org> (documentation)
+=head1 FURTHER QUESTIONS?
 
-Jess Robinson <cpan@desert-island.demon.co.uk>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 3162223..8ccdf7a 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use warnings;
 use base qw/DBIx::Class/;
 use DBIx::Class::Carp;
+use DBIx::Class::_Util 'dbic_internal_try';
 use Try::Tiny;
 use namespace::clean;
 
@@ -78,8 +79,8 @@ deflation/inflation as defined in the storage class. For instance, for
 a C<datetime> field the methods C<parse_datetime> and C<format_datetime>
 would be called on deflation/inflation. If the storage class does not
 provide a specialized inflator/deflator, C<[parse|format]_datetime> will
-be used as a fallback. See L<DateTime::Format> for more information on
-date formatting.
+be used as a fallback. See L<DateTime/Formatters And Stringification>
+for more information on date formatting.
 
 For more help with using components, see L<DBIx::Class::Manual::Component/USING>.
 
@@ -197,7 +198,7 @@ sub _flate_or_fallback
   my $preferred_method = sprintf($method_fmt, $info->{ _ic_dt_method });
   my $method = $parser->can($preferred_method) || sprintf($method_fmt, 'datetime');
 
-  return try {
+  return dbic_internal_try {
     $parser->$method($value);
   }
   catch {
@@ -310,15 +311,13 @@ use the old way you'll see a warning - please fix your code then!
 
 =back
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-Matt S. Trout <mst@shadowcatsystems.co.uk>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 CONTRIBUTORS
-
-Aran Deltac <bluefeet@cpan.org>
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index aa06dbc..3a515a8 100644 (file)
@@ -43,10 +43,14 @@ sub register_column {
 sub _file_column_file {
     my ($self, $column, $filename) = @_;
 
-    my $column_info = $self->column_info($column);
+    my $column_info = $self->result_source->column_info($column);
 
     return unless $column_info->{is_file_column};
 
+    # DO NOT CHANGE
+    # This call to id() is generally incorrect - will not DTRT on
+    # multicolumn key. However changing this may introduce
+    # backwards-comp regressions, thus leaving as is
     my $id = $self->id || $self->throw_exception(
         'id required for filename generation'
     );
@@ -60,8 +64,10 @@ sub _file_column_file {
 sub delete {
     my ( $self, @rest ) = @_;
 
-    for ( $self->columns ) {
-        if ( $self->column_info($_)->{is_file_column} ) {
+    my $colinfos = $self->result_source->columns_info;
+
+    for ( keys %$colinfos ) {
+        if ( $colinfos->{$_}{is_file_column} ) {
             rmtree( [$self->_file_column_file($_)->dir], 0, 0 );
             last; # if we've deleted one, we've deleted them all
         }
@@ -75,9 +81,11 @@ sub insert {
 
     # cache our file columns so we can write them to the fs
     # -after- we have a PK
+    my $colinfos = $self->result_source->columns_info;
+
     my %file_column;
-    for ( $self->columns ) {
-        if ( $self->column_info($_)->{is_file_column} ) {
+    for ( keys %$colinfos ) {
+        if ( $colinfos->{$_}{is_file_column} ) {
             $file_column{$_} = $self->$_;
             $self->store_column($_ => $self->$_->{filename});
         }
@@ -206,14 +214,16 @@ Method made to be overridden for callback purposes.
 
 sub _file_column_callback {}
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-Victor Igumnov
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-This library is free software, you can redistribute it and/or modify
-it under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 60cced0..9b1761f 100644 (file)
@@ -55,5 +55,13 @@ documentation. It should behave the same way.
 Existing components, and documentation and example on how to
 develop new ones.
 
-=cut
+=head1 FURTHER QUESTIONS?
 
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 6865aac..07142d9 100644 (file)
@@ -132,6 +132,13 @@ L<DBIx::Class::Row> - Basic row methods.
 
 L<DBIx::Class::Manual::Cookbook>
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-Aran Clary Deltac <bluefeet@cpan.org>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 0cb560b..324ff64 100644 (file)
@@ -146,8 +146,9 @@ Next, you can execute your complex query using bind parameters like this:
   );
 
 ... and you'll get back a perfect L<DBIx::Class::ResultSet> (except, of course,
-that you cannot modify the rows it contains, e.g. cannot call L</update>,
-L</delete>, ...  on it).
+that you cannot modify the rows it contains, e.g. cannot call
+L<update|DBIx::Class::ResultSet/update> or
+L<delete|DBIx::Class::ResultSet/delete> on it).
 
 Note that you cannot have bind parameters unless is_virtual is set to true.
 
@@ -448,8 +449,8 @@ See also L<SQL::Abstract/Literal SQL with placeholders and bind values
 =head2 Software Limits
 
 When your RDBMS does not have a working SQL limit mechanism (e.g. Sybase ASE)
-and L<GenericSubQ|SQL::Abstract::Limit/GenericSubQ> is either too slow or does
-not work at all, you can try the
+and L<GenericSubQ|DBIx::Class::SQLMaker::LimitDialects/GenericSubQ> is either
+too slow or does not work at all, you can try the
 L<software_limit|DBIx::Class::ResultSet/software_limit>
 L<DBIx::Class::ResultSet> attribute, which skips over records to simulate limits
 in the Perl layer.
@@ -1065,7 +1066,7 @@ See L<DBIx::Class::ResultSetColumn> for more documentation.
 
 Sometimes you have a (set of) result objects that you want to put into a
 resultset without the need to hit the DB again. You can do that by using the
-L<set_cache|DBIx::Class::Resultset/set_cache> method:
+L<set_cache|DBIx::Class::ResultSet/set_cache> method:
 
  my @uploadable_groups;
  while (my $group = $groups->next) {
@@ -1341,7 +1342,7 @@ row.
 
           # Abort the whole job
           if ($_ =~ /horrible_problem/) {
-            print "something horrible happend, aborting job!";
+            print "something horrible happened, aborting job!";
             die $_;                # rethrow error
           }
 
@@ -1380,9 +1381,11 @@ row.
   }
 
 In this example it might be hard to see where the rollbacks, releases and
-commits are happening, but it works just the same as for plain L<<txn_do>>: If
-the C<try>-block around C<txn_do> fails, a rollback is issued. If the C<try>
-succeeds, the transaction is committed (or the savepoint released).
+commits are happening, but it works just the same as for plain
+L<txn_do|DBIx::Class::Storage/txn_do>: If the L<try|Try::Tiny/try>-block
+around L<txn_do|DBIx::Class::Storage/txn_do> fails, a rollback is issued.
+If the L<try|Try::Tiny/try> succeeds, the transaction is committed
+(or the savepoint released).
 
 While you can get more fine-grained control using C<svp_begin>, C<svp_release>
 and C<svp_rollback>, it is strongly recommended to use C<txn_do> with coderefs.
@@ -1734,30 +1737,30 @@ L<DBIx::Class::ResultSet/create> and L<DBIx::Class::Row/update> family of
 methods:
 
   $resultset->create({
-    numbers => [1, 2, 3]
+    numbers => [1, 2, 3],
   });
 
-  $result->update(
-    {
-      numbers => [1, 2, 3]
-    },
-    {
-      day => '2008-11-24'
-    }
-  );
+  $result->update({
+    numbers => [1, 2, 3],
+  });
 
 In conditions (e.g. C<\%cond> in the L<DBIx::Class::ResultSet/search> family of
 methods) you cannot directly use array references (since this is interpreted as
 a list of values to be C<OR>ed), but you can use the following syntax to force
 passing them as bind values:
 
-  $resultset->search(
-    {
-      numbers => \[ '= ?', [numbers => [1, 2, 3]] ]
-    }
-  );
+  $resultset->search({
+    numbers => { -value => [1, 2, 3] },
+  });
+
+Or using the more generic (and more cumbersome) literal syntax:
+
+  $resultset->search({
+    numbers => \[ '= ?', [ numbers => [1, 2, 3] ] ]
+  });
+
 
-See L<SQL::Abstract/array_datatypes> and L<SQL::Abstract/Literal SQL with
+See L<SQL::Abstract/-value> and L<SQL::Abstract/Literal SQL with
 placeholders and bind values (subqueries)> for more explanation. Note that
 L<DBIx::Class> sets L<SQL::Abstract/bindtype> to C<columns>, so you must pass
 the bind values (the C<[1, 2, 3]> arrayref in the above example) wrapped in
@@ -1767,11 +1770,11 @@ C<< [column_name => value] >>.
 =head2 Formatting DateTime objects in queries
 
 To ensure C<WHERE> conditions containing L<DateTime> arguments are properly
-formatted to be understood by your RDBMS, you must use the C<DateTime>
+formatted to be understood by your RDBMS, you must use the L<DateTime>
 formatter returned by L<DBIx::Class::Storage::DBI/datetime_parser> to format
 any L<DateTime> objects you pass to L<search|DBIx::Class::ResultSet/search>
 conditions. Any L<Storage|DBIx::Class::Storage> object attached to your
-L<Schema|DBIx::Class::Schema> provides a correct C<DateTime> formatter, so
+L<Schema|DBIx::Class::Schema> provides a correct L<DateTime> formatter, so
 all you have to do is:
 
   my $dtf = $schema->storage->datetime_parser;
@@ -1790,12 +1793,11 @@ Without doing this the query will contain the simple stringification of the
 C<DateTime> object, which almost never matches the RDBMS expectations.
 
 This kludge is necessary only for conditions passed to
-L<DBIx::Class::ResultSet/search>, whereas
-L<create|DBIx::Class::ResultSet/create>,
-L<find|DBIx::Class::ResultSet/find>,
-L<DBIx::Class::Row/update> (but not L<DBIx::Class::ResultSet/update>) are all
+L<search|DBIx::Class::ResultSet/search> and L<DBIx::Class::ResultSet/find>,
+whereas L<create|DBIx::Class::ResultSet/create> and
+L<DBIx::Class::Row/update> (but not L<DBIx::Class::ResultSet/update>) are
 L<DBIx::Class::InflateColumn>-aware and will do the right thing when supplied
-an inflated C<DateTime> object.
+an inflated L<DateTime> object.
 
 =head2 Using Unicode
 
@@ -1840,7 +1842,7 @@ See L<DBD::mysql> for further details.
 =head3 Oracle
 
 Information about Oracle support for unicode can be found in
-L<DBD::Oracle/Unicode>.
+L<DBD::Oracle/UNICODE>.
 
 =head3 PostgreSQL
 
@@ -2202,10 +2204,9 @@ classes dynamically based on the database schema then there will be a
 significant startup delay.
 
 For production use a statically defined schema (which can be generated
-using L<DBIx::Class::Schema::Loader|DBIx::Class::Schema::Loader> to dump
-the database schema once - see
+using L<DBIx::Class::Schema::Loader> to dump the database schema once - see
 L<make_schema_at|DBIx::Class::Schema::Loader/make_schema_at> and
-L<dump_directory|DBIx::Class::Schema::Loader/dump_directory> for more
+L<dump_directory|DBIx::Class::Schema::Loader::Base/dump_directory> for more
 details on creating static schemas from a database).
 
 =head2 Move Common Startup into a Base Class
@@ -2251,10 +2252,11 @@ avoiding L<Module::Find|Module::Find>.
 
 =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:
+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/CachedKids> hash:
 
     # print all currently cached prepared statements
     print for keys %{$schema->storage->dbh->{CachedKids}};
@@ -2277,3 +2279,14 @@ You can accomplish this with L<Tie::Cache> or L<Tie::Cache::LRU>:
     });
 
 =cut
+
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index b6eaa25..0b4966b 100644 (file)
@@ -60,3 +60,15 @@ are used most often.
 =item L<DBIx::Class::InflateColumn> - Making objects out of your column values.
 
 =back
+
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
index 59b114e..3f9b882 100644 (file)
@@ -8,368 +8,108 @@ This tutorial will guide you through the process of setting up and
 testing a very basic CD database using SQLite, with DBIx::Class::Schema
 as the database frontend.
 
-The database consists of the following:
-
-  table 'artist' with columns:  artistid, name
-  table 'cd'     with columns:  cdid, artist, title, year
-  table 'track'  with columns:  trackid, cd, title
+The database structure is based on the following rules:
 
+  An artist can have many cds, and each cd belongs to just one artist.
+  A cd can have many tracks, and each track belongs to just one cd.
 
-And these rules exists:
+The database is implemented with the following:
 
-  one artist can have many cds
-  one cd belongs to one artist
-  one cd can have many tracks
-  one track belongs to one cd
+  table 'artist' with columns:  artistid, name
+  table 'cd'     with columns:  cdid, artistid, title, year
+  table 'track'  with columns:  trackid, cdid, title
 
+Each of the table's first columns is the primary key; any subsequent
+keys are foreign keys.
 
 =head2 Installation
 
-Install DBIx::Class via CPAN should be sufficient.
-
-=head3 Create the database/tables
-
-First make and change the directory:
-
-  mkdir app
-  cd app
-  mkdir db
-  cd db
-
-This example uses SQLite which is a dependency of DBIx::Class, so you
-shouldn't have to install extra software.
-
-Save the following into a example.sql in the directory db
-
-  CREATE TABLE artist (
-    artistid INTEGER PRIMARY KEY,
-    name TEXT NOT NULL
-  );
-
-  CREATE TABLE cd (
-    cdid INTEGER PRIMARY KEY,
-    artist INTEGER NOT NULL REFERENCES artist(artistid),
-    title TEXT NOT NULL
-  );
-
-  CREATE TABLE track (
-    trackid INTEGER PRIMARY KEY,
-    cd INTEGER NOT NULL REFERENCES cd(cdid),
-    title TEXT NOT NULL
-  );
-
-and create the SQLite database file:
-
-  sqlite3 example.db < example.sql
-
-=head3 Set up DBIx::Class::Schema
+You'll need to install DBIx::Class via CPAN, and you'll also need to
+install sqlite3 (not sqlite) if it's not already intalled.
 
-Change directory back from db to the directory app:
+=head3 The database/tables/data
 
-  cd ../
+Your distribution already comes with a pre-filled SQLite database
+F<examples/Schema/db/example.db>. You can see it by e.g.
 
-Now create some more directories:
+  cpanm --look DBIx::Class
 
-  mkdir MyApp
-  mkdir MyApp/Schema
-  mkdir MyApp/Schema/Result
-  mkdir MyApp/Schema/ResultSet
+If for some reason the file is unreadable on your system, you can
+recreate it as follows:
 
-Then, create the following DBIx::Class::Schema classes:
+  cp -a <unpacked-DBIC-tarball>/examples/Schema dbicapp
+  cd dbicapp
+  rm db/example.db
+  sqlite3 db/example.db < db/example.sql
+  perl insertdb.pl
 
-MyApp/Schema.pm:
+=head3 Testing the database
 
-  package MyApp::Schema;
-  use base qw/DBIx::Class::Schema/;
-  __PACKAGE__->load_namespaces;
+Enter the example Schema directory
 
-  1;
+  cd <unpacked-DBIC-tarball>/examples/Schema
 
+Run the script testdb.pl, which will test that the database has
+successfully been filled.
 
-MyApp/Schema/Result/Artist.pm:
+When this script is run, it should output the following:
 
-  package MyApp::Schema::Result::Artist;
-  use base qw/DBIx::Class::Core/;
-  __PACKAGE__->table('artist');
-  __PACKAGE__->add_columns(qw/ artistid name /);
-  __PACKAGE__->set_primary_key('artistid');
-  __PACKAGE__->has_many('cds' => 'MyApp::Schema::Result::Cd');
+ get_tracks_by_cd(Bad):
+ Leave Me Alone
+ Smooth Criminal
+ Dirty Diana
 
-  1;
+ get_tracks_by_artist(Michael Jackson):
+ Billie Jean (from the CD 'Thriller')
+ Beat It (from the CD 'Thriller')
+ Leave Me Alone (from the CD 'Bad')
+ Smooth Criminal (from the CD 'Bad')
+ Dirty Diana (from the CD 'Bad')
 
+ get_cd_by_track(Stan):
+ The Marshall Mathers LP has the track 'Stan'.
 
-MyApp/Schema/Result/Cd.pm:
+ get_cds_by_artist(Michael Jackson):
+ Thriller
+ Bad
 
-  package MyApp::Schema::Result::Cd;
-  use base qw/DBIx::Class::Core/;
-  __PACKAGE__->load_components(qw/InflateColumn::DateTime/);
-  __PACKAGE__->table('cd');
-  __PACKAGE__->add_columns(qw/ cdid artist title year/);
-  __PACKAGE__->set_primary_key('cdid');
-  __PACKAGE__->belongs_to('artist' => 'MyApp::Schema::Result::Artist');
-  __PACKAGE__->has_many('tracks' => 'MyApp::Schema::Result::Track');
+ get_artist_by_track(Dirty Diana):
+ Michael Jackson recorded the track 'Dirty Diana'.
 
-  1;
+ get_artist_by_cd(The Marshall Mathers LP):
+ Eminem recorded the CD 'The Marshall Mathers LP'.
 
 
-MyApp/Schema/Result/Track.pm:
+=head3 Discussion about the results
 
-  package MyApp::Schema::Result::Track;
-  use base qw/DBIx::Class::Core/;
-  __PACKAGE__->table('track');
-  __PACKAGE__->add_columns(qw/ trackid cd title /);
-  __PACKAGE__->set_primary_key('trackid');
-  __PACKAGE__->belongs_to('cd' => 'MyApp::Schema::Result::Cd');
+The data model defined in this example has an artist with multiple CDs,
+and a CD with multiple tracks; thus, it's simple to traverse from a
+track back to a CD, and from there back to an artist. This is
+demonstrated in the get_tracks_by_artist routine, where we easily walk
+from the individual track back to the title of the CD that the track
+came from ($track->cd->title).
 
-  1;
-
-
-=head3 Write a script to insert some records
-
-insertdb.pl
-
-  #!/usr/bin/perl
-
-  use strict;
-  use warnings;
-
-  use MyApp::Schema;
-
-  my $schema = MyApp::Schema->connect('dbi:SQLite:db/example.db');
-
-  my @artists = (['Michael Jackson'], ['Eminem']);
-  $schema->populate('Artist', [
-     [qw/name/],
-     @artists,
-  ]);
-
-  my %albums = (
-    'Thriller' => 'Michael Jackson',
-    'Bad' => 'Michael Jackson',
-    'The Marshall Mathers LP' => 'Eminem',
-  );
-
-  my @cds;
-  foreach my $lp (keys %albums) {
-    my $artist = $schema->resultset('Artist')->find({
-      name => $albums{$lp}
-    });
-    push @cds, [$lp, $artist->id];
-  }
-
-  $schema->populate('Cd', [
-    [qw/title artist/],
-    @cds,
-  ]);
-
-
-  my %tracks = (
-    'Beat It'         => 'Thriller',
-    'Billie Jean'     => 'Thriller',
-    'Dirty Diana'     => 'Bad',
-    'Smooth Criminal' => 'Bad',
-    'Leave Me Alone'  => 'Bad',
-    'Stan'            => 'The Marshall Mathers LP',
-    'The Way I Am'    => 'The Marshall Mathers LP',
-  );
-
-  my @tracks;
-  foreach my $track (keys %tracks) {
-    my $cdname = $schema->resultset('Cd')->find({
-      title => $tracks{$track},
-    });
-    push @tracks, [$cdname->id, $track];
-  }
-
-  $schema->populate('Track',[
-    [qw/cd title/],
-    @tracks,
-  ]);
-
-=head3 Create and run the test scripts
-
-testdb.pl:
-
-  #!/usr/bin/perl
-
-  use strict;
-  use warnings;
-
-  use MyApp::Schema;
-
-  my $schema = MyApp::Schema->connect('dbi:SQLite:db/example.db');
-  # for other DSNs, e.g. MySQL, see the perldoc for the relevant dbd
-  # driver, e.g perldoc L<DBD::mysql>.
-
-  get_tracks_by_cd('Bad');
-  get_tracks_by_artist('Michael Jackson');
-
-  get_cd_by_track('Stan');
-  get_cds_by_artist('Michael Jackson');
-
-  get_artist_by_track('Dirty Diana');
-  get_artist_by_cd('The Marshall Mathers LP');
-
-
-  sub get_tracks_by_cd {
-    my $cdtitle = shift;
-    print "get_tracks_by_cd($cdtitle):\n";
-    my $rs = $schema->resultset('Track')->search(
-      {
-        'cd.title' => $cdtitle
-      },
-      {
-        join     => [qw/ cd /],
-      }
-    );
-    while (my $track = $rs->next) {
-      print $track->title . "\n";
-    }
-    print "\n";
-  }
-
-  sub get_tracks_by_artist {
-    my $artistname = shift;
-    print "get_tracks_by_artist($artistname):\n";
-    my $rs = $schema->resultset('Track')->search(
-      {
-        'artist.name' => $artistname
-      },
-      {
-        join => {
-          'cd' => 'artist'
-        },
-      }
-    );
-    while (my $track = $rs->next) {
-      print $track->title . "\n";
-    }
-    print "\n";
-  }
-
-
-  sub get_cd_by_track {
-    my $tracktitle = shift;
-    print "get_cd_by_track($tracktitle):\n";
-    my $rs = $schema->resultset('Cd')->search(
-      {
-        'tracks.title' => $tracktitle
-      },
-      {
-        join     => [qw/ tracks /],
-      }
-    );
-    my $cd = $rs->first;
-    print $cd->title . "\n\n";
-  }
-
-  sub get_cds_by_artist {
-    my $artistname = shift;
-    print "get_cds_by_artist($artistname):\n";
-    my $rs = $schema->resultset('Cd')->search(
-      {
-        'artist.name' => $artistname
-      },
-      {
-        join     => [qw/ artist /],
-      }
-    );
-    while (my $cd = $rs->next) {
-      print $cd->title . "\n";
-    }
-    print "\n";
-  }
-
-
-
-  sub get_artist_by_track {
-    my $tracktitle = shift;
-    print "get_artist_by_track($tracktitle):\n";
-    my $rs = $schema->resultset('Artist')->search(
-      {
-        'tracks.title' => $tracktitle
-      },
-      {
-        join => {
-          'cds' => 'tracks'
-        }
-      }
-    );
-    my $artist = $rs->first;
-    print $artist->name . "\n\n";
-  }
-
-  sub get_artist_by_cd {
-    my $cdtitle = shift;
-    print "get_artist_by_cd($cdtitle):\n";
-    my $rs = $schema->resultset('Artist')->search(
-      {
-        'cds.title' => $cdtitle
-      },
-      {
-        join     => [qw/ cds /],
-      }
-    );
-    my $artist = $rs->first;
-    print $artist->name . "\n\n";
-  }
-
-
-
-It should output:
-
-  get_tracks_by_cd(Bad):
-  Dirty Diana
-  Smooth Criminal
-  Leave Me Alone
-
-  get_tracks_by_artist(Michael Jackson):
-  Beat it
-  Billie Jean
-  Dirty Diana
-  Smooth Criminal
-  Leave Me Alone
-
-  get_cd_by_track(Stan):
-  The Marshall Mathers LP
-
-  get_cds_by_artist(Michael Jackson):
-  Thriller
-  Bad
-
-  get_artist_by_track(Dirty Diana):
-  Michael Jackson
-
-  get_artist_by_cd(The Marshall Mathers LP):
-  Eminem
-
-=head1 Notes
-
-A reference implementation of the database and scripts in this example
-are available in the main distribution for DBIx::Class under the
-directory F<examples/Schema>.
-
-With these scripts we're relying on @INC looking in the current
-working directory.  You may want to add the MyApp namespaces to
-@INC in a different way when it comes to deployment.
-
-The F<testdb.pl> script is an excellent start for testing your database
-model.
+Note also that in the get_tracks_by_cd and get_tracks_by_artist
+routines, the result set is called multiple times with the 'next'
+iterator.  In contrast, get_cd_by_track uses the 'first' result set
+method, since only one CD is expected to have a specific track.
 
 This example uses L<DBIx::Class::Schema/load_namespaces> to load in the
 appropriate L<Result|DBIx::Class::Manual::ResultClass> classes from the
 C<MyApp::Schema::Result> namespace, and any required
 L<ResultSet|DBIx::Class::ResultSet> classes from the
-C<MyApp::Schema::ResultSet> namespace (although we created the directory
-in the directions above we did not add, or need to add, any resultset
-classes).
+C<MyApp::Schema::ResultSet> namespace (although we did not add, nor needed
+any such classes in this example).
+
+=head1 FURTHER QUESTIONS?
 
-=head1 TODO
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 AUTHOR
+=head1 COPYRIGHT AND LICENSE
 
-  sc_ from irc.perl.org#dbix-class
-  Kieren Diment <kd@totaldatasolution.com>
-  Nigel Metheringham <nigelm@cpan.org>
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
index 71595d5..426233c 100644 (file)
@@ -58,7 +58,7 @@ L<DBIx::Class::Manual::Cookbook>.
 
 =item .. store/retrieve Unicode data in my database?
 
-Make sure you database supports Unicode and set the connect
+Make sure your database supports Unicode and set the connect
 attributes appropriately - see
 L<DBIx::Class::Manual::Cookbook/Using Unicode>
 
@@ -78,19 +78,19 @@ lot later.
 If your database server allows you to run queries across multiple
 databases at once, then so can DBIx::Class. All you need to do is make
 sure you write the database name as part of the
-L<DBIx::Class::ResultSource/table> call. Eg:
+L<table|DBIx::Class::ResultSourceProxy::Table/table> call. Eg:
 
   __PACKAGE__->table('mydb.mytablename');
 
-And load all the Result classes for both / all databases using one
-L<DBIx::Class::Schema/load_namespaces> call.
+And load all the Result classes for both / all databases by calling
+L<DBIx::Class::Schema/load_namespaces>.
 
 =item .. use DBIx::Class across PostgreSQL/DB2/Oracle schemas?
 
-Add the name of the schema to the L<DBIx::Class::ResultSource/table>
-as part of the name, and make sure you give the one user you are going
-to connect with has permissions to read/write all the schemas/tables as
-necessary.
+Add the name of the schema to the table name, when invoking
+L<table|DBIx::Class::ResultSourceProxy::Table/table>, and make sure the user
+you are about to connect as has permissions to read/write all the
+schemas/tables as necessary.
 
 =back
 
@@ -154,7 +154,7 @@ See L<DBIx::Class::Relationship>.
 =item .. use a relationship?
 
 Use its name. An accessor is created using the name. See examples in
-L<DBIx::Class::Manual::Cookbook/Using relationships>.
+L<DBIx::Class::Manual::Cookbook/USING RELATIONSHIPS>.
 
 =back
 
@@ -262,6 +262,39 @@ alter session statements on database connection establishment:
  ->on_connect_do("ALTER SESSION SET NLS_SORT = 'BINARY_CI'");
  ->on_connect_do("ALTER SESSION SET NLS_SORT = 'GERMAN_CI'");
 
+=item .. format a DateTime object for searching?
+
+L<search|DBIx::Class::ResultSet/search> and L<find|DBIx::Class::ResultSet/find>
+do not take L<DBIx::Class::InflateColumn> into account, and so your L<DateTime>
+object will not be correctly deflated into a format your RDBMS expects.
+
+The L<datetime_parser|DBIx::Class::Storage::DBI/datetime_parser> method on your
+storage object can be used to return the object that would normally do this, so
+it's easy to do it manually:
+
+  my $dtf = $schema->storage->datetime_parser;
+  my $rs = $schema->resultset('users')->search(
+    {
+      signup_date => {
+        -between => [
+          $dtf->format_datetime($dt_start),
+          $dtf->format_datetime($dt_end),
+        ],
+      }
+    },
+  );
+
+With in a Result Class method, you can get this from the
+L<C<result_source>|DBIx::Class::Row/result_source>.
+
+  my $dtf = $self->result_source->storage->datetime_parser;
+
+This kludge is necessary only for conditions passed to
+L<search|DBIx::Class::ResultSet/search> and L<DBIx::Class::ResultSet/find>,
+whereas L<create|DBIx::Class::ResultSet/create> and L<DBIx::Class::Row/update>
+(but not L<DBIx::Class::ResultSet/update>) are
+L<DBIx::Class::InflateColumn>-aware and will do the right thing when supplied
+an inflated L<DateTime> object.
 
 =back
 
@@ -451,8 +484,8 @@ what create_related() from L<DBIx::Class::Relationship::Base> does, you could
 add this to Book.pm:
 
   sub foo {
-    my ($self, $relname, $col_data) = @_;
-    return $self->related_resultset($relname)->create($col_data);
+    my ($self, $rel_name, $col_data) = @_;
+    return $self->related_resultset($rel_name)->create($col_data);
   }
 
 Invoked like this:
@@ -658,3 +691,14 @@ Taken from:
 L<http://dev.mysql.com/doc/refman/5.1/en/resetting-permissions.html>.
 
 =back
+
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index a28bb35..e71f952 100644 (file)
@@ -6,9 +6,9 @@ DBIx::Class::Manual::Features - A boatload of DBIx::Class features with links to
 
 =head2 Large Community
 
-Currently there are 88 people listed as contributors to DBIC.  That ranges
-from documentation help, to test help, to added features, to entire database
-support.
+There are L<hundres of DBIC contributors|DBIx::Class/AUTHORS> listed in
+F<AUTHORS>. That ranges from documentation help, to test help, to added
+features, to entire database support.
 
 =head2 Active Community
 
@@ -479,7 +479,9 @@ on our system (Microsoft SQL) is:
    ) rpt_score
  WHERE rno__row__index BETWEEN 1 AND 1
 
-See: L<DBIx::Class::ResultSet/related_resultset>, L<DBIx::ClassResultSet/search_related>, and L<DBIx::Class::ResultSet/get_column>.
+See: L<DBIx::Class::ResultSet/related_resultset>,
+L<DBIx::Class::ResultSet/search_related>, and
+L<DBIx::Class::ResultSet/get_column>.
 
 =head2 bonus rel methods
 
@@ -661,5 +663,15 @@ Better:
     price => \['price + ?', [inc => $inc]],
  });
 
-See L<SQL::Abstract/Literal_SQL_with_placeholders_and_bind_values_(subqueries)>
+See L<SQL::Abstract/Literal SQL with placeholders and bind values (subqueries)>
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 136355d..ae783a5 100644 (file)
@@ -66,11 +66,11 @@ relationships must be used.
 A Schema object represents your entire table collection, plus the
 connection to the database. You can create one or more schema objects,
 connected to various databases, with various users, using the same set
-of table L</Result class> definitions.
+of table L</Result Class> definitions.
 
 At least one L<DBIx::Class::Schema> class is needed per database.
 
-=head2 Result class
+=head2 Result Class
 
 A Result class defines both a source of data (usually one per table),
 and the methods that will be available in the L</Result> objects
@@ -87,7 +87,7 @@ ResultSource objects represent the source of your data, these are
 sometimes (incorrectly) called table objects.
 
 ResultSources do not need to be directly created, a ResultSource
-instance is created for each L</Result class> in your L</Schema>, by
+instance is created for each L</Result Class> in your L</Schema>, by
 the proxied methods C<table> and C<add_columns>.
 
 See also: L<DBIx::Class::ResultSource/METHODS>
@@ -148,17 +148,20 @@ to issue multiple SQL queries.
 A normalised database is a sane database. Each table contains only
 data belonging to one concept, related tables refer to the key field
 or fields of each other. Some links to webpages about normalisation
-can be found in L<DBIx::Class::Manual::FAQ|the FAQ>.
+can be found in L<the FAQ|DBIx::Class::Manual::FAQ>.
 
 =head2 Related data
 
 In SQL, related data actually refers to data that are normalised into
 the same table. (Yes. DBIC does mis-use this term.)
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index f13e14e..c4c928e 100644 (file)
@@ -471,4 +471,13 @@ information on this can be found in L<DBIx::Class::Manual::Troubleshooting>
 
 =back
 
-=cut
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 625a4d9..bc4f846 100644 (file)
@@ -242,7 +242,7 @@ To join two relations at the same level, use an arrayref instead:
 
 Or combine the two:
 
-  join => { room => [ 'chair', { table => 'leg' } ]
+  join => { room => [ 'chair', { table => 'leg' } ] }
 
 =head2 Table aliases
 
@@ -274,3 +274,13 @@ The aliases are: C<room> and C<room_2>.
 
 =cut
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 1d7415a..0c31ac0 100644 (file)
@@ -192,3 +192,14 @@ L<DBIx::Class::Manual::SQLHackers>.
 
 Continue with L<DBIx::Class::Tutorial> and
 L<DBIx::Class/"WHERE TO START READING">.
+
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index cb352a2..45ecdbd 100644 (file)
@@ -179,12 +179,13 @@ Examples and explaining paragraphs can be repeated as necessary.
 
 =back
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
-
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 29ff9e9..310acaa 100644 (file)
@@ -51,10 +51,13 @@ C<Result Class>. This document serves as a general overview of C<Result Class>
 declaration best practices, and offers an index of the available methods
 (and the Components/Roles which provide them).
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index b28a960..f5e06b8 100644 (file)
@@ -122,7 +122,7 @@ Linux system (as well as their derivative distributions such as Centos,
 White Box and Scientific Linux).
 
 Distributions affected include Fedora 5 through to Fedora 8 and RHEL5
-upto and including RHEL5 Update 2. Fedora 9 (which uses perl 5.10) has
+up to and including RHEL5 Update 2. Fedora 9 (which uses perl 5.10) has
 never been affected - this is purely a perl 5.8.8 issue.
 
 As of September 2008 the following packages are known to be fixed and so
@@ -158,5 +158,13 @@ can grow very large.
 
 The solution is to use the smallest practical value for LongReadLen.
 
-=cut
+=head1 FURTHER QUESTIONS?
 
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 23ffebe..a7ab5e5 100644 (file)
@@ -1,9 +1,19 @@
 package DBIx::Class::Optional::Dependencies;
 
-use warnings;
-use strict;
+### This may look crazy, but it in fact tangibly ( by 50(!)% ) shortens
+#   the skip-test time when everything requested is unavailable
+BEGIN {
+  if ( $ENV{RELEASE_TESTING} ) {
+    require warnings and warnings->import;
+    require strict and strict->import;
+  }
+}
 
-use Carp ();
+sub croak {
+  require Carp;
+  Carp::croak(@_);
+};
+###
 
 # NO EXTERNAL NON-5.8.1 CORE DEPENDENCIES EVER (e.g. C::A::G)
 # This module is to be loaded by Makefile.PM on a pristine system
@@ -11,110 +21,71 @@ use Carp ();
 # POD is generated automatically by calling _gen_pod from the
 # Makefile.PL in $AUTHOR mode
 
-# NOTE: the rationale for 2 JSON::Any versions is that
-# we need the newer only to work around JSON::XS, which
-# itself is an optional dep
-my $min_json_any = {
-  'JSON::Any'                     => '1.23',
-};
-my $test_and_dist_json_any = {
-  'JSON::Any'                     => '1.31',
-};
-
+# *DELIBERATELY* not making a group for these - they must disappear
+# forever as optdeps in the first place
 my $moose_basic = {
   'Moose'                         => '0.98',
   'MooseX::Types'                 => '0.21',
   'MooseX::Types::LoadableClass'  => '0.011',
 };
 
-my $replicated = {
-  %$moose_basic,
-};
+my $dbic_reqs = {
 
-my $admin_basic = {
-  %$moose_basic,
-  %$min_json_any,
-  'MooseX::Types::Path::Class'    => '0.05',
-  'MooseX::Types::JSON'           => '0.02',
-  'namespace::autoclean'          => '0.09',
-};
+  # NOTE: the rationale for 2 JSON::Any versions is that
+  # we need the newer only to work around JSON::XS, which
+  # itself is an optional dep
+  _json_any => {
+    req => {
+      'JSON::Any' => '1.23',
+    },
+  },
 
-my $admin_script = {
-  %$moose_basic,
-  %$admin_basic,
-  'Getopt::Long::Descriptive' => '0.081',
-  'Text::CSV'                 => '1.16',
-};
+  _json_xs_compatible_json_any => {
+    req => {
+      'JSON::Any' => '1.31',
+    },
+  },
 
-my $datetime_basic = {
-  'DateTime'                      => '0.55',
-  'DateTime::Format::Strptime'    => '1.2',
-};
+  # a common placeholder for engines with IC::DT support based off DT::F::S
+  _ic_dt_strptime_based => {
+    augment => {
+      ic_dt => {
+        req => {
+          'DateTime::Format::Strptime' => '1.2',
+        },
+      },
+    }
+  },
 
-my $id_shortener = {
-  'Math::BigInt'                  => '1.80',
-  'Math::Base36'                  => '0.07',
-};
+  _rdbms_generic_odbc => {
+    req => {
+      'DBD::ODBC' => 0,
+    }
+  },
 
-my $rdbms_sqlite = {
-  'DBD::SQLite'                   => '0',
-};
-my $rdbms_pg = {
-  'DBD::Pg'                       => '0',
-};
-my $rdbms_mssql_odbc = {
-  'DBD::ODBC'                     => '0',
-};
-my $rdbms_mssql_sybase = {
-  'DBD::Sybase'                   => '0',
-};
-my $rdbms_mssql_ado = {
-  'DBD::ADO'                      => '0',
-};
-my $rdbms_msaccess_odbc = {
-  'DBD::ODBC'                     => '0',
-};
-my $rdbms_msaccess_ado = {
-  'DBD::ADO'                      => '0',
-};
-my $rdbms_mysql = {
-  'DBD::mysql'                    => '0',
-};
-my $rdbms_oracle = {
-  'DBD::Oracle'                   => '0',
-  %$id_shortener,
-};
-my $rdbms_ase = {
-  'DBD::Sybase'                   => '0',
-};
-my $rdbms_db2 = {
-  'DBD::DB2'                      => '0',
-};
-my $rdbms_db2_400 = {
-  'DBD::ODBC'                     => '0',
-};
-my $rdbms_informix = {
-  'DBD::Informix'                 => '0',
-};
-my $rdbms_sqlanywhere = {
-  'DBD::SQLAnywhere'              => '0',
-};
-my $rdbms_sqlanywhere_odbc = {
-  'DBD::ODBC'                     => '0',
-};
-my $rdbms_firebird = {
-  'DBD::Firebird'                 => '0',
-};
-my $rdbms_firebird_interbase = {
-  'DBD::InterBase'                => '0',
-};
-my $rdbms_firebird_odbc = {
-  'DBD::ODBC'                     => '0',
-};
+  _rdbms_generic_ado => {
+    req => {
+      'DBD::ADO' => 0,
+    }
+  },
+
+  # must list any dep used by adhoc testing
+  # this prevents the "skips due to forgotten deps" issue
+  test_adhoc => {
+    req => {
+      'Class::DBI::Plugin::DeepAbstractSearch' => '0',
+      'Class::DBI' => '3.000005',
+      'Date::Simple' => '3.03',
+      'YAML' => '0',
+      'Class::Unload' => '0.07',
+      'Time::Piece' => '0',
+      'Time::Piece::MySQL' => '0',
+      'DBD::mysql' => '4.023',
+    },
+  },
 
-my $reqs = {
   replicated => {
-    req => $replicated,
+    req => $moose_basic,
     pod => {
       title => 'Storage::Replicated',
       desc => 'Modules required for L<DBIx::Class::Storage::DBI::Replicated>',
@@ -122,16 +93,28 @@ my $reqs = {
   },
 
   test_replicated => {
+    include => 'replicated',
     req => {
-      %$replicated,
-      'Test::Moose'               => '0',
+      'Test::Moose' => '0',
     },
   },
 
+  config_file_reader => {
+    pod => {
+      title => 'Generic config reader',
+      desc => 'Modules required for generic config file parsing, currently Config::Any (rarely used at runtime)',
+    },
+    req => {
+      'Config::Any' => '0.20',
+    },
+  },
 
   admin => {
+    include => [qw( _json_any config_file_reader )],
     req => {
-      %$admin_basic,
+      %$moose_basic,
+      'MooseX::Types::Path::Class' => '0.05',
+      'MooseX::Types::JSON' => '0.02',
     },
     pod => {
       title => 'DBIx::Class::Admin',
@@ -140,8 +123,10 @@ my $reqs = {
   },
 
   admin_script => {
+    include => 'admin',
     req => {
-      %$admin_script,
+      'Getopt::Long::Descriptive' => '0.081',
+      'Text::CSV' => '1.16',
     },
     pod => {
       title => 'dbicadmin',
@@ -151,21 +136,46 @@ my $reqs = {
 
   deploy => {
     req => {
-      'SQL::Translator'           => '0.11016',
+      'SQL::Translator'           => '0.11018',
     },
     pod => {
       title => 'Storage::DBI::deploy()',
-      desc => 'Modules required for L<DBIx::Class::Storage::DBI/deploy> and L<DBIx::Class::Storage::DBI/deployment_statements>',
+      desc => 'Modules required for L<DBIx::Class::Storage::DBI/deployment_statements> and L<DBIx::Class::Schema/deploy>',
+    },
+  },
+
+  ic_dt => {
+    req => {
+      'DateTime' => '0.55',
+      'DateTime::TimeZone::OlsonDB' => 0,
+    },
+    pod => {
+      title => 'InflateColumn::DateTime support',
+      desc =>
+        'Modules required for L<DBIx::Class::InflateColumn::DateTime>. '
+      . 'Note that this group does not require much on its own, but '
+      . 'instead is augmented by various RDBMS-specific groups. See the '
+      . 'documentation of each C<rbms_*> group for details',
     },
   },
 
   id_shortener => {
-    req => $id_shortener,
+    req => {
+      'Math::BigInt' => '1.80',
+      'Math::Base36' => '0.07',
+    },
   },
 
-  test_component_accessor => {
+  cdbicompat => {
     req => {
-      'Class::Unload'             => '0.07',
+      'Class::Data::Inheritable' => '0',
+      'Class::Trigger' => '0',
+      'DBIx::ContextualFetch' => '0',
+      'Clone' => '0.32',
+    },
+    pod => {
+      title => 'DBIx::Class::CDBICompat support',
+      desc => 'Modules required for L<DBIx::Class::CDBICompat>'
     },
   },
 
@@ -173,6 +183,7 @@ my $reqs = {
     req => {
       'Test::Pod'                 => '1.42',
     },
+    release_testing_mandatory => 1,
   },
 
   test_podcoverage => {
@@ -180,6 +191,7 @@ my $reqs = {
       'Test::Pod::Coverage'       => '1.08',
       'Pod::Coverage'             => '0.20',
     },
+    release_testing_mandatory => 1,
   },
 
   test_whitespace => {
@@ -187,22 +199,23 @@ my $reqs = {
       'Test::EOL'                 => '1.0',
       'Test::NoTabs'              => '0.9',
     },
+    release_testing_mandatory => 1,
   },
 
   test_strictures => {
     req => {
       'Test::Strict'              => '0.20',
     },
+    release_testing_mandatory => 1,
   },
 
   test_prettydebug => {
-    req => $min_json_any,
+    include => '_json_any',
   },
 
   test_admin_script => {
+    include => [qw( admin_script _json_xs_compatible_json_any )],
     req => {
-      %$admin_script,
-      %$test_and_dist_json_any,
       'JSON' => 0,
       'JSON::PP' => 0,
       'Cpanel::JSON::XS' => 0,
@@ -223,43 +236,15 @@ my $reqs = {
     },
   },
 
-  test_dt => {
-    req => $datetime_basic,
-  },
-
-  test_dt_sqlite => {
-    req => {
-      %$datetime_basic,
-      # t/36datetime.t
-      # t/60core.t
-      'DateTime::Format::SQLite'  => '0',
-    },
-  },
-
-  test_dt_mysql => {
-    req => {
-      %$datetime_basic,
-      # t/inflate/datetime_mysql.t
-      # (doesn't need Mysql itself)
-      'DateTime::Format::MySQL'   => '0',
-    },
-  },
-
-  test_dt_pg => {
-    req => {
-      %$datetime_basic,
-      # t/inflate/datetime_pg.t
-      # (doesn't need PG itself)
-      'DateTime::Format::Pg'      => '0.16004',
-    },
-  },
-
-  test_cdbicompat => {
-    req => {
-      'Class::DBI::Plugin::DeepAbstractSearch' => '0',
-      %$datetime_basic,
-      'Time::Piece::MySQL'        => '0',
-      'Date::Simple'              => '3.03',
+  binary_data => {
+    pod => {
+      title => 'Binary datatype support (certain RDBMS)',
+      desc =>
+        'Some RDBMS engines require specific versions of the respective DBD '
+      . 'driver for binary data support. Note that this group does not '
+      . 'require anything on its own, but instead is augmented by various '
+      . 'RDBMS-specific groups. See the documentation of each rbms_* group '
+      . 'for details',
     },
   },
 
@@ -267,29 +252,61 @@ my $reqs = {
   # is a core dep of DBIC for testing
   rdbms_sqlite => {
     req => {
-      %$rdbms_sqlite,
+      'DBD::SQLite' => 0,
     },
     pod => {
       title => 'SQLite support',
       desc => 'Modules required to connect to SQLite',
     },
+    augment => {
+      ic_dt => {
+        req => {
+          'DateTime::Format::SQLite' => '0',
+        },
+      },
+    },
+  },
+
+  # centralize the specification, as we have ICDT tests which can
+  # test the full behavior of RDBMS-specific ICDT on top of bare SQLite
+  _ic_dt_pg_base => {
+    augment => {
+      ic_dt => {
+        req => {
+          'DateTime::Format::Pg' => '0.16004',
+        },
+      },
+    },
+  },
+
+  ic_dt_pg => {
+    include => [qw( ic_dt _ic_dt_pg_base )],
   },
 
   rdbms_pg => {
+    include => '_ic_dt_pg_base',
     req => {
-      # when changing this list make sure to adjust xt/optional_deps.t
-      %$rdbms_pg,
+      'DBD::Pg' => 0,
     },
     pod => {
       title => 'PostgreSQL support',
       desc => 'Modules required to connect to PostgreSQL',
     },
+    augment => {
+      binary_data => {
+        req => {
+          'DBD::Pg' => '2.009002'
+        },
+      }
+    },
+  },
+
+  _rdbms_mssql_common => {
+    include => '_ic_dt_strptime_based',
   },
 
   rdbms_mssql_odbc => {
-    req => {
-      %$rdbms_mssql_odbc,
-    },
+    include => [qw( _rdbms_generic_odbc _rdbms_mssql_common )],
     pod => {
       title => 'MSSQL support via DBD::ODBC',
       desc => 'Modules required to connect to MSSQL via DBD::ODBC',
@@ -297,8 +314,9 @@ my $reqs = {
   },
 
   rdbms_mssql_sybase => {
+    include => '_rdbms_mssql_common',
     req => {
-      %$rdbms_mssql_sybase,
+      'DBD::Sybase' => 0,
     },
     pod => {
       title => 'MSSQL support via DBD::Sybase',
@@ -307,19 +325,19 @@ my $reqs = {
   },
 
   rdbms_mssql_ado => {
-    req => {
-      %$rdbms_mssql_ado,
-    },
+    include => [qw( _rdbms_generic_ado _rdbms_mssql_common )],
     pod => {
       title => 'MSSQL support via DBD::ADO (Windows only)',
       desc => 'Modules required to connect to MSSQL via DBD::ADO. This particular DBD is available on Windows only',
     },
   },
 
+  _rdbms_msaccess_common => {
+    include => '_ic_dt_strptime_based',
+  },
+
   rdbms_msaccess_odbc => {
-    req => {
-      %$rdbms_msaccess_odbc,
-    },
+    include => [qw( _rdbms_generic_odbc _rdbms_msaccess_common )],
     pod => {
       title => 'MS Access support via DBD::ODBC',
       desc => 'Modules required to connect to MS Access via DBD::ODBC',
@@ -327,18 +345,33 @@ my $reqs = {
   },
 
   rdbms_msaccess_ado => {
-    req => {
-      %$rdbms_msaccess_ado,
-    },
+    include => [qw( _rdbms_generic_ado _rdbms_msaccess_common )],
     pod => {
       title => 'MS Access support via DBD::ADO (Windows only)',
       desc => 'Modules required to connect to MS Access via DBD::ADO. This particular DBD is available on Windows only',
     },
   },
 
+  # centralize the specification, as we have ICDT tests which can
+  # test the full behavior of RDBMS-specific ICDT on top of bare SQLite
+  _ic_dt_mysql_base => {
+    augment => {
+      ic_dt => {
+        req => {
+          'DateTime::Format::MySQL' => '0',
+        },
+      },
+    },
+  },
+
+  ic_dt_mysql => {
+    include => [qw( ic_dt _ic_dt_mysql_base )],
+  },
+
   rdbms_mysql => {
+    include => '_ic_dt_mysql_base',
     req => {
-      %$rdbms_mysql,
+      'DBD::mysql' => 0,
     },
     pod => {
       title => 'MySQL support',
@@ -347,18 +380,27 @@ my $reqs = {
   },
 
   rdbms_oracle => {
+    include => 'id_shortener',
     req => {
-      %$rdbms_oracle,
+      'DBD::Oracle' => 0,
     },
     pod => {
       title => 'Oracle support',
       desc => 'Modules required to connect to Oracle',
     },
+    augment => {
+      ic_dt => {
+        req => {
+          'DateTime::Format::Oracle' => '0',
+        },
+      },
+    },
   },
 
   rdbms_ase => {
+    include => '_ic_dt_strptime_based',
     req => {
-      %$rdbms_ase,
+      'DBD::Sybase' => 0,
     },
     pod => {
       title => 'Sybase ASE support',
@@ -366,9 +408,20 @@ my $reqs = {
     },
   },
 
+  _rdbms_db2_common => {
+    augment => {
+      ic_dt => {
+        req => {
+          'DateTime::Format::DB2' => '0',
+        },
+      },
+    },
+  },
+
   rdbms_db2 => {
+    include => '_rdbms_db2_common',
     req => {
-      %$rdbms_db2,
+      'DBD::DB2' => 0,
     },
     pod => {
       title => 'DB2 support',
@@ -377,9 +430,7 @@ my $reqs = {
   },
 
   rdbms_db2_400 => {
-    req => {
-      %$rdbms_db2_400,
-    },
+    include => [qw( _rdbms_generic_odbc _rdbms_db2_common )],
     pod => {
       title => 'DB2 on AS/400 support',
       desc => 'Modules required to connect to DB2 on AS/400',
@@ -387,8 +438,9 @@ my $reqs = {
   },
 
   rdbms_informix => {
+    include => '_ic_dt_strptime_based',
     req => {
-      %$rdbms_informix,
+      'DBD::Informix' => 0,
     },
     pod => {
       title => 'Informix support',
@@ -396,9 +448,14 @@ my $reqs = {
     },
   },
 
+  _rdbms_sqlanywhere_common => {
+    include => '_ic_dt_strptime_based',
+  },
+
   rdbms_sqlanywhere => {
+    include => '_rdbms_sqlanywhere_common',
     req => {
-      %$rdbms_sqlanywhere,
+      'DBD::SQLAnywhere' => 0,
     },
     pod => {
       title => 'SQLAnywhere support',
@@ -407,18 +464,21 @@ my $reqs = {
   },
 
   rdbms_sqlanywhere_odbc => {
-    req => {
-      %$rdbms_sqlanywhere_odbc,
-    },
+    include => [qw( _rdbms_generic_odbc _rdbms_sqlanywhere_common )],
     pod => {
       title => 'SQLAnywhere support via DBD::ODBC',
       desc => 'Modules required to connect to SQLAnywhere via DBD::ODBC',
     },
   },
 
+  _rdbms_firebird_common => {
+    include => '_ic_dt_strptime_based',
+  },
+
   rdbms_firebird => {
+    include => '_rdbms_firebird_common',
     req => {
-      %$rdbms_firebird,
+      'DBD::Firebird' => 0,
     },
     pod => {
       title => 'Firebird support',
@@ -427,8 +487,9 @@ my $reqs = {
   },
 
   rdbms_firebird_interbase => {
+    include => '_rdbms_firebird_common',
     req => {
-      %$rdbms_firebird_interbase,
+      'DBD::InterBase' => 0,
     },
     pod => {
       title => 'Firebird support via DBD::InterBase',
@@ -437,195 +498,216 @@ my $reqs = {
   },
 
   rdbms_firebird_odbc => {
-    req => {
-      %$rdbms_firebird_odbc,
-    },
+    include => [qw( _rdbms_generic_odbc _rdbms_firebird_common )],
     pod => {
       title => 'Firebird support via DBD::ODBC',
       desc => 'Modules required to connect to Firebird via DBD::ODBC',
     },
   },
 
-# the order does matter because the rdbms support group might require
-# a different version that the test group
-  test_rdbms_pg => {
+  test_rdbms_sqlite => {
+    include => 'rdbms_sqlite',
     req => {
-      $ENV{DBICTEST_PG_DSN}
-        ? (
-          # when changing this list make sure to adjust xt/optional_deps.t
-          %$rdbms_pg,
-          ($^O ne 'MSWin32' ? ('Sys::SigAction' => '0') : ()),
-          'DBD::Pg'               => '2.009002',
-        ) : ()
+      ###
+      ### IMPORTANT - do not raise this dependency
+      ### even though many bugfixes are present in newer versions, the general DBIC
+      ### rule is to bend over backwards for available DBDs (given upgrading them is
+      ### often *not* easy or even possible)
+      ###
+      'DBD::SQLite' => '1.29',
     },
   },
 
+  test_rdbms_pg => {
+    include => 'rdbms_pg',
+    env => [
+      DBICTEST_PG_DSN => 1,
+      DBICTEST_PG_USER => 0,
+      DBICTEST_PG_PASS => 0,
+    ],
+  },
+
   test_rdbms_mssql_odbc => {
-    req => {
-      $ENV{DBICTEST_MSSQL_ODBC_DSN}
-        ? (
-          %$rdbms_mssql_odbc,
-        ) : ()
-    },
+    include => 'rdbms_mssql_odbc',
+    env => [
+      DBICTEST_MSSQL_ODBC_DSN => 1,
+      DBICTEST_MSSQL_ODBC_USER => 0,
+      DBICTEST_MSSQL_ODBC_PASS => 0,
+    ],
   },
 
   test_rdbms_mssql_ado => {
-    req => {
-      $ENV{DBICTEST_MSSQL_ADO_DSN}
-        ? (
-          %$rdbms_mssql_ado,
-        ) : ()
-    },
+    include => 'rdbms_mssql_ado',
+    env => [
+      DBICTEST_MSSQL_ADO_DSN => 1,
+      DBICTEST_MSSQL_ADO_USER => 0,
+      DBICTEST_MSSQL_ADO_PASS => 0,
+    ],
   },
 
   test_rdbms_mssql_sybase => {
-    req => {
-      $ENV{DBICTEST_MSSQL_DSN}
-        ? (
-          %$rdbms_mssql_sybase,
-        ) : ()
-    },
+    include => 'rdbms_mssql_sybase',
+    env => [
+      DBICTEST_MSSQL_DSN => 1,
+      DBICTEST_MSSQL_USER => 0,
+      DBICTEST_MSSQL_PASS => 0,
+    ],
   },
 
   test_rdbms_msaccess_odbc => {
+    include => 'rdbms_msaccess_odbc',
+    env => [
+      DBICTEST_MSACCESS_ODBC_DSN => 1,
+      DBICTEST_MSACCESS_ODBC_USER => 0,
+      DBICTEST_MSACCESS_ODBC_PASS => 0,
+    ],
     req => {
-      $ENV{DBICTEST_MSACCESS_ODBC_DSN}
-        ? (
-          %$rdbms_msaccess_odbc,
-          %$datetime_basic,
-          'Data::GUID' => '0',
-        ) : ()
+      'Data::GUID' => '0',
     },
   },
 
   test_rdbms_msaccess_ado => {
+    include => 'rdbms_msaccess_ado',
+    env => [
+      DBICTEST_MSACCESS_ADO_DSN => 1,
+      DBICTEST_MSACCESS_ADO_USER => 0,
+      DBICTEST_MSACCESS_ADO_PASS => 0,
+    ],
     req => {
-      $ENV{DBICTEST_MSACCESS_ADO_DSN}
-        ? (
-          %$rdbms_msaccess_ado,
-          %$datetime_basic,
-          'Data::GUID' => 0,
-        ) : ()
+      'Data::GUID' => 0,
     },
   },
 
   test_rdbms_mysql => {
-    req => {
-      $ENV{DBICTEST_MYSQL_DSN}
-        ? (
-          %$rdbms_mysql,
-        ) : ()
-    },
+    include => 'rdbms_mysql',
+    env => [
+      DBICTEST_MYSQL_DSN => 1,
+      DBICTEST_MYSQL_USER => 0,
+      DBICTEST_MYSQL_PASS => 0,
+    ],
   },
 
   test_rdbms_oracle => {
+    include => 'rdbms_oracle',
+    env => [
+      DBICTEST_ORA_DSN => 1,
+      DBICTEST_ORA_USER => 0,
+      DBICTEST_ORA_PASS => 0,
+    ],
     req => {
-      $ENV{DBICTEST_ORA_DSN}
-        ? (
-          %$rdbms_oracle,
-          'DateTime::Format::Oracle' => '0',
-          'DBD::Oracle'              => '1.24',
-        ) : ()
+      'DBD::Oracle'              => '1.24',
     },
   },
 
   test_rdbms_ase => {
-    req => {
-      $ENV{DBICTEST_SYBASE_DSN}
-        ? (
-          %$rdbms_ase,
-        ) : ()
-    },
+    include => 'rdbms_ase',
+    env => [
+      DBICTEST_SYBASE_DSN => 1,
+      DBICTEST_SYBASE_USER => 0,
+      DBICTEST_SYBASE_PASS => 0,
+    ],
   },
 
   test_rdbms_db2 => {
-    req => {
-      $ENV{DBICTEST_DB2_DSN}
-        ? (
-          %$rdbms_db2,
-        ) : ()
-    },
+    include => 'rdbms_db2',
+    env => [
+      DBICTEST_DB2_DSN => 1,
+      DBICTEST_DB2_USER => 0,
+      DBICTEST_DB2_PASS => 0,
+    ],
   },
 
   test_rdbms_db2_400 => {
-    req => {
-      $ENV{DBICTEST_DB2_400_DSN}
-        ? (
-          %$rdbms_db2_400,
-        ) : ()
-    },
+    include => 'rdbms_db2_400',
+    env => [
+      DBICTEST_DB2_400_DSN => 1,
+      DBICTEST_DB2_400_USER => 0,
+      DBICTEST_DB2_400_PASS => 0,
+    ],
   },
 
   test_rdbms_informix => {
-    req => {
-      $ENV{DBICTEST_INFORMIX_DSN}
-        ? (
-          %$rdbms_informix,
-        ) : ()
-    },
+    include => 'rdbms_informix',
+    env => [
+      DBICTEST_INFORMIX_DSN => 1,
+      DBICTEST_INFORMIX_USER => 0,
+      DBICTEST_INFORMIX_PASS => 0,
+    ],
   },
 
   test_rdbms_sqlanywhere => {
-    req => {
-      $ENV{DBICTEST_SQLANYWHERE_DSN}
-        ? (
-          %$rdbms_sqlanywhere,
-        ) : ()
-    },
+    include => 'rdbms_sqlanywhere',
+    env => [
+      DBICTEST_SQLANYWHERE_DSN => 1,
+      DBICTEST_SQLANYWHERE_USER => 0,
+      DBICTEST_SQLANYWHERE_PASS => 0,
+    ],
   },
 
   test_rdbms_sqlanywhere_odbc => {
-    req => {
-      $ENV{DBICTEST_SQLANYWHERE_ODBC_DSN}
-        ? (
-          %$rdbms_sqlanywhere_odbc,
-        ) : ()
-    },
+    include => 'rdbms_sqlanywhere_odbc',
+    env => [
+      DBICTEST_SQLANYWHERE_ODBC_DSN => 1,
+      DBICTEST_SQLANYWHERE_ODBC_USER => 0,
+      DBICTEST_SQLANYWHERE_ODBC_PASS => 0,
+    ],
   },
 
   test_rdbms_firebird => {
-    req => {
-      $ENV{DBICTEST_FIREBIRD_DSN}
-        ? (
-          %$rdbms_firebird,
-        ) : ()
-    },
+    include => 'rdbms_firebird',
+    env => [
+      DBICTEST_FIREBIRD_DSN => 1,
+      DBICTEST_FIREBIRD_USER => 0,
+      DBICTEST_FIREBIRD_PASS => 0,
+    ],
   },
 
   test_rdbms_firebird_interbase => {
-    req => {
-      $ENV{DBICTEST_FIREBIRD_INTERBASE_DSN}
-        ? (
-          %$rdbms_firebird_interbase,
-        ) : ()
-    },
+    include => 'rdbms_firebird_interbase',
+    env => [
+      DBICTEST_FIREBIRD_INTERBASE_DSN => 1,
+      DBICTEST_FIREBIRD_INTERBASE_USER => 0,
+      DBICTEST_FIREBIRD_INTERBASE_PASS => 0,
+    ],
   },
 
   test_rdbms_firebird_odbc => {
-    req => {
-      $ENV{DBICTEST_FIREBIRD_ODBC_DSN}
-        ? (
-          %$rdbms_firebird_odbc,
-        ) : ()
-    },
+    include => 'rdbms_firebird_odbc',
+    env => [
+      DBICTEST_FIREBIRD_ODBC_DSN => 1,
+      DBICTEST_FIREBIRD_ODBC_USER => 0,
+      DBICTEST_FIREBIRD_ODBC_PASS => 0,
+    ],
   },
 
   test_memcached => {
+    env => [
+      DBICTEST_MEMCACHED => 1,
+    ],
     req => {
-      $ENV{DBICTEST_MEMCACHED}
-        ? (
-          'Cache::Memcached' => 0,
-        ) : ()
+      'Cache::Memcached' => 0,
     },
   },
 
   dist_dir => {
+    # we need to run the dbicadmin so we can self-generate its POD
+    # also we do not want surprises in case JSON::XS is in the path
+    # so make sure we get an always-working JSON::Any
+    include => [qw(
+      admin_script
+      _json_xs_compatible_json_any
+      id_shortener
+      deploy
+      test_pod
+      test_podcoverage
+      test_whitespace
+      test_strictures
+    )],
     req => {
-      %$test_and_dist_json_any,
       'ExtUtils::MakeMaker' => '6.64',
-      'Pod::Inherit'        => '0.90',
-      'Pod::Tree'           => '0',
-    }
+      'Module::Install'     => '1.06',
+      'Pod::Inherit'        => '0.91',
+    },
   },
 
   dist_upload => {
@@ -633,107 +715,478 @@ my $reqs = {
       'CPAN::Uploader' => '0.103001',
     },
   },
-
 };
 
-our %req_availability_cache;
 
-sub req_list_for {
-  my ($class, $group) = @_;
 
-  Carp::croak "req_list_for() expects a requirement group name"
-    unless $group;
+### Public API
+
+sub import {
+  my $class = shift;
 
-  my $deps = $reqs->{$group}{req}
-    or Carp::croak "Requirement group '$group' does not exist";
+  if (@_) {
 
-  return { %$deps };
+    my $action = shift;
+
+    if ($action eq '-die_without') {
+      my $err;
+      {
+        local $@;
+        eval { $class->die_unless_req_ok_for(\@_); 1 }
+          or $err = $@;
+      }
+      die "\n$err\n" if $err;
+    }
+    elsif ($action eq '-list_missing') {
+      print $class->modreq_missing_for(\@_);
+      print "\n";
+      exit 0;
+    }
+    elsif ($action eq '-skip_all_without') {
+
+      # sanity check - make sure ->current_test is 0 and no plan has been declared
+      do {
+        local $@;
+        defined eval {
+          Test::Builder->new->current_test
+            or
+          Test::Builder->new->has_plan
+        };
+      } and croak("Unable to invoke -skip_all_without after testing has started");
+
+      if ( my $missing = $class->req_missing_for(\@_) ) {
+
+        die ("\nMandatory requirements not satisfied during release-testing: $missing\n\n")
+          if $ENV{RELEASE_TESTING} and $class->_groups_to_reqs(\@_)->{release_testing_mandatory};
+
+        print "1..0 # SKIP requirements not satisfied: $missing\n";
+        exit 0;
+      }
+    }
+    elsif ($action =~ /^-/) {
+      croak "Unknown import-time action '$action'";
+    }
+    else {
+      croak "$class is not an exporter, unable to import '$action'";
+    }
+  }
+
+  1;
+}
+
+sub unimport {
+  croak( __PACKAGE__ . " does not implement unimport" );
 }
 
+# OO for (mistakenly considered) ease of extensibility, not due to any need to
+# carry state of any sort. This API is currently used outside, so leave as-is.
+# FIXME - make sure to not propagate this further if module is extracted as a
+# standalone library - keep the stupidity to a DBIC-secific shim!
+#
+sub req_list_for {
+  shift->_groups_to_reqs(shift)->{effective_modreqs};
+}
 
-sub die_unless_req_ok_for {
-  my ($class, $group) = @_;
+sub modreq_list_for {
+  shift->_groups_to_reqs(shift)->{modreqs};
+}
 
-  Carp::croak "die_unless_req_ok_for() expects a requirement group name"
-    unless $group;
+sub req_group_list {
+  +{ map
+    { $_ => $_[0]->_groups_to_reqs($_) }
+    grep { $_ !~ /^_/ } keys %$dbic_reqs
+  }
+}
 
-  $class->_check_deps($group)->{status}
-    or die sprintf( "Required modules missing, unable to continue: %s\n", $class->_check_deps($group)->{missing} );
+sub req_errorlist_for { shift->modreq_errorlist_for(shift) }  # deprecated
+sub modreq_errorlist_for {
+  my ($self, $groups) = @_;
+  $self->_errorlist_for_modreqs( $self->_groups_to_reqs($groups)->{modreqs} );
 }
 
 sub req_ok_for {
-  my ($class, $group) = @_;
+  shift->req_missing_for(shift) ? 0 : 1;
+}
+
+sub req_missing_for {
+  my ($self, $groups) = @_;
+
+  my $reqs = $self->_groups_to_reqs($groups);
 
-  Carp::croak "req_ok_for() expects a requirement group name"
-    unless $group;
+  my $mods_missing = $reqs->{missing_envvars}
+    ? $self->_list_physically_missing_modules( $reqs->{modreqs} )
+    : $self->modreq_missing_for($groups)
+  ;
+
+  return '' if
+    ! $mods_missing
+      and
+    ! $reqs->{missing_envvars}
+  ;
+
+  my @res = $mods_missing || ();
 
-  return $class->_check_deps($group)->{status};
+  push @res, 'the following group(s) of environment variables: ' . join ' and ', sort map
+    { __envvar_group_desc($_) }
+    @{$reqs->{missing_envvars}}
+  if $reqs->{missing_envvars};
+
+  return (
+    ( join ' as well as ', @res )
+      .
+    ( $reqs->{modreqs_fully_documented} ? " (see @{[ ref $self || $self ]} documentation for details)" : '' ),
+  );
 }
 
-sub req_missing_for {
-  my ($class, $group) = @_;
+sub modreq_missing_for {
+  my ($self, $groups) = @_;
+
+  my $reqs = $self->_groups_to_reqs($groups);
+  my $modreq_errors = $self->_errorlist_for_modreqs($reqs->{modreqs})
+    or return '';
+
+  join ' ', map
+    { $reqs->{modreqs}{$_} ? "$_~$reqs->{modreqs}{$_}" : $_ }
+    sort { lc($a) cmp lc($b) } keys %$modreq_errors
+  ;
+}
+
+my $tb;
+sub skip_without {
+  my ($self, $groups) = @_;
+
+  $tb ||= do { local $@; eval { Test::Builder->new } }
+    or croak "Calling skip_without() before loading Test::Builder makes no sense";
 
-  Carp::croak "req_missing_for() expects a requirement group name"
-    unless $group;
+  if ( my $err = $self->req_missing_for($groups) ) {
+    my ($fn, $ln) = (caller(0))[1,2];
+    $tb->skip("block in $fn around line $ln requires $err");
+    local $^W = 0;
+    last SKIP;
+  }
 
-  return $class->_check_deps($group)->{missing};
+  1;
 }
 
-sub req_errorlist_for {
-  my ($class, $group) = @_;
+sub die_unless_req_ok_for {
+  if (my $err = shift->req_missing_for(shift) ) {
+    die "Unable to continue due to missing requirements: $err\n";
+  }
+}
+
+
+
+### Private functions
+
+# potentially shorten group desc
+sub __envvar_group_desc {
+  my @envs = @{$_[0]};
+
+  my (@res, $last_prefix);
+  while (my $ev = shift @envs) {
+    my ($pref, $sep, $suff) = split / ([\_\-]) (?= [^\_\-]+ \z )/x, $ev;
+
+    if ( defined $sep and ($last_prefix||'') eq $pref ) {
+        push @res, "...${sep}${suff}"
+    }
+    else {
+      push @res, $ev;
+    }
 
-  Carp::croak "req_errorlist_for() expects a requirement group name"
-    unless $group;
+    $last_prefix = $pref if $sep;
+  }
 
-  return $class->_check_deps($group)->{errorlist};
+  join '/', @res;
 }
 
-sub _check_deps {
-  my ($class, $group) = @_;
+my $groupname_re = qr/ [a-z_] [0-9_a-z]* /x;
+my $modname_re = qr/ [A-Z_a-z] [0-9A-Z_a-z]* (?:::[0-9A-Z_a-z]+)* /x;
+my $modver_re = qr/ [0-9]+ (?: \. [0-9]+ )? /x;
+
+# Expand includes from a random group in a specific order:
+# nonvariable groups first, then their includes, then the variable groups,
+# then their includes.
+# This allows reliably marking the rest of the mod reqs as variable (this is
+# also why variable includes are currently not allowed)
+sub __expand_includes {
+  my ($groups, $seen) = @_;
+
+  # !! DIFFERENT !! behavior and return depending on invocation mode
+  # (easier to recurse this way)
+  my $is_toplevel = $seen
+    ? 0
+    : !! ($seen = {})
+  ;
+
+  my ($res_per_type, $missing_envvars);
+
+  # breadth-first evaluation, with non-variable includes on top
+  for my $g (@$groups) {
+
+    croak "Invalid requirement group name '$g': only ascii alphanumerics and _ are allowed"
+      if $g !~ qr/ \A $groupname_re \z/x;
+
+    my $r = $dbic_reqs->{$g}
+      or croak "Requirement group '$g' is not defined";
+
+    # always do this check *before* the $seen check
+    croak "Group '$g' with variable effective_modreqs can not be specified as an 'include'"
+      if ( $r->{env} and ! $is_toplevel );
 
-  return $req_availability_cache{$group} ||= do {
+    next if $seen->{$g}++;
 
-    my $deps = $class->req_list_for ($group);
+    my $req_type = 'static';
 
-    my %errors;
-    for my $mod (keys %$deps) {
-      my $req_line = "require $mod;";
-      if (my $ver = $deps->{$mod}) {
-        $req_line .= "$mod->VERSION($ver);";
+    if ( my @e = @{$r->{env}||[]} ) {
+
+      croak "Unexpected 'env' attribute under group '$g' (only allowed in test_* groups)"
+        unless $g =~ /^test_/;
+
+      croak "Unexpected *odd* list in 'env' under group '$g'"
+        if @e % 2;
+
+      # deconstruct the whole thing
+      my (@group_envnames_list, $some_envs_required, $some_required_missing);
+      while (@e) {
+        push @group_envnames_list, my $envname = shift @e;
+
+        # env required or not
+        next unless shift @e;
+
+        $some_envs_required ||= 1;
+
+        $some_required_missing ||= (
+          ! defined $ENV{$envname}
+            or
+          ! length $ENV{$envname}
+        );
       }
 
-      eval $req_line;
+      croak "None of the envvars in group '$g' declared as required, making the requirement moot"
+        unless $some_envs_required;
 
-      $errors{$mod} = $@ if $@;
+      if ($some_required_missing) {
+        push @{$missing_envvars->{$g}}, \@group_envnames_list;
+        $req_type = 'variable';
+      }
     }
 
-    my $res;
+    push @{$res_per_type->{"base_${req_type}"}}, $g;
 
-    if (keys %errors) {
-      my $missing = join (', ', map { $deps->{$_} ? "$_ >= $deps->{$_}" : $_ } (sort keys %errors) );
-      $missing .= " (see $class for details)" if $reqs->{$group}{pod};
-      $res = {
-        status => 0,
-        errorlist => \%errors,
-        missing => $missing,
-      };
+    if (my $i = $dbic_reqs->{$g}{include}) {
+      $i = [ $i ] unless ref $i eq 'ARRAY';
+
+      croak "Malformed 'include' for group '$g': must be another existing group name or arrayref of existing group names"
+        unless @$i;
+
+      push @{$res_per_type->{"incs_${req_type}"}}, @$i;
+    }
+  }
+
+  my @ret = map {
+    @{ $res_per_type->{"base_${_}"} || [] },
+    ( $res_per_type->{"incs_${_}"} ? __expand_includes( $res_per_type->{"incs_${_}"}, $seen ) : () ),
+  } qw(static variable);
+
+  return ! $is_toplevel ? @ret : do {
+    my $rv = {};
+    $rv->{$_} = {
+      idx => 1 + keys %$rv,
+      missing_envvars => $missing_envvars->{$_},
+    } for @ret;
+    $rv->{$_}{user_requested} = 1 for @$groups;
+    $rv;
+  };
+}
+
+### Private OO API
+our %req_unavailability_cache;
+
+# this method is just a lister and envvar/metadata checker - it does not try to load anything
+sub _groups_to_reqs {
+  my ($self, $want) = @_;
+
+  $want = [ $want || () ]
+    unless ref $want eq 'ARRAY';
+
+  croak "@{[ (caller(1))[3] ]}() expects a requirement group name or arrayref of group names"
+    unless @$want;
+
+  my $ret = {
+    modreqs => {},
+    modreqs_fully_documented => 1,
+  };
+
+  my $groups;
+  for my $piece (@$want) {
+    if ($piece =~ qr/ \A $groupname_re \z /x) {
+      push @$groups, $piece;
+    }
+    elsif ( my ($mod, $ver) = $piece =~ qr/ \A ($modname_re) \>\= ($modver_re) \z /x ) {
+      croak "Ad hoc module specification lists '$mod' twice"
+        if exists $ret->{modreqs}{$mod};
+
+      croak "Ad hoc module specification '${mod} >= $ver' (or greater) not listed in the test_adhoc optdep group" if (
+        ! defined $dbic_reqs->{test_adhoc}{req}{$mod}
+          or
+        $dbic_reqs->{test_adhoc}{req}{$mod} < $ver
+      );
+
+      $ret->{modreqs}{$mod} = $ver;
+      $ret->{modreqs_fully_documented} = 0;
     }
     else {
-      $res = {
-        status => 1,
-        errorlist => {},
-        missing => '',
-      };
+      croak "Unsupported argument '$piece' supplied to @{[ (caller(1))[3] ]}()"
     }
+  }
 
-    $res;
-  };
+  my $all_groups = __expand_includes($groups);
+
+  # pre-assemble list of augmentations, perform basic sanity checks
+  # Note that below we *DO NOT* respect the source/target reationship, but
+  # instead always default to augment the "later" group
+  # This is done so that the "stable/variable" boundary keeps working as
+  # expected
+  my $augmentations;
+  for my $requesting_group (keys %$all_groups) {
+    if (my $ag = $dbic_reqs->{$requesting_group}{augment}) {
+      for my $target_group (keys %$ag) {
+
+        croak "Group '$requesting_group' claims to augment a non-existent group '$target_group'"
+          unless $dbic_reqs->{$target_group};
+
+        croak "Augmentation combined with variable effective_modreqs currently unsupported for group '$requesting_group'"
+          if $dbic_reqs->{$requesting_group}{env};
+
+        croak "Augmentation of group '$target_group' with variable effective_modreqs unsupported (requested by '$requesting_group')"
+          if $dbic_reqs->{$target_group}{env};
+
+        if (my @foreign = grep { $_ ne 'req' } keys %{$ag->{$target_group}} ) {
+          croak "Only 'req' augmentations are currently supported (group '$requesting_group' attempts to alter '$foreign[0]' of group '$target_group'";
+        }
+
+        $ret->{augments}{$target_group} = 1;
+
+        # no augmentation for stuff that hasn't been selected
+        if ( $all_groups->{$target_group} and my $ar = $ag->{$target_group}{req} ) {
+          push @{$augmentations->{
+            ( $all_groups->{$requesting_group}{idx} < $all_groups->{$target_group}{idx} )
+              ? $target_group
+              : $requesting_group
+          }}, $ar;
+        }
+      }
+    }
+  }
+
+  for my $group (sort { $all_groups->{$a}{idx} <=> $all_groups->{$b}{idx} } keys %$all_groups ) {
+
+    my $group_reqs = $dbic_reqs->{$group}{req};
+
+    # sanity-check
+    for my $req_bag ($group_reqs, @{ $augmentations->{$group} || [] } ) {
+      for (keys %$req_bag) {
+
+        $_ =~ / \A $modname_re \z /x
+          or croak "Requirement '$_' in group '$group' is not a valid module name";
+
+        # !!!DO NOT CHANGE!!!
+        # remember - version.pm may not be available on the system
+        croak "Requirement '$_' in group '$group' specifies an invalid version '$req_bag->{$_}' (only plain non-underscored floating point decimals are supported)"
+          if ( ($req_bag->{$_}||0) !~ qr/ \A $modver_re \z /x );
+      }
+    }
+
+    if (my $e = $all_groups->{$group}{missing_envvars}) {
+      push @{$ret->{missing_envvars}}, @$e;
+    }
+
+    # assemble into the final ret
+    for my $type (
+      'modreqs',
+      ( $ret->{missing_envvars} ? () : 'effective_modreqs' ),
+    ) {
+      for my $req_bag ($group_reqs, @{ $augmentations->{$group} || [] } ) {
+        for my $mod (keys %$req_bag) {
+
+          $ret->{$type}{$mod} = $req_bag->{$mod}||0 if (
+
+            ! exists $ret->{$type}{$mod}
+              or
+            # we sanitized the version to be numeric above - we can just -gt it
+            ($req_bag->{$mod}||0) > $ret->{$type}{$mod}
+
+          );
+        }
+      }
+    }
+
+    $ret->{modreqs_fully_documented} &&= !!$dbic_reqs->{$group}{pod}
+      if $all_groups->{$group}{user_requested};
+
+    $ret->{release_testing_mandatory} ||= !!$dbic_reqs->{$group}{release_testing_mandatory};
+  }
+
+  return $ret;
 }
 
-sub req_group_list {
-  return { map { $_ => { %{ $reqs->{$_}{req} || {} } } } (keys %$reqs) };
+
+# this method tries to find/load specified modreqs and returns a hashref of
+# module/loaderror pairs for anything that failed
+sub _errorlist_for_modreqs {
+  # args supposedly already went through _groups_to_reqs and are therefore sanitized
+  # safe to eval at will
+  my ($self, $reqs) = @_;
+
+  my $ret;
+
+  for my $m ( keys %$reqs ) {
+    my $v = $reqs->{$m};
+
+    if (! exists $req_unavailability_cache{$m}{$v} ) {
+      local $@;
+      eval( "require $m;" . ( $v ? "$m->VERSION(q($v))" : '' ) );
+      $req_unavailability_cache{$m}{$v} = $@;
+    }
+
+    $ret->{$m} = $req_unavailability_cache{$m}{$v}
+      if $req_unavailability_cache{$m}{$v};
+  }
+
+  $ret;
+}
+
+# Unlike the above DO NOT try to load anything
+# This is executed when some needed envvars are not available
+# which in turn means a module load will never be reached anyway
+# This is important because some modules (especially DBDs) can be
+# *really* fickle when a require() is attempted, with pretty confusing
+# side-effects (especially on windows)
+sub _list_physically_missing_modules {
+  my ($self, $modreqs) = @_;
+
+  # in case there is a coderef in @INC there is nothing we can definitively prove
+  # so short circuit directly
+  return '' if grep { length ref $_ } @INC;
+
+  my @definitely_missing;
+  for my $mod (keys %$modreqs) {
+    (my $fn = $mod . '.pm') =~ s|::|/|g;
+
+    push @definitely_missing, $mod unless grep
+      # this should work on any combination of slashes
+      { $_ and -d $_ and -f "$_/$fn" and -r "$_/$fn" }
+      @INC
+    ;
+  }
+
+  join ' ', map
+    { $modreqs->{$_} ? "$_~$modreqs->{$_}" : $_ }
+    sort { lc($a) cmp lc($b) } @definitely_missing
+  ;
 }
 
+
 # This is to be called by the author only (automatically in Makefile.PL)
 sub _gen_pod {
   my ($class, $distver, $pod_dir) = @_;
@@ -762,11 +1215,16 @@ sub _gen_pod {
 
   File::Path::mkpath([$dir]);
 
-  my $sqltver = $class->req_list_for ('deploy')->{'SQL::Translator'}
+  my $sqltver = $class->req_list_for('deploy')->{'SQL::Translator'}
     or die "Hrmm? No sqlt dep?";
 
-  my @chunks = (
-    <<"EOC",
+
+  my @chunks;
+
+#@@
+#@@ HEADER
+#@@
+  push @chunks, <<"EOC";
 #########################################################################
 #####################  A U T O G E N E R A T E D ########################
 #########################################################################
@@ -775,152 +1233,415 @@ sub _gen_pod {
 # will be lost. If you need to change the generated text edit _gen_pod()
 # at the end of $modfn
 #
+
+=head1 NAME
+
+$class - Optional module dependency specifications (for module authors)
 EOC
-    '=head1 NAME',
-    "$class - Optional module dependency specifications (for module authors)",
-    '=head1 SYNOPSIS',
-    <<"EOS",
-Somewhere in your build-file (e.g. L<Module::Install>'s Makefile.PL):
+
+
+#@@
+#@@ SYNOPSIS HEADING
+#@@
+  push @chunks, <<"EOC";
+=head1 SYNOPSIS
+
+Somewhere in your build-file (e.g. L<ExtUtils::MakeMaker>'s F<Makefile.PL>):
 
   ...
 
-  configure_requires 'DBIx::Class' => '$distver';
+  \$EUMM_ARGS{CONFIGURE_REQUIRES} = {
+    \%{ \$EUMM_ARGS{CONFIGURE_REQUIRES} || {} },
+    'DBIx::Class' => '$distver',
+  };
 
-  require $class;
+  ...
 
-  my \$deploy_deps = $class->req_list_for('deploy');
+  my %DBIC_DEPLOY_AND_ORACLE_DEPS = %{ eval {
+    require $class;
+    $class->req_list_for([qw( deploy rdbms_oracle ic_dt )]);
+  } || {} };
 
-  for (keys %\$deploy_deps) {
-    requires \$_ => \$deploy_deps->{\$_};
-  }
+  \$EUMM_ARGS{PREREQ_PM} = {
+    \%DBIC_DEPLOY_AND_ORACLE_DEPS,
+    \%{ \$EUMM_ARGS{PREREQ_PM} || {} },
+  };
 
   ...
 
-Note that there are some caveats regarding C<configure_requires()>, more info
-can be found at L<Module::Install/configure_requires>
-EOS
-    '=head1 DESCRIPTION',
-    <<'EOD',
+  ExtUtils::MakeMaker::WriteMakefile(\%EUMM_ARGS);
+
+B<Note>: The C<eval> protection within the example is due to support for
+requirements during L<the C<configure> build phase|CPAN::Meta::Spec/Phases>
+not being available on a sufficient portion of production installations of
+Perl. Robust support for such dependency requirements is available in the
+L<CPAN> installer only since version C<1.94_56> first made available for
+production with perl version C<5.12>. It is the belief of the current
+maintainer that support for requirements during the C<configure> build phase
+will not be sufficiently ubiquitous until the B<year 2020> at the earliest,
+hence the extra care demonstrated above. It should also be noted that some
+3rd party installers (e.g. L<cpanminus|App::cpanminus>) do the right thing
+with configure requirements independent from the versions of perl and CPAN
+available.
+EOC
+
+
+#@@
+#@@ DESCRIPTION HEADING
+#@@
+  push @chunks, <<'EOC';
+=head1 DESCRIPTION
+
 Some of the less-frequently used features of L<DBIx::Class> have external
 module dependencies on their own. In order not to burden the average user
-with modules he will never use, these optional dependencies are not included
+with modules they will never use, these optional dependencies are not included
 in the base Makefile.PL. Instead an exception with a descriptive message is
-thrown when a specific feature is missing one or several modules required for
-its operation. This module is the central holding place for  the current list
+thrown when a specific feature can't find one or several modules required for
+its operation. This module is the central holding place for the current list
 of such dependencies, for DBIx::Class core authors, and DBIx::Class extension
 authors alike.
-EOD
-    '=head1 CURRENT REQUIREMENT GROUPS',
-    <<'EOD',
-Dependencies are organized in C<groups> and each group can list one or more
-required modules, with an optional minimum version (or 0 for any version).
-The group name can be used in the
-EOD
-  );
 
-  for my $group (sort keys %$reqs) {
-    my $p = $reqs->{$group}{pod}
-      or next;
+Dependencies are organized in L<groups|/CURRENT REQUIREMENT GROUPS> where each
+group can list one or more required modules, with an optional minimum version
+(or 0 for any version). In addition groups prefixed with C<test_> can specify
+a set of environment variables, some (or all) of which are marked as required
+for the group to be considered by L</req_list_for>
+
+Each group name (or a combination thereof) can be used in the
+L<public methods|/METHODS> as described below.
+EOC
 
-    my $modlist = $reqs->{$group}{req}
-      or next;
 
-    next unless keys %$modlist;
+#@@
+#@@ REQUIREMENT GROUPLIST HEADING
+#@@
+  push @chunks, '=head1 CURRENT REQUIREMENT GROUPS';
+
+  my $standalone_info;
+
+  for my $group (sort keys %$dbic_reqs) {
+
+    my $info = $standalone_info->{$group} ||= $class->_groups_to_reqs($group);
+
+    next unless (
+      $info->{modreqs_fully_documented}
+        and
+      ( $info->{augments} or $info->{modreqs} )
+    );
+
+    my $p = $dbic_reqs->{$group}{pod};
 
     push @chunks, (
       "=head2 $p->{title}",
-      "$p->{desc}",
+      "=head3 $group",
+      $p->{desc},
       '=over',
-      ( map { "=item * $_" . ($modlist->{$_} ? " >= $modlist->{$_}" : '') } (sort keys %$modlist) ),
-      '=back',
-      "Requirement group: B<$group>",
     );
+
+    if ( keys %{ $info->{modreqs}||{} } ) {
+      push @chunks, map
+        { "=item * $_" . ($info->{modreqs}{$_} ? " >= $info->{modreqs}{$_}" : '') }
+        ( sort keys %{ $info->{modreqs} } )
+      ;
+    }
+    else {
+      push @chunks, '=item * No standalone requirements',
+    }
+
+    push @chunks, '=back';
+
+    for my $ag ( sort keys %{ $info->{augments} || {} } ) {
+      my $ag_info = $standalone_info->{$ag} ||= $class->_groups_to_reqs($ag);
+
+      my $newreqs = $class->modreq_list_for([ $group, $ag ]);
+      for (keys %$newreqs) {
+        delete $newreqs->{$_} if (
+          ( defined $info->{modreqs}{$_}    and $info->{modreqs}{$_}    == $newreqs->{$_} )
+            or
+          ( defined $ag_info->{modreqs}{$_} and $ag_info->{modreqs}{$_} == $newreqs->{$_} )
+        );
+      }
+
+      if (keys %$newreqs) {
+        push @chunks, (
+          "Combined with L</$ag> additionally requires:",
+          '=over',
+          ( map
+            { "=item * $_" . ($newreqs->{$_} ? " >= $newreqs->{$_}" : '') }
+            ( sort keys %$newreqs )
+          ),
+          '=back',
+        );
+      }
+    }
   }
 
-  push @chunks, (
-    '=head1 METHODS',
-    '=head2 req_group_list',
-    '=over',
-    '=item Arguments: none',
-    '=item Return Value: \%list_of_requirement_groups',
-    '=back',
-    <<'EOD',
+
+#@@
+#@@ API DOCUMENTATION HEADING
+#@@
+  push @chunks, <<'EOC';
+
+=head1 IMPORT-LIKE ACTIONS
+
+Even though this module is not an L<Exporter>, it recognizes several C<actions>
+supplied to its C<import> method.
+
+=head2 -skip_all_without
+
+=over
+
+=item Arguments: @group_names
+
+=back
+
+A convenience wrapper for use during testing:
+EOC
+
+  push @chunks, " use $class -skip_all_without => qw(admin test_rdbms_mysql);";
+
+  push @chunks, 'Roughly equivalent to the following code:';
+
+  push @chunks, sprintf <<'EOS', ($class) x 2;
+
+ BEGIN {
+   require %s;
+   if ( my $missing = %s->req_missing_for(\@group_names_) ) {
+     print "1..0 # SKIP requirements not satisfied: $missing\n";
+     exit 0;
+   }
+ }
+EOS
+
+  push @chunks, <<'EOC';
+
+It also takes into account the C<RELEASE_TESTING> environment variable and
+behaves like L</-die_without> for any requirement groups marked as
+C<release_testing_mandatory>.
+
+=head2 -die_without
+
+=over
+
+=item Arguments: @group_names
+
+=back
+
+A convenience wrapper around L</die_unless_req_ok_for>:
+EOC
+
+  push @chunks, " use $class -die_without => qw(deploy admin);";
+
+  push @chunks, <<'EOC';
+
+=head2 -list_missing
+
+=over
+
+=item Arguments: @group_names
+
+=back
+
+A convenience wrapper around L</modreq_missing_for>:
+
+ perl -Ilib -MDBIx::Class::Optional::Dependencies=-list_missing,deploy,admin | cpanm
+
+=head1 METHODS
+
+=head2 req_group_list
+
+=over
+
+=item Arguments: none
+
+=item Return Value: \%list_of_requirement_groups
+
+=back
+
 This method should be used by DBIx::Class packagers, to get a hashref of all
-dependencies keyed by dependency group. Each key (group name) can be supplied
-to one of the group-specific methods below.
-EOD
-
-    '=head2 req_list_for',
-    '=over',
-    '=item Arguments: $group_name',
-    '=item Return Value: \%list_of_module_version_pairs',
-    '=back',
-    <<'EOD',
+dependencies B<keyed> by dependency group. Each key (group name), or a combination
+thereof (as an arrayref) can be supplied to the methods below.
+The B<values> of the returned hash are currently a set of options B<without a
+well defined structure>. If you have use for any of the contents - contact the
+maintainers, instead of treating this as public (left alone stable) API.
+
+=head2 req_list_for
+
+=over
+
+=item Arguments: $group_name | \@group_names
+
+=item Return Value: \%set_of_module_version_pairs
+
+=back
+
 This method should be used by DBIx::Class extension authors, to determine the
-version of modules a specific feature requires in the B<current> version of
-DBIx::Class. See the L</SYNOPSIS> for a real-world
-example.
-EOD
-
-    '=head2 req_ok_for',
-    '=over',
-    '=item Arguments: $group_name',
-    '=item Return Value: 1|0',
-    '=back',
-    <<'EOD',
-Returns true or false depending on whether all modules required by
-C<$group_name> are present on the system and loadable.
-EOD
-
-    '=head2 req_missing_for',
-    '=over',
-    '=item Arguments: $group_name',
-    '=item Return Value: $error_message_string',
-    '=back',
-    <<"EOD",
-Returns a single line string suitable for inclusion in larger error messages.
-This method would normally be used by DBIx::Class core-module author, to
-indicate to the user that he needs to install specific modules before he will
-be able to use a specific feature.
+version of modules a specific set of features requires for this version of
+DBIx::Class (regardless of their availability on the system).
+See the L</SYNOPSIS> for a real-world example.
+
+When handling C<test_*> groups this method behaves B<differently> from
+L</modreq_list_for> below (and is the only such inconsistency among the
+C<req_*> methods). If a particular group declares as requirements some
+C<environment variables> and these requirements are not satisfied (the envvars
+are unset) - then the C<module requirements> of this group are not included in
+the returned list.
+
+=head2 modreq_list_for
+
+=over
+
+=item Arguments: $group_name | \@group_names
+
+=item Return Value: \%set_of_module_version_pairs
+
+=back
+
+Same as L</req_list_for> but does not take into consideration any
+C<environment variable requirements> - returns just the list of required
+modules.
+
+=head2 req_ok_for
+
+=over
+
+=item Arguments: $group_name | \@group_names
+
+=item Return Value: 1|0
+
+=back
+
+Returns true or false depending on whether all modules/envvars required by
+the group(s) are loadable/set on the system.
+
+=head2 req_missing_for
+
+=over
+
+=item Arguments: $group_name | \@group_names
+
+=item Return Value: $error_message_string
+
+=back
+
+Returns a single-line string suitable for inclusion in larger error messages.
+This method would normally be used by DBIx::Class core features, to indicate to
+the user that they need to install specific modules and/or set specific
+environment variables before being able to use a specific feature set.
 
 For example if some of the requirements for C<deploy> are not available,
 the returned string could look like:
+EOC
 
- SQL::Translator >= $sqltver (see $class for details)
+  push @chunks, qq{ "SQL::Translator~$sqltver" (see $class documentation for details)};
 
+  push @chunks, <<'EOC';
 The author is expected to prepend the necessary text to this message before
-returning the actual error seen by the user.
-EOD
-
-    '=head2 die_unless_req_ok_for',
-    '=over',
-    '=item Arguments: $group_name',
-    '=back',
-    <<'EOD',
-Checks if L</req_ok_for> passes for the supplied C<$group_name>, and
+returning the actual error seen by the user. See also L</modreq_missing_for>
+
+=head2 modreq_missing_for
+
+=over
+
+=item Arguments: $group_name | \@group_names
+
+=item Return Value: $error_message_string
+
+=back
+
+Same as L</req_missing_for> except that the error string is guaranteed to be
+either empty, or contain a set of module requirement specifications suitable
+for piping to e.g. L<cpanminus|App::cpanminus>. The method explicitly does not
+attempt to validate the state of required environment variables (if any).
+
+For instance if some of the requirements for C<deploy> are not available,
+the returned string could look like:
+EOC
+
+  push @chunks, qq{ "SQL::Translator~$sqltver"};
+
+  push @chunks, <<'EOC';
+
+See also L</-list_missing>.
+
+=head2 skip_without
+
+=over
+
+=item Arguments: $group_name | \@group_names
+
+=back
+
+A convenience wrapper around L<skip|Test::More/SKIP>. It does not take neither
+a reason (it is generated by L</req_missing_for>) nor an amount of skipped tests
+(it is always C<1>, thus mandating unconditional use of
+L<done_testing|Test::More/done_testing>). Most useful in combination with ad hoc
+requirement specifications:
+EOC
+
+  push @chunks, <<EOC;
+  SKIP: {
+    $class->skip_without([ deploy YAML>=0.90 ]);
+
+    ...
+  }
+EOC
+
+  push @chunks, <<'EOC';
+
+=head2 die_unless_req_ok_for
+
+=over
+
+=item Arguments: $group_name | \@group_names
+
+=back
+
+Checks if L</req_ok_for> passes for the supplied group(s), and
 in case of failure throws an exception including the information
-from L</req_missing_for>.
-EOD
-
-    '=head2 req_errorlist_for',
-    '=over',
-    '=item Arguments: $group_name',
-    '=item Return Value: \%list_of_loaderrors_per_module',
-    '=back',
-    <<'EOD',
+from L</req_missing_for>. See also L</-die_without>.
+
+=head2 modreq_errorlist_for
+
+=over
+
+=item Arguments: $group_name | \@group_names
+
+=item Return Value: \%set_of_loaderrors_per_module
+
+=back
+
 Returns a hashref containing the actual errors that occurred while attempting
-to load each module in the requirement group.
-EOD
-    '=head1 AUTHOR',
-    'See L<DBIx::Class/CONTRIBUTORS>.',
-    '=head1 LICENSE',
-    'You may distribute this code under the same terms as Perl itself',
-  );
+to load each module in the requirement group(s).
+
+=head2 req_errorlist_for
+
+Deprecated method name, equivalent (via proxy) to L</modreq_errorlist_for>.
+
+EOC
+
+#@@
+#@@ FOOTER
+#@@
+  push @chunks, <<'EOC';
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+EOC
 
-  open (my $fh, '>', $podfn) or Carp::croak "Unable to write to $podfn: $!";
-  print $fh join ("\n\n", @chunks);
-  print $fh "\n";
-  close ($fh);
+  eval {
+    open (my $fh, '>', $podfn) or die;
+    print $fh join ("\n\n", @chunks) or die;
+    print $fh "\n" or die;
+    close ($fh) or die;
+  } or croak( "Unable to write $podfn: " . ( $! || $@ || 'unknown error') );
 }
 
 1;
index 5e40dc0..4c9a14c 100644 (file)
@@ -147,7 +147,7 @@ Returns an B<ordered> resultset of all other objects in the same
 group excluding the one you called it on.
 
 The ordering is a backwards-compatibility artifact - if you need
-a resultset with no ordering applied use L</_siblings>
+a resultset with no ordering applied use C<_siblings>
 
 =cut
 sub siblings {
@@ -367,7 +367,30 @@ sub move_to {
 
     my $position_column = $self->position_column;
 
-    if ($self->is_column_changed ($position_column) ) {
+    my $is_txn;
+    if ($is_txn = $self->result_source->schema->storage->transaction_depth) {
+      # Reload position state from storage
+      # The thinking here is that if we are in a transaction, it is
+      # *more likely* the object went out of sync due to resultset
+      # level shenanigans. Instead of always reloading (slow) - go
+      # ahead and hand-hold only in the case of higher layers
+      # requesting the safety of a txn
+
+      $self->store_column(
+        $position_column,
+        ( $self->result_source
+                ->resultset
+                 ->search($self->_storage_ident_condition, { rows => 1, columns => $position_column })
+                  ->cursor
+                   ->next
+        )[0] || $self->throw_exception(
+          sprintf "Unable to locate object '%s' in storage - object went ouf of sync...?",
+          $self->ID
+        ),
+      );
+      delete $self->{_dirty_columns}{$position_column};
+    }
+    elsif ($self->is_column_changed ($position_column) ) {
       # something changed our position, we need to know where we
       # used to be - use the stashed value
       $self->store_column($position_column, delete $self->{_column_data_in_storage}{$position_column});
@@ -380,7 +403,7 @@ sub move_to {
       return 0;
     }
 
-    my $guard = $self->result_source->schema->txn_scope_guard;
+    my $guard = $is_txn ? undef : $self->result_source->schema->txn_scope_guard;
 
     my ($direction, @between);
     if ( $from_position < $to_position ) {
@@ -402,7 +425,7 @@ sub move_to {
     $self->_shift_siblings ($direction, @between);
     $self->_ordered_internal_update({ $position_column => $new_pos_val });
 
-    $guard->commit;
+    $guard->commit if $guard;
     return 1;
 }
 
@@ -719,20 +742,13 @@ sub _shift_siblings {
     if (
       first { $_ eq $position_column } ( map { @$_ } (values %{{ $rsrc->unique_constraints }} ) )
     ) {
-        my $cursor = $shift_rs->search (
+        my $clean_rs = $rsrc->resultset;
+
+        for ( $shift_rs->search (
           {}, { order_by => { "-$ord", $position_column }, select => [$position_column, @pcols] }
-        )->cursor;
-        my $rs = $rsrc->resultset;
-
-        my @all_data = $cursor->all;
-        while (my $data = shift @all_data) {
-          my $pos = shift @$data;
-          my $cond;
-          for my $i (0.. $#pcols) {
-            $cond->{$pcols[$i]} = $data->[$i];
-          }
-
-          $rs->find($cond)->update ({ $position_column => $pos + ( ($op eq '+') ? 1 : -1 ) });
+        )->cursor->all ) {
+          my $pos = shift @$_;
+          $clean_rs->find(@$_)->update ({ $position_column => $pos + ( ($op eq '+') ? 1 : -1 ) });
         }
     }
     else {
@@ -861,33 +877,31 @@ will prevent such race conditions going undetected.
 
 =head2 Multiple Moves
 
-Be careful when issuing move_* methods to multiple objects.  If
-you've pre-loaded the objects then when you move one of the objects
-the position of the other object will not reflect their new value
-until you reload them from the database - see
-L<DBIx::Class::Row/discard_changes>.
+If you have multiple same-group result objects already loaded from storage,
+you need to be careful when executing C<move_*> operations on them:
+without a L</position_column> reload the L</_position_value> of the
+"siblings" will be out of sync with the underlying storage.
+
+Starting from version C<0.082800> DBIC will implicitly perform such
+reloads when the C<move_*> happens as a part of a transaction
+(a good example of such situation is C<< $ordered_resultset->delete_all >>).
 
-There are times when you will want to move objects as groups, such
-as changing the parent of several objects at once - this directly
-conflicts with this problem.  One solution is for us to write a
-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.
+If it is not possible for you to wrap the entire call-chain in a transaction,
+you will need to call L<DBIx::Class::Row/discard_changes> to get an object
+up-to-date before proceeding, otherwise undefined behavior will result.
 
 =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
-
- Original code framework
-   Aran Deltac <bluefeet@cpan.org>
-
- Constraints support and code generalisation
-   Peter Rabbitson <ribasushi@cpan.org>
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index cb204b7..9bda5ca 100644 (file)
@@ -87,7 +87,7 @@ sub ID {
 
 sub _create_ID {
   my ($self, %vals) = @_;
-  return undef unless 0 == grep { !defined } values %vals;
+  return undef if grep { !defined } values %vals;
   return join '|', ref $self || $self, $self->result_source->name,
     map { $_ . '=' . $vals{$_} } sort keys %vals;
 }
@@ -134,15 +134,17 @@ sub _mk_ident_cond {
   return \%cond;
 }
 
-1;
-
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
+1;
index 26bd6df..b4d509c 100644 (file)
@@ -7,6 +7,8 @@ use warnings;
 
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::PK::Auto - Automatic primary key class
@@ -41,12 +43,13 @@ The code that was handled here is now in Row for efficiency.
 The code that was handled here is now in ResultSource, and is being proxied to
 Row as well.
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index c7fed59..1962d79 100644 (file)
@@ -10,6 +10,8 @@ __PACKAGE__->load_components(qw/PK::Auto/);
 
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::PK::Auto::DB2 - (DEPRECATED) Automatic primary key class for DB2
@@ -18,12 +20,13 @@ DBIx::Class::PK::Auto::DB2 - (DEPRECATED) Automatic primary key class for DB2
 
 Just load PK::Auto instead; auto-inc is now handled by Storage.
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index ce0ee2c..5704ffe 100644 (file)
@@ -10,6 +10,8 @@ __PACKAGE__->load_components(qw/PK::Auto/);
 
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::PK::Auto::MSSQL - (DEPRECATED) Automatic primary key class for MSSQL
@@ -18,12 +20,13 @@ DBIx::Class::PK::Auto::MSSQL - (DEPRECATED) Automatic primary key class for MSSQ
 
 Just load PK::Auto instead; auto-inc is now handled by Storage.
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index fd152f7..10fbfd6 100644 (file)
@@ -10,6 +10,8 @@ __PACKAGE__->load_components(qw/PK::Auto/);
 
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::PK::Auto::MySQL - (DEPRECATED) Automatic primary key class for MySQL
@@ -18,12 +20,13 @@ DBIx::Class::PK::Auto::MySQL - (DEPRECATED) Automatic primary key class for MySQ
 
 Just load PK::Auto instead; auto-inc is now handled by Storage.
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 45e4b0d..391b72a 100644 (file)
@@ -10,6 +10,8 @@ __PACKAGE__->load_components(qw/PK::Auto/);
 
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::PK::Auto::Oracle - (DEPRECATED) Automatic primary key class for Oracle
@@ -18,12 +20,13 @@ DBIx::Class::PK::Auto::Oracle - (DEPRECATED) Automatic primary key class for Ora
 
 Just load PK::Auto instead; auto-inc is now handled by Storage.
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index a1b24cd..1ad32b6 100644 (file)
@@ -10,6 +10,8 @@ __PACKAGE__->load_components(qw/PK::Auto/);
 
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::PK::Auto::Pg - (DEPRECATED) Automatic primary key class for Pg
@@ -18,12 +20,13 @@ DBIx::Class::PK::Auto::Pg - (DEPRECATED) Automatic primary key class for Pg
 
 Just load PK::Auto instead; auto-inc is now handled by Storage.
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 3bc5c5e..e98f7f0 100644 (file)
@@ -10,6 +10,8 @@ __PACKAGE__->load_components(qw/PK::Auto/);
 
 1;
 
+__END__
+
 =head1 NAME
 
 DBIx::Class::PK::Auto::SQLite - (DEPRECATED) Automatic primary key class for SQLite
@@ -18,12 +20,13 @@ DBIx::Class::PK::Auto::SQLite - (DEPRECATED) Automatic primary key class for SQL
 
 Just load PK::Auto instead; auto-inc is now handled by Storage.
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 427b5aa..195514c 100644 (file)
@@ -13,6 +13,10 @@ __PACKAGE__->load_own_components(qw/
   Base
 /);
 
+1;
+
+__END__
+
 =head1 NAME
 
 DBIx::Class::Relationship - Inter-table relationships
@@ -105,7 +109,7 @@ L<DBIx::Class::Relationship::Base>.
 
 All helper methods are called similar to the following template:
 
-  __PACKAGE__->$method_name('relname', 'Foreign::Class', \%cond|\@cond|\&cond?, \%attrs?);
+  __PACKAGE__->$method_name('rel_name', 'Foreign::Class', \%cond|\@cond|\&cond?, \%attrs?);
 
 Both C<cond> and C<attrs> are optional. Pass C<undef> for C<cond> if
 you want to use the default value for it, but still want to set C<attrs>.
@@ -327,7 +331,7 @@ The second is almost exactly the same as the accessor method but "_rs"
 is added to the end of the method name, eg C<$accessor_name_rs()>.
 This method works just like the normal accessor, except that it always
 returns a resultset, even in list context. The third method, named C<<
-add_to_$relname >>, will also be added to your Row items; this allows
+add_to_$rel_name >>, will also be added to your Row items; this allows
 you to insert new related items, using the same mechanism as in
 L<DBIx::Class::Relationship::Base/"create_related">.
 
@@ -629,17 +633,13 @@ L<DBIx::Class::ResultSet> for a L<list of standard resultset
 attributes|DBIx::Class::ResultSet/ATTRIBUTES> which can be assigned to
 relationships as well.
 
-=cut
-
-1;
-
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
-
-=head1 LICENSE
+=head1 FURTHER QUESTIONS?
 
-You may distribute this code under the same terms as Perl itself.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=cut
+=head1 COPYRIGHT AND LICENSE
 
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 3a12f28..40deeaf 100644 (file)
@@ -3,9 +3,8 @@ package # hide from PAUSE
 
 use strict;
 use warnings;
-use Sub::Name;
 use DBIx::Class::Carp;
-use DBIx::Class::_Util 'fail_on_internal_wantarray';
+use DBIx::Class::_Util qw(quote_sub perlstring);
 use namespace::clean;
 
 our %_pod_inherit_config =
@@ -23,81 +22,91 @@ sub register_relationship {
 
 sub add_relationship_accessor {
   my ($class, $rel, $acc_type) = @_;
-  my %meth;
+
   if ($acc_type eq 'single') {
-    my $rel_info = $class->relationship_info($rel);
-    $meth{$rel} = sub {
+    quote_sub "${class}::${rel}" => sprintf(<<'EOC', perlstring $rel);
       my $self = shift;
+
       if (@_) {
-        $self->set_from_related($rel, @_);
-        return $self->{_relationship_data}{$rel} = $_[0];
-      } elsif (exists $self->{_relationship_data}{$rel}) {
-        return $self->{_relationship_data}{$rel};
-      } else {
-        my $cond = $self->result_source->_resolve_condition(
-          $rel_info->{cond}, $rel, $self, $rel
+        $self->set_from_related( %1$s => @_ );
+        return $self->{_relationship_data}{%1$s} = $_[0];
+      }
+      elsif (exists $self->{_relationship_data}{%1$s}) {
+        return $self->{_relationship_data}{%1$s};
+      }
+      else {
+        my $relcond = $self->result_source->_resolve_relationship_condition(
+          rel_name => %1$s,
+          foreign_alias => %1$s,
+          self_alias => 'me',
+          self_result_object => $self,
+        );
+
+        return undef if (
+          $relcond->{join_free_condition}
+            and
+          $relcond->{join_free_condition} ne DBIx::Class::_Util::UNRESOLVABLE_CONDITION
+            and
+          scalar grep { not defined $_ } values %%{ $relcond->{join_free_condition} || {} }
+            and
+          $self->result_source->relationship_info(%1$s)->{attrs}{undef_on_null_fk}
         );
-        if ($rel_info->{attrs}->{undef_on_null_fk}){
-          return undef unless ref($cond) eq 'HASH';
-          return undef if grep { not defined $_ } values %$cond;
-        }
-        my $val = $self->find_related($rel, {}, {});
+
+        my $val = $self->search_related( %1$s )->single;
         return $val unless $val;  # $val instead of undef so that null-objects can go through
 
-        return $self->{_relationship_data}{$rel} = $val;
+        return $self->{_relationship_data}{%1$s} = $val;
       }
-    };
-  } elsif ($acc_type eq 'filter') {
+EOC
+  }
+  elsif ($acc_type eq 'filter') {
     $class->throw_exception("No such column '$rel' to filter")
        unless $class->has_column($rel);
+
     my $f_class = $class->relationship_info($rel)->{class};
-    $class->inflate_column($rel,
-      { inflate => sub {
-          my ($val, $self) = @_;
-          return $self->find_or_new_related($rel, {}, {});
-        },
-        deflate => sub {
-          my ($val, $self) = @_;
-          $self->throw_exception("'$val' isn't a $f_class") unless $val->isa($f_class);
-
-          # MASSIVE FIXME - this code assumes we pointed at the PK, but the belongs_to
-          # helper does not check any of this
-          # fixup the code a bit to make things saner, but ideally 'filter' needs to
-          # be deprecated ASAP and removed shortly after
-          # Not doing so before 0.08250 however, too many things in motion already
-          my ($pk_col, @rest) = $val->result_source->_pri_cols_or_die;
-          $self->throw_exception(
-            "Relationship '$rel' of type 'filter' can not work with a multicolumn primary key on source '$f_class'"
-          ) if @rest;
-
-          my $pk_val = $val->get_column($pk_col);
-          carp_unique (
-            "Unable to deflate 'filter'-type relationship '$rel' (related object "
-          . "primary key not retrieved), assuming undef instead"
-          ) if ( ! defined $pk_val and $val->in_storage );
-
-          return $pk_val;
-        }
-      }
-    );
-  } elsif ($acc_type eq 'multi') {
-    $meth{$rel} = sub {
-      DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and wantarray and my $sog = fail_on_internal_wantarray($_[0]);
-      shift->search_related($rel, @_)
-    };
-    $meth{"${rel}_rs"} = sub { shift->search_related_rs($rel, @_) };
-    $meth{"add_to_${rel}"} = sub { shift->create_related($rel, @_); };
-  } else {
-    $class->throw_exception("No such relationship accessor type '$acc_type'");
+
+    $class->inflate_column($rel, {
+      inflate => sub {
+        my ($val, $self) = @_;
+        return $self->find_or_new_related($rel, {}, {});
+      },
+      deflate => sub {
+        my ($val, $self) = @_;
+        $self->throw_exception("'$val' isn't a $f_class") unless $val->isa($f_class);
+
+        # MASSIVE FIXME - this code assumes we pointed at the PK, but the belongs_to
+        # helper does not check any of this
+        # fixup the code a bit to make things saner, but ideally 'filter' needs to
+        # be deprecated ASAP and removed shortly after
+        # Not doing so before 0.08250 however, too many things in motion already
+        my ($pk_col, @rest) = $val->result_source->_pri_cols_or_die;
+        $self->throw_exception(
+          "Relationship '$rel' of type 'filter' can not work with a multicolumn primary key on source '$f_class'"
+        ) if @rest;
+
+        my $pk_val = $val->get_column($pk_col);
+        carp_unique (
+          "Unable to deflate 'filter'-type relationship '$rel' (related object "
+        . "primary key not retrieved), assuming undef instead"
+        ) if ( ! defined $pk_val and $val->in_storage );
+
+        return $pk_val;
+      },
+    });
   }
-  {
-    no strict 'refs';
-    no warnings 'redefine';
-    foreach my $meth (keys %meth) {
-      my $name = join '::', $class, $meth;
-      *$name = subname($name, $meth{$meth});
-    }
+  elsif ($acc_type eq 'multi') {
+
+    quote_sub "${class}::${rel}_rs", "shift->search_related_rs( $rel => \@_ )";
+    quote_sub "${class}::add_to_${rel}", "shift->create_related( $rel => \@_ )";
+    quote_sub "${class}::${rel}", sprintf( <<'EOC', perlstring $rel );
+      DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and my $sog = DBIx::Class::_Util::fail_on_internal_wantarray;
+      shift->search_related( %s => @_ )
+EOC
+  }
+  else {
+    $class->throw_exception("No such relationship accessor type '$acc_type'");
   }
+
 }
 
 1;
index 20a9c17..f5d34f8 100644 (file)
@@ -7,6 +7,7 @@ use base qw/DBIx::Class/;
 
 use Scalar::Util qw/weaken blessed/;
 use Try::Tiny;
+use DBIx::Class::_Util 'UNRESOLVABLE_CONDITION';
 use namespace::clean;
 
 =head1 NAME
@@ -38,11 +39,11 @@ methods, for predefined ones, look in L<DBIx::Class::Relationship>.
 
 =over 4
 
-=item Arguments: 'relname', 'Foreign::Class', $condition, $attrs
+=item Arguments: $rel_name, $foreign_class, $condition, $attrs
 
 =back
 
-  __PACKAGE__->add_relationship('relname',
+  __PACKAGE__->add_relationship('rel_name',
                                 'Foreign::Class',
                                 $condition, $attrs);
 
@@ -180,11 +181,32 @@ L<SQL::Abstract> and the resulting SQL will be used verbatim as the C<ON>
 clause of the C<JOIN> statement associated with this relationship.
 
 While every coderef-based condition must return a valid C<ON> clause, it may
-elect to additionally return a simplified join-free condition hashref when
-invoked as C<< $result->relationship >>, as opposed to
-C<< $rs->related_resultset('relationship') >>. In this case C<$result> is
-passed to the coderef as C<< $args->{self_rowobj} >>, so a user can do the
-following:
+elect to additionally return a simplified B<optional> join-free condition
+consisting of a hashref with B<all keys being fully qualified names of columns
+declared on the corresponding result source>. This boils down to two scenarios:
+
+=over
+
+=item *
+
+When relationship resolution is invoked after C<< $result->$rel_name >>, as
+opposed to C<< $rs->related_resultset($rel_name) >>, the C<$result> object
+is passed to the coderef as C<< $args->{self_result_object} >>.
+
+=item *
+
+Alternatively when the user-space invokes resolution via
+C<< $result->set_from_related( $rel_name => $foreign_values_or_object ) >>, the
+corresponding data is passed to the coderef as C<< $args->{foreign_values} >>,
+B<always> in the form of a hashref. If a foreign result object is supplied
+(which is valid usage of L</set_from_related>), its values will be extracted
+into hashref form by calling L<get_columns|DBIx::Class::Row/get_columns>.
+
+=back
+
+Note that the above scenarios are mutually exclusive, that is you will be supplied
+none or only one of C<self_result_object> and C<foreign_values>. In other words if
+you define your condition coderef as:
 
   sub {
     my $args = shift;
@@ -194,14 +216,17 @@ following:
         "$args->{foreign_alias}.artist" => { -ident => "$args->{self_alias}.artistid" },
         "$args->{foreign_alias}.year"   => { '>', "1979", '<', "1990" },
       },
-      $args->{self_rowobj} && {
-        "$args->{foreign_alias}.artist" => $args->{self_rowobj}->artistid,
+      ! $args->{self_result_object} ? () : {
+        "$args->{foreign_alias}.artist" => $args->{self_result_object}->artistid,
         "$args->{foreign_alias}.year"   => { '>', "1979", '<', "1990" },
       },
+      ! $args->{foreign_values} ? () : {
+        "$args->{self_alias}.artistid" => $args->{foreign_values}{artist},
+      }
     );
   }
 
-Now this code:
+Then this code:
 
     my $artist = $schema->resultset("Artist")->find({ id => 4 });
     $artist->cds_80s->all;
@@ -218,25 +243,46 @@ With the bind values:
 
     '4', '1990', '1979'
 
-Note that in order to be able to use
-L<< $result->create_related|DBIx::Class::Relationship::Base/create_related >>,
-the coderef must not only return as its second such a "simple" condition
-hashref which does not depend on joins being available, but the hashref must
-contain only plain values/deflatable objects, such that the result can be
-passed directly to L<DBIx::Class::Relationship::Base/set_from_related>. For
-instance the C<year> constraint in the above example prevents the relationship
-from being used to create related objects (an exception will be thrown).
+While this code:
+
+    my $cd = $schema->resultset("CD")->search({ artist => 1 }, { rows => 1 })->single;
+    my $artist = $schema->resultset("Artist")->new({});
+    $artist->set_from_related('cds_80s');
+
+Will properly set the C<< $artist->artistid >> field of this new object to C<1>
+
+Note that in order to be able to use L</set_from_related> (and by extension
+L<< $result->create_related|DBIx::Class::Relationship::Base/create_related >>),
+the returned join free condition B<must> contain only plain values/deflatable
+objects. For instance the C<year> constraint in the above example prevents
+the relationship from being used to create related objects using
+C<< $artst->create_related( cds_80s => { title => 'blah' } ) >> (an
+exception will be thrown).
 
 In order to allow the user to go truly crazy when generating a custom C<ON>
 clause, the C<$args> hashref passed to the subroutine contains some extra
 metadata. Currently the supplied coderef is executed as:
 
   $relationship_info->{cond}->({
-    self_alias        => The alias of the invoking resultset ('me' in case of a result object),
-    foreign_alias     => The alias of the to-be-joined resultset (often matches relname),
-    self_resultsource => The invocant's resultsource,
-    foreign_relname   => The relationship name (does *not* always match foreign_alias),
-    self_rowobj       => The invocant itself in case of a $result_object->$relationship call
+    self_resultsource   => The resultsource instance on which rel_name is registered
+    rel_name            => The relationship name (does *NOT* always match foreign_alias)
+
+    self_alias          => The alias of the invoking resultset
+    foreign_alias       => The alias of the to-be-joined resultset (does *NOT* always match rel_name)
+
+    # only one of these (or none at all) will ever be supplied to aid in the
+    # construction of a join-free condition
+
+    self_result_object  => The invocant *object* itself in case of a call like
+                           $result_object->$rel_name( ... )
+
+    foreign_values      => A *hashref* of related data: may be passed in directly or
+                           derived via ->get_columns() from a related object in case of
+                           $result_object->set_from_related( $rel_name, $foreign_result_object )
+
+    # deprecated inconsistent names, will be forever available for legacy code
+    self_rowobj         => Old deprecated slot for self_result_object
+    foreign_relname     => Old deprecated slot for rel_name
   });
 
 =head3 attributes
@@ -288,7 +334,7 @@ Then, assuming MyApp::Schema::LinerNotes has an accessor named notes, you can do
 
 For a 'belongs_to relationship, note the 'cascade_update':
 
-  MyApp::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd,
+  MyApp::Schema::Track->belongs_to( cd => 'MyApp::Schema::CD', 'cd,
       { proxy => ['title'], cascade_update => 1 }
   );
   $track->title('New Title');
@@ -299,7 +345,7 @@ For a 'belongs_to relationship, note the 'cascade_update':
 A hashref where each key is the accessor you want installed in the main class,
 and its value is the name of the original in the foreign class.
 
-  MyApp::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd', {
+  MyApp::Schema::Track->belongs_to( cd => 'MyApp::Schema::CD', 'cd', {
       proxy => { cd_title => 'title' },
   });
 
@@ -309,7 +355,7 @@ This will create an accessor named C<cd_title> on the C<$track> result object.
 
 NOTE: you can pass a nested struct too, for example:
 
-  MyApp::Schema::Track->belongs_to( cd => 'DBICTest::Schema::CD', 'cd', {
+  MyApp::Schema::Track->belongs_to( cd => 'MyApp::Schema::CD', 'cd', {
     proxy => [ 'year', { cd_title => 'title' } ],
   });
 
@@ -360,7 +406,7 @@ the relationship attributes.
 
 The C<belongs_to> relationship does not update across relationships
 by default, so if you have a 'proxy' attribute on a belongs_to and want to
-use 'update' on it, you muse set C<< cascade_update => 1 >>.
+use 'update' on it, you must set C<< cascade_update => 1 >>.
 
 This is not a RDMS style cascade update - it purely means that when
 an object has update called on it, all the related objects also
@@ -453,48 +499,49 @@ this instance (like in the case of C<might_have> relationships).
 =cut
 
 sub related_resultset {
-  my $self = shift;
+  $_[0]->throw_exception(
+    '$result->related_resultset() no longer accepts extra search arguments, '
+  . 'you need to switch to ...->related_resultset($relname)->search_rs(...) '
+  . 'instead (it was never documented and more importantly could never work '
+  . 'reliably due to the heavy caching involved)'
+  ) if @_ > 2;
 
-  $self->throw_exception("Can't call *_related as class methods")
-    unless ref $self;
+  $_[0]->throw_exception("Can't call *_related as class methods")
+    unless ref $_[0];
 
-  my $rel = shift;
+  return $_[0]->{related_resultsets}{$_[1]}
+    if defined $_[0]->{related_resultsets}{$_[1]};
 
-  return $self->{related_resultsets}{$rel}
-    if defined $self->{related_resultsets}{$rel};
+  my ($self, $rel) = @_;
 
   return $self->{related_resultsets}{$rel} = do {
 
-    my $rel_info = $self->relationship_info($rel)
-      or $self->throw_exception( "No such relationship '$rel'" );
+    my $rsrc = $self->result_source;
 
-    my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {});
-    $attrs = { %{$rel_info->{attrs} || {}}, %$attrs };
+    my $rel_info = $rsrc->relationship_info($rel)
+      or $self->throw_exception( "No such relationship '$rel'" );
 
-    $self->throw_exception( "Invalid query: @_" )
-      if (@_ > 1 && (@_ % 2 == 1));
-    my $query = ((@_ > 1) ? {@_} : shift);
+    my $cond_res = $rsrc->_resolve_relationship_condition(
+      rel_name => $rel,
+      self_result_object => $self,
 
-    my $rsrc = $self->result_source;
+      # this may look weird, but remember that we are making a resultset
+      # out of an existing object, with the new source being at the head
+      # of the FROM chain. Having a 'me' alias is nothing but expected there
+      foreign_alias => 'me',
 
-    # condition resolution may fail if an incomplete master-object prefetch
-    # is encountered - that is ok during prefetch construction (not yet in_storage)
-    my ($cond, $is_crosstable) = try {
-      $rsrc->_resolve_condition( $rel_info->{cond}, $rel, $self, $rel )
-    }
-    catch {
-      if ($self->in_storage) {
-        $self->throw_exception ($_);
-      }
+      self_alias => "!!!\xFF()!!!_SHOULD_NEVER_BE_SEEN_IN_USE_!!!()\xFF!!!",
 
-      $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION;  # RV
-    };
+      # not strictly necessary, but shouldn't hurt either
+      require_join_free_condition => !!(ref $rel_info->{cond} ne 'CODE'),
+    );
 
     # keep in mind that the following if() block is part of a do{} - no return()s!!!
-    if ($is_crosstable) {
-      $self->throw_exception (
-        "A cross-table relationship condition returned for statically declared '$rel'"
-      ) unless ref $rel_info->{cond} eq 'CODE';
+    if (
+      ! $cond_res->{join_free_condition}
+        and
+      ref $rel_info->{cond} eq 'CODE'
+    ) {
 
       # A WHOREIFFIC hack to reinvoke the entire condition resolution
       # with the correct alias. Another way of doing this involves a
@@ -506,20 +553,28 @@ sub related_resultset {
       # root alias as 'me', instead of $rel (as opposed to invoking
       # $rs->search_related)
 
-      local $rsrc->{_relationships}{me} = $rsrc->{_relationships}{$rel};  # make the fake 'me' rel
+      # make the fake 'me' rel
+      local $rsrc->{_relationships}{me} = {
+        %{ $rsrc->{_relationships}{$rel} },
+        _original_name => $rel,
+      };
+
       my $obj_table_alias = lc($rsrc->source_name) . '__row';
       $obj_table_alias =~ s/\W+/_/g;
 
       $rsrc->resultset->search(
         $self->ident_condition($obj_table_alias),
         { alias => $obj_table_alias },
-      )->search_related('me', $query, $attrs)
+      )->search_related('me', undef, $rel_info->{attrs})
     }
     else {
+
       # FIXME - this conditional doesn't seem correct - got to figure out
       # at some point what it does. Also the entire UNRESOLVABLE_CONDITION
       # business seems shady - we could simply not query *at all*
-      if ($cond eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION) {
+      my $attrs;
+      if ( $cond_res->{join_free_condition} eq UNRESOLVABLE_CONDITION ) {
+        $attrs = { %{$rel_info->{attrs}} };
         my $reverse = $rsrc->reverse_relationship_info($rel);
         foreach my $rev_rel (keys %$reverse) {
           if ($reverse->{$rev_rel}{attrs}{accessor} && $reverse->{$rev_rel}{attrs}{accessor} eq 'multi') {
@@ -529,29 +584,10 @@ sub related_resultset {
           }
         }
       }
-      elsif (ref $cond eq 'ARRAY') {
-        $cond = [ map {
-          if (ref $_ eq 'HASH') {
-            my $hash;
-            foreach my $key (keys %$_) {
-              my $newkey = $key !~ /\./ ? "me.$key" : $key;
-              $hash->{$newkey} = $_->{$key};
-            }
-            $hash;
-          } else {
-            $_;
-          }
-        } @$cond ];
-      }
-      elsif (ref $cond eq 'HASH') {
-       foreach my $key (grep { ! /\./ } keys %$cond) {
-          $cond->{"me.$key"} = delete $cond->{$key};
-        }
-      }
 
-      $query = ($query ? { '-and' => [ $cond, $query ] } : $cond);
       $rsrc->related_source($rel)->resultset->search(
-        $query, $attrs
+        $cond_res->{join_free_condition},
+        $attrs || $rel_info->{attrs},
       );
     }
   };
@@ -627,38 +663,15 @@ your storage until you call L<DBIx::Class::Row/insert> on it.
 =cut
 
 sub new_related {
-  my ($self, $rel, $values) = @_;
-
-  # FIXME - this is a bad position for this (also an identical copy in
-  # set_from_related), but I have no saner way to hook, and I absolutely
-  # want this to throw at least for coderefs, instead of the "insert a NULL
-  # when it gets hard" insanity --ribasushi
-  #
-  # sanity check - currently throw when a complex coderef rel is encountered
-  # FIXME - should THROW MOAR!
-
-  if (ref $self) {  # cdbi calls this as a class method, /me vomits
-
-    my $rsrc = $self->result_source;
-    my $rel_info = $rsrc->relationship_info($rel)
-      or $self->throw_exception( "No such relationship '$rel'" );
-    my (undef, $crosstable, $cond_targets) = $rsrc->_resolve_condition (
-      $rel_info->{cond}, $rel, $self, $rel
-    );
-
-    $self->throw_exception("Custom relationship '$rel' does not resolve to a join-free condition fragment")
-      if $crosstable;
-
-    if (my @unspecified_rel_condition_chunks = grep { ! exists $values->{$_} } @{$cond_targets||[]} ) {
-      $self->throw_exception(sprintf (
-        "Custom relationship '%s' not definitive - returns conditions instead of values for column(s): %s",
-        $rel,
-        map { "'$_'" } @unspecified_rel_condition_chunks
-      ));
-    }
-  }
-
-  return $self->search_related($rel)->new_result($values);
+  my ($self, $rel, $data) = @_;
+
+  return $self->search_related($rel)->new_result( $self->result_source->_resolve_relationship_condition (
+    infer_values_based_on => $data,
+    rel_name => $rel,
+    self_result_object => $self,
+    foreign_alias => $rel,
+    self_alias => 'me',
+  )->{inferred_values} );
 }
 
 =head2 create_related
@@ -792,44 +805,21 @@ call set_from_related on the book.
 This is called internally when you pass existing objects as values to
 L<DBIx::Class::ResultSet/create>, or pass an object to a belongs_to accessor.
 
-The columns are only set in the local copy of the object, call L</update> to
-set them in the storage.
+The columns are only set in the local copy of the object, call
+L<update|DBIx::Class::Row/update> to update them in the storage.
 
 =cut
 
 sub set_from_related {
   my ($self, $rel, $f_obj) = @_;
 
-  my $rsrc = $self->result_source;
-  my $rel_info = $rsrc->relationship_info($rel)
-    or $self->throw_exception( "No such relationship '$rel'" );
-
-  if (defined $f_obj) {
-    my $f_class = $rel_info->{class};
-    $self->throw_exception( "Object '$f_obj' isn't a ".$f_class )
-      unless blessed $f_obj and $f_obj->isa($f_class);
-  }
-
-
-  # FIXME - this is a bad position for this (also an identical copy in
-  # new_related), but I have no saner way to hook, and I absolutely
-  # want this to throw at least for coderefs, instead of the "insert a NULL
-  # when it gets hard" insanity --ribasushi
-  #
-  # sanity check - currently throw when a complex coderef rel is encountered
-  # FIXME - should THROW MOAR!
-  my ($cond, $crosstable, $cond_targets) = $rsrc->_resolve_condition (
-    $rel_info->{cond}, $f_obj, $rel, $rel
-  );
-  $self->throw_exception("Custom relationship '$rel' does not resolve to a join-free condition fragment")
-    if $crosstable;
-  $self->throw_exception(sprintf (
-    "Custom relationship '%s' not definitive - returns conditions instead of values for column(s): %s",
-    $rel,
-    map { "'$_'" } @$cond_targets
-  )) if $cond_targets;
-
-  $self->set_columns($cond);
+  $self->set_columns( $self->result_source->_resolve_relationship_condition (
+    infer_values_based_on => {},
+    rel_name => $rel,
+    foreign_values => $f_obj,
+    foreign_alias => $rel,
+    self_alias => 'me',
+  )->{inferred_values} );
 
   return 1;
 }
@@ -986,13 +976,16 @@ Removes the link between the current object and the related object. Note that
 the related object itself won't be deleted unless you call ->delete() on
 it. This method just removes the link between the two objects.
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index b594d3a..a3e7dbc 100644 (file)
@@ -7,6 +7,7 @@ package # hide from PAUSE
 use strict;
 use warnings;
 use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 use namespace::clean;
 
 our %_pod_inherit_config =
@@ -41,7 +42,7 @@ sub belongs_to {
     )  unless $class->has_column($f_key);
 
     $class->ensure_class_loaded($f_class);
-    my $f_rsrc = try {
+    my $f_rsrc = dbic_internal_try {
       $f_class->result_source_instance;
     }
     catch {
@@ -60,6 +61,8 @@ sub belongs_to {
   else {
     if (ref $cond eq 'HASH') { # ARRAY is also valid
       my $cond_rel;
+      # FIXME This loop is ridiculously incomplete and dangerous
+      # staving off changes until implmentation of the swindon consensus
       for (keys %$cond) {
         if (m/\./) { # Explicit join condition
           $cond_rel = $cond;
@@ -89,6 +92,7 @@ sub belongs_to {
   $class->add_relationship($rel, $f_class,
     $cond,
     {
+      is_depends_on => 1,
       accessor => $acc_type,
       $fk_columns ? ( fk_columns => $fk_columns ) : (),
       %{$attrs || {}}
@@ -98,14 +102,4 @@ sub belongs_to {
   return 1;
 }
 
-# Attempt to remove the POD so it (maybe) falls off the indexer
-
-#=head1 AUTHORS
-#
-#Alexander Hartmaier <Alexander.Hartmaier@t-systems.at>
-#
-#Matt S. Trout <mst@shadowcatsystems.co.uk>
-#
-#=cut
-
 1;
index bcd3800..59aefc1 100644 (file)
@@ -4,6 +4,7 @@ package # hide from PAUSE
 use strict;
 use warnings;
 use DBIx::Class::Carp;
+use DBIx::Class::_Util 'dbic_internal_try';
 use namespace::clean;
 
 our %_pod_inherit_config =
@@ -28,7 +29,7 @@ sub delete {
     my $ret = $self->next::method(@rest);
 
     foreach my $rel (@cascade) {
-      if( my $rel_rs = eval{ $self->search_related($rel) } ) {
+      if( my $rel_rs = dbic_internal_try { $self->search_related($rel) } ) {
         $rel_rs->delete_all;
       } else {
         carp "Skipping cascade delete on relationship '$rel' - related resultsource '$rels{$rel}{class}' is not registered with this schema";
index fd84b30..053eda6 100644 (file)
@@ -3,7 +3,7 @@ package # hide from PAUSE
 
 use strict;
 use warnings;
-use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 use namespace::clean;
 
 our %_pod_inherit_config =
@@ -30,7 +30,7 @@ sub has_many {
 
 # FIXME - this check needs to be moved to schema-composition time...
 #    # only perform checks if the far side appears already loaded
-#    if (my $f_rsrc = try { $f_class->result_source_instance } ) {
+#    if (my $f_rsrc = dbic_internal_try { $f_class->result_source_instance } ) {
 #      $class->throw_exception(
 #        "No such column '$f_key' on foreign class ${f_class} ($guess)"
 #      ) if !$f_rsrc->has_column($f_key);
@@ -46,6 +46,7 @@ sub has_many {
     join_type => 'LEFT',
     cascade_delete => $default_cascade,
     cascade_copy => $default_cascade,
+    is_depends_on => 0,
     %{$attrs||{}}
   });
 }
index 7935a2b..3141259 100644 (file)
@@ -5,6 +5,7 @@ use strict;
 use warnings;
 use DBIx::Class::Carp;
 use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 use namespace::clean;
 
 our %_pod_inherit_config =
@@ -34,7 +35,7 @@ sub _has_one {
       # at this point we need to load the foreigner, expensive or not
       $class->ensure_class_loaded($f_class);
 
-      $f_rsrc = try {
+      $f_rsrc = dbic_internal_try {
         my $r = $f_class->result_source_instance;
         die "There got to be some columns by now... (exception caught and rewritten by catch below)"
           unless $r->columns;
@@ -60,7 +61,7 @@ sub _has_one {
 # FIXME - this check needs to be moved to schema-composition time...
 #    # only perform checks if the far side was not preloaded above *AND*
 #    # appears to have been loaded by something else (has a rsrc_instance)
-#    if (! $f_rsrc and $f_rsrc = try { $f_class->result_source_instance }) {
+#    if (! $f_rsrc and $f_rsrc = dbic_internal_try { $f_class->result_source_instance }) {
 #      $class->throw_exception(
 #        "No such column '$f_key' on foreign class ${f_class} ($guess)"
 #      ) if !$f_rsrc->has_column($f_key);
@@ -77,6 +78,7 @@ sub _has_one {
    { accessor => 'single',
      cascade_update => $default_cascade,
      cascade_delete => $default_cascade,
+     is_depends_on => 0,
      ($join_type ? ('join_type' => $join_type) : ()),
      %{$attrs || {}} });
   1;
@@ -91,8 +93,9 @@ sub _validate_has_one_condition {
     my $self_id = $cond->{$foreign_id};
 
     # we can ignore a bad $self_id because add_relationship handles this
-    # warning
+    # exception
     return unless $self_id =~ /^self\.(.*)$/;
+
     my $key = $1;
     $class->throw_exception("Defining rel on ${class} that includes '$key' but no such column defined here yet")
         unless $class->has_column($key);
index ef63b08..c000a84 100644 (file)
@@ -5,9 +5,12 @@ use strict;
 use warnings;
 
 use DBIx::Class::Carp;
-use Sub::Name 'subname';
-use Scalar::Util 'blessed';
-use DBIx::Class::_Util 'fail_on_internal_wantarray';
+use DBIx::Class::_Util qw(fail_on_internal_wantarray quote_sub);
+
+# FIXME - this souldn't be needed
+my $cu;
+BEGIN { $cu = \&carp_unique }
+
 use namespace::clean;
 
 our %_pod_inherit_config =
@@ -26,10 +29,6 @@ sub many_to_many {
     "missing foreign relation in many-to-many"
   ) unless $f_rel;
 
-  {
-    no strict 'refs';
-    no warnings 'redefine';
-
     my $add_meth = "add_to_${meth}";
     my $remove_meth = "remove_from_${meth}";
     my $set_meth = "set_${meth}";
@@ -57,95 +56,142 @@ EOW
       }
     }
 
-    $rel_attrs->{alias} ||= $f_rel;
-
-    my $rs_meth_name = join '::', $class, $rs_meth;
-    *$rs_meth_name = subname $rs_meth_name, sub {
-      my $self = shift;
-      my $attrs = @_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {};
-      my $rs = $self->search_related($rel)->search_related(
-        $f_rel, @_ > 0 ? @_ : undef, { %{$rel_attrs||{}}, %$attrs }
-      );
-      return $rs;
+    my $qsub_attrs = {
+      '$rel_attrs' => \{ alias => $f_rel, %{ $rel_attrs||{} } },
+      '$carp_unique' => \$cu,
     };
 
-    my $meth_name = join '::', $class, $meth;
-    *$meth_name = subname $meth_name, sub {
-      DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and wantarray and my $sog = fail_on_internal_wantarray($_[0]);
-      my $self = shift;
-      my $rs = $self->$rs_meth( @_ );
-      return (wantarray ? $rs->all : $rs);
-    };
+    quote_sub "${class}::${rs_meth}", sprintf( <<'EOC', $rel, $f_rel ), $qsub_attrs;
 
-    my $add_meth_name = join '::', $class, $add_meth;
-    *$add_meth_name = subname $add_meth_name, sub {
-      my $self = shift;
-      @_ > 0 or $self->throw_exception(
-        "${add_meth} needs an object or hashref"
+      # this little horror is there replicating a deprecation from
+      # within search_rs() itself
+      shift->search_related_rs( q{%1$s} )
+            ->search_related_rs(
+              q{%2$s},
+              undef,
+              ( @_ > 1 and ref $_[-1] eq 'HASH' )
+                ? { %%$rel_attrs, %%{ pop @_ } }
+                : $rel_attrs
+            )->search_rs(@_)
+      ;
+EOC
+
+
+    quote_sub "${class}::${meth}", sprintf( <<'EOC', $rs_meth );
+
+      DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and my $sog = DBIx::Class::_Util::fail_on_internal_wantarray;
+
+      my $rs = shift->%s( @_ );
+
+      wantarray ? $rs->all : $rs;
+EOC
+
+
+    quote_sub "${class}::${add_meth}", sprintf( <<'EOC', $add_meth, $rel, $f_rel ), $qsub_attrs;
+
+      ( @_ >= 2 and @_ <= 3 ) or $_[0]->throw_exception(
+        "'%1$s' expects an object or hashref to link to, and an optional hashref of link data"
       );
-      my $source = $self->result_source;
-      my $schema = $source->schema;
-      my $rel_source_name = $source->relationship_info($rel)->{source};
-      my $rel_source = $schema->resultset($rel_source_name)->result_source;
-      my $f_rel_source_name = $rel_source->relationship_info($f_rel)->{source};
-      my $f_rel_rs = $schema->resultset($f_rel_source_name)->search({}, $rel_attrs||{});
-
-      my $obj;
-      if (ref $_[0]) {
-        if (ref $_[0] eq 'HASH') {
-          $obj = $f_rel_rs->find_or_create($_[0]);
-        } else {
-          $obj = $_[0];
-        }
-      } else {
-        $obj = $f_rel_rs->find_or_create({@_});
+
+      $_[0]->throw_exception(
+        "The optional link data supplied to '%1$s' is not a hashref (it was previously ignored)"
+      ) if $_[2] and ref $_[2] ne 'HASH';
+
+      my( $self, $far_obj ) = @_;
+
+      my $guard;
+
+      # the API needs is always expected to return the far object, possibly
+      # creating it in the process
+      if( not defined Scalar::Util::blessed( $far_obj ) ) {
+
+        $guard = $self->result_source->schema->storage->txn_scope_guard;
+
+        # reify the hash into an actual object
+        $far_obj = $self->result_source
+                         ->related_source( q{%2$s} )
+                          ->related_source( q{%3$s} )
+                           ->resultset
+                            ->search_rs( undef, $rel_attrs )
+                             ->find_or_create( $far_obj );
       }
 
-      my $link_vals = @_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {};
-      my $link = $self->search_related($rel)->new_result($link_vals);
-      $link->set_from_related($f_rel, $obj);
+      my $link = $self->new_related(
+        q{%2$s},
+        $_[2] || {},
+      );
+
+      $link->set_from_related( q{%3$s}, $far_obj );
+
       $link->insert();
-      return $obj;
-    };
 
-    my $set_meth_name = join '::', $class, $set_meth;
-    *$set_meth_name = subname $set_meth_name, sub {
+      $guard->commit if $guard;
+
+      $far_obj;
+EOC
+
+
+    quote_sub "${class}::${set_meth}", sprintf( <<'EOC', $set_meth, $add_meth, $rel, $f_rel ), $qsub_attrs;
+
       my $self = shift;
-      @_ > 0 or $self->throw_exception(
-        "{$set_meth} needs a list of objects or hashrefs"
+
+      my $set_to = ( ref $_[0] eq 'ARRAY' )
+        ? ( shift @_ )
+        : do {
+          $carp_unique->(
+            "Calling '%1$s' with a list of items to link to is deprecated, use an arrayref instead"
+          );
+
+          # gobble up everything from @_ into a new arrayref
+          [ splice @_ ]
+        }
+      ;
+
+      # make sure folks are not invoking a bizarre mix of deprecated and curent syntax
+      $self->throw_exception(
+        "'%1$s' expects an arrayref of objects or hashrefs to link to, and an optional hashref of link data"
+      ) if (
+        @_ > 1
+          or
+        ( @_ and ref $_[0] ne 'HASH' )
       );
-      my @to_set = (ref($_[0]) eq 'ARRAY' ? @{ $_[0] } : @_);
+
+      my $guard;
+
+      # there will only be a single delete() op, unless we have what to set to
+      $guard = $self->result_source->schema->storage->txn_scope_guard
+        if @$set_to;
+
       # if there is a where clause in the attributes, ensure we only delete
       # rows that are within the where restriction
-      if ($rel_attrs && $rel_attrs->{where}) {
-        $self->search_related( $rel, $rel_attrs->{where},{join => $f_rel})->delete;
-      } else {
-        $self->search_related( $rel, {} )->delete;
-      }
+      $self->search_related(
+        q{%3$s},
+        ( $rel_attrs->{where}
+          ? ( $rel_attrs->{where}, { join => q{%4$s} } )
+          : ()
+        )
+      )->delete;
+
       # add in the set rel objects
-      $self->$add_meth($_, ref($_[1]) ? $_[1] : {}) for (@to_set);
-    };
+      $self->%2$s(
+        $_,
+        @_, # at this point @_ is either empty or contains a lone link-data hash
+      ) for @$set_to;
 
-    my $remove_meth_name = join '::', $class, $remove_meth;
-    *$remove_meth_name = subname $remove_meth_name, sub {
-      my ($self, $obj) = @_;
-      $self->throw_exception("${remove_meth} needs an object")
-        unless blessed ($obj);
-      my $rel_source = $self->search_related($rel)->result_source;
-      my $cond = $rel_source->relationship_info($f_rel)->{cond};
-      my ($link_cond, $crosstable) = $rel_source->_resolve_condition(
-        $cond, $obj, $f_rel, $f_rel
-      );
+      $guard->commit if $guard;
+EOC
 
-      $self->throw_exception(
-        "Custom relationship '$rel' does not resolve to a join-free condition, "
-       ."unable to use with the ManyToMany helper '$f_rel'"
-      ) if $crosstable;
 
-      $self->search_related($rel, $link_cond)->delete;
-    };
+    quote_sub "${class}::${remove_meth}", sprintf( <<'EOC', $remove_meth, $rel, $f_rel );
+
+      $_[0]->throw_exception("'%1$s' expects an object")
+        unless defined Scalar::Util::blessed( $_[1] );
+
+      $_[0]->search_related_rs( q{%2$s} )
+            ->search_rs( $_[1]->ident_condition( q{%3$s} ), { join => q{%3$s} } )
+             ->delete;
+EOC
 
-  }
 }
 
 1;
index 6f204f6..0db5780 100644 (file)
@@ -3,8 +3,9 @@ package # hide from PAUSE
 
 use strict;
 use warnings;
-use Sub::Name ();
-use base qw/DBIx::Class/;
+use base 'DBIx::Class';
+use DBIx::Class::_Util 'quote_sub';
+use namespace::clean;
 
 our %_pod_inherit_config =
   (
@@ -22,21 +23,17 @@ sub register_relationship {
 sub proxy_to_related {
   my ($class, $rel, $proxy_args) = @_;
   my %proxy_map = $class->_build_proxy_map_from($proxy_args);
-  no strict 'refs';
-  no warnings 'redefine';
-  foreach my $meth_name ( keys %proxy_map ) {
-    my $proxy_to_col = $proxy_map{$meth_name};
-    my $name = join '::', $class, $meth_name;
-    *$name = Sub::Name::subname $name => sub {
-      my $self = shift;
-      my $relobj = $self->$rel;
-      if (@_ && !defined $relobj) {
-        $relobj = $self->create_related($rel, { $proxy_to_col => $_[0] });
-        @_ = ();
-      }
-      return ($relobj ? $relobj->$proxy_to_col(@_) : undef);
-   }
-  }
+
+  quote_sub "${class}::$_", sprintf( <<'EOC', $rel, $proxy_map{$_} )
+    my $self = shift;
+    my $relobj = $self->%1$s;
+    if (@_ && !defined $relobj) {
+      $relobj = $self->create_related( %1$s => { %2$s => $_[0] } );
+      @_ = ();
+    }
+    $relobj ? $relobj->%2$s(@_) : undef;
+EOC
+    for keys %proxy_map
 }
 
 sub _build_proxy_map_from {
index 4d002ab..65d2adb 100644 (file)
@@ -103,6 +103,9 @@ sub inflate_result {
   return $mk_hash->($_[2], $_[3], 'is_root');
 }
 
+1;
+
+__END__
 
 =head1 CAVEATS
 
@@ -131,6 +134,13 @@ The returned hash contains the raw database values.
 
 =back
 
-=cut
+=head1 FURTHER QUESTIONS?
 
-1;
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 32b3be4..0a6c002 100644 (file)
@@ -5,10 +5,13 @@ use warnings;
 use base qw/DBIx::Class/;
 use DBIx::Class::Carp;
 use DBIx::Class::ResultSetColumn;
+use DBIx::Class::ResultClass::HashRefInflator;
 use Scalar::Util qw/blessed weaken reftype/;
-use DBIx::Class::_Util 'fail_on_internal_wantarray';
+use DBIx::Class::_Util qw(
+  dbic_internal_try
+  fail_on_internal_wantarray fail_on_internal_call UNRESOLVABLE_CONDITION
+);
 use Try::Tiny;
-use Data::Compare (); # no imports!!! guard against insane architecture
 
 # not importing first() as it will clash with our own method
 use List::Util ();
@@ -57,7 +60,7 @@ just stores all the conditions needed to create the query.
 
 A basic ResultSet representing the data of an entire table is returned
 by calling C<resultset> on a L<DBIx::Class::Schema> and passing in a
-L<Source|DBIx::Class::Manual::Glossary/Source> name.
+L<Source|DBIx::Class::Manual::Glossary/ResultSource> name.
 
   my $users_rs = $schema->resultset('User');
 
@@ -78,34 +81,6 @@ However, if it is used in a boolean context it is B<always> true.  So if
 you want to check if a resultset has any results, you must use C<if $rs
 != 0>.
 
-=head1 CUSTOM ResultSet CLASSES THAT USE Moose
-
-If you want to make your custom ResultSet classes with L<Moose>, use a template
-similar to:
-
-    package MyApp::Schema::ResultSet::User;
-
-    use Moose;
-    use namespace::autoclean;
-    use MooseX::NonMoose;
-    extends 'DBIx::Class::ResultSet';
-
-    sub BUILDARGS { $_[2] }
-
-    ...your code...
-
-    __PACKAGE__->meta->make_immutable;
-
-    1;
-
-The L<MooseX::NonMoose> is necessary so that the L<Moose> constructor does not
-clash with the regular ResultSet constructor. Alternatively, you can use:
-
-    __PACKAGE__->meta->make_immutable(inline_constructor => 0);
-
-The L<BUILDARGS|Moose::Manual::Construction/BUILDARGS> is necessary because the
-signature of the ResultSet C<new> is C<< ->new($source, \%args) >>.
-
 =head1 EXAMPLES
 
 =head2 Chaining resultsets
@@ -193,6 +168,93 @@ Which is the same as:
 
 See: L</search>, L</count>, L</get_column>, L</all>, L</create>.
 
+=head2 Custom ResultSet classes
+
+To add methods to your resultsets, you can subclass L<DBIx::Class::ResultSet>, similar to:
+
+  package MyApp::Schema::ResultSet::User;
+
+  use strict;
+  use warnings;
+
+  use base 'DBIx::Class::ResultSet';
+
+  sub active {
+    my $self = shift;
+    $self->search({ $self->current_source_alias . '.active' => 1 });
+  }
+
+  sub unverified {
+    my $self = shift;
+    $self->search({ $self->current_source_alias . '.verified' => 0 });
+  }
+
+  sub created_n_days_ago {
+    my ($self, $days_ago) = @_;
+    $self->search({
+      $self->current_source_alias . '.create_date' => {
+        '<=',
+      $self->result_source->schema->storage->datetime_parser->format_datetime(
+        DateTime->now( time_zone => 'UTC' )->subtract( days => $days_ago )
+      )}
+    });
+  }
+
+  sub users_to_warn { shift->active->unverified->created_n_days_ago(7) }
+
+  1;
+
+See L<DBIx::Class::Schema/load_namespaces> on how DBIC can discover and
+automatically attach L<Result|DBIx::Class::Manual::ResultClass>-specific
+L<ResulSet|DBIx::Class::ResultSet> classes.
+
+=head3 ResultSet subclassing with Moose and similar constructor-providers
+
+Using L<Moose> or L<Moo> in your ResultSet classes is usually overkill, but
+you may find it useful if your ResultSets contain a lot of business logic
+(e.g. C<has xml_parser>, C<has json>, etc) or if you just prefer to organize
+your code via roles.
+
+In order to write custom ResultSet classes with L<Moo> you need to use the
+following template. The L<BUILDARGS|Moo/BUILDARGS> is necessary due to the
+unusual signature of the L<constructor provided by DBIC
+|DBIx::Class::ResultSet/new> C<< ->new($source, \%args) >>.
+
+  use Moo;
+  extends 'DBIx::Class::ResultSet';
+  sub BUILDARGS { $_[2] } # ::RS::new() expects my ($class, $rsrc, $args) = @_
+
+  ...your code...
+
+  1;
+
+If you want to build your custom ResultSet classes with L<Moose>, you need
+a similar, though a little more elaborate template in order to interface the
+inlining of the L<Moose>-provided
+L<object constructor|Moose::Manual::Construction/WHERE'S THE CONSTRUCTOR?>,
+with the DBIC one.
+
+  package MyApp::Schema::ResultSet::User;
+
+  use Moose;
+  use MooseX::NonMoose;
+  extends 'DBIx::Class::ResultSet';
+
+  sub BUILDARGS { $_[2] } # ::RS::new() expects my ($class, $rsrc, $args) = @_
+
+  ...your code...
+
+  __PACKAGE__->meta->make_immutable;
+
+  1;
+
+The L<MooseX::NonMoose> is necessary so that the L<Moose> constructor does not
+entirely overwrite the DBIC one (in contrast L<Moo> does this automatically).
+Alternatively, you can skip L<MooseX::NonMoose> and get by with just L<Moose>
+instead by doing:
+
+  __PACKAGE__->meta->make_immutable(inline_constructor => 0);
+
 =head1 METHODS
 
 =head2 new
@@ -240,14 +302,18 @@ creation B<will not work>. See also warning pertaining to L</create>.
 
 sub new {
   my $class = shift;
-  return $class->new_result(@_) if ref $class;
+
+  if (ref $class) {
+    DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call;
+    return $class->new_result(@_);
+  }
 
   my ($source, $attrs) = @_;
   $source = $source->resolve
     if $source->isa('DBIx::Class::ResultSourceHandle');
 
   $attrs = { %{$attrs||{}} };
-  delete @{$attrs}{qw(_last_sqlmaker_alias_map _related_results_construction)};
+  delete @{$attrs}{qw(_last_sqlmaker_alias_map _simple_passthrough_construction)};
 
   if ($attrs->{page}) {
     $attrs->{rows} ||= 10;
@@ -328,7 +394,7 @@ sub search {
   my $rs = $self->search_rs( @_ );
 
   if (wantarray) {
-    DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and my $sog = fail_on_internal_wantarray($rs);
+    DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and my $sog = fail_on_internal_wantarray;
     return $rs->all;
   }
   elsif (defined wantarray) {
@@ -377,7 +443,7 @@ sub search_rs {
     $call_cond = shift;
   }
   # fish out attrs in the ($condref, $attr) case
-  elsif (@_ == 2 and ( ! defined $_[0] or (ref $_[0]) ne '') ) {
+  elsif (@_ == 2 and ( ! defined $_[0] or length ref $_[0] ) ) {
     ($call_cond, $call_attrs) = @_;
   }
   elsif (@_ % 2) {
@@ -391,7 +457,7 @@ sub search_rs {
     for my $i (0 .. $#_) {
       next if $i % 2;
       $self->throw_exception ('All keys in condition key/value pairs must be plain scalars')
-        if (! defined $_[$i] or ref $_[$i] ne '');
+        if (! defined $_[$i] or length ref $_[$i] );
     }
 
     $call_cond = { @_ };
@@ -585,59 +651,22 @@ sub _normalize_selection {
 sub _stack_cond {
   my ($self, $left, $right) = @_;
 
-  # collapse single element top-level conditions
-  # (single pass only, unlikely to need recursion)
-  for ($left, $right) {
-    if (ref $_ eq 'ARRAY') {
-      if (@$_ == 0) {
-        $_ = undef;
-      }
-      elsif (@$_ == 1) {
-        $_ = $_->[0];
-      }
-    }
-    elsif (ref $_ eq 'HASH') {
-      my ($first, $more) = keys %$_;
-
-      # empty hash
-      if (! defined $first) {
-        $_ = undef;
-      }
-      # one element hash
-      elsif (! defined $more) {
-        if ($first eq '-and' and ref $_->{'-and'} eq 'HASH') {
-          $_ = $_->{'-and'};
-        }
-        elsif ($first eq '-or' and ref $_->{'-or'} eq 'ARRAY') {
-          $_ = $_->{'-or'};
-        }
-      }
-    }
-  }
-
-  # merge hashes with weeding out of duplicates (simple cases only)
-  if (ref $left eq 'HASH' and ref $right eq 'HASH') {
-
-    # shallow copy to destroy
-    $right = { %$right };
-    for (grep { exists $right->{$_} } keys %$left) {
-      # the use of eq_deeply here is justified - the rhs of an
-      # expression can contain a lot of twisted weird stuff
-      delete $right->{$_} if Data::Compare::Compare( $left->{$_}, $right->{$_} );
-    }
-
-    $right = undef unless keys %$right;
-  }
-
+  (
+    (ref $_ eq 'ARRAY' and !@$_)
+      or
+    (ref $_ eq 'HASH' and ! keys %$_)
+  ) and $_ = undef for ($left, $right);
 
-  if (defined $left xor defined $right) {
+  # either one of the two undef
+  if ( (defined $left) xor (defined $right) ) {
     return defined $left ? $left : $right;
   }
-  elsif (! defined $left) {
-    return undef;
+  # both undef
+  elsif ( ! defined $left ) {
+    return undef
   }
   else {
-    return { -and => [ $left, $right ] };
+    return $self->result_source->schema->storage->_collapse_cond({ -and => [$left, $right] });
   }
 }
 
@@ -784,40 +813,41 @@ sub find {
     . "corresponding to the columns of the specified unique constraint '$constraint_name'"
     ) unless @c_cols == @_;
 
-    $call_cond = {};
     @{$call_cond}{@c_cols} = @_;
   }
 
-  my %related;
+  # process relationship data if any
   for my $key (keys %$call_cond) {
     if (
-      my $keyref = ref($call_cond->{$key})
+      length ref($call_cond->{$key})
         and
       my $relinfo = $rsrc->relationship_info($key)
+        and
+      # implicitly skip has_many's (likely MC)
+      (ref (my $val = delete $call_cond->{$key}) ne 'ARRAY' )
     ) {
-      my $val = delete $call_cond->{$key};
-
-      next if $keyref eq 'ARRAY'; # has_many for multi_create
-
-      my $rel_q = $rsrc->_resolve_condition(
+      my ($rel_cond, $crosstable) = $rsrc->_resolve_condition(
         $relinfo->{cond}, $val, $key, $key
       );
-      die "Can't handle complex relationship conditions in find" if ref($rel_q) ne 'HASH';
-      @related{keys %$rel_q} = values %$rel_q;
+
+      $self->throw_exception("Complex condition via relationship '$key' is unsupported in find()")
+         if $crosstable or ref($rel_cond) ne 'HASH';
+
+      # supplement condition
+      # relationship conditions take precedence (?)
+      @{$call_cond}{keys %$rel_cond} = values %$rel_cond;
     }
   }
 
-  # relationship conditions take precedence (?)
-  @{$call_cond}{keys %related} = values %related;
-
   my $alias = exists $attrs->{alias} ? $attrs->{alias} : $self->{attrs}{alias};
   my $final_cond;
   if (defined $constraint_name) {
     $final_cond = $self->_qualify_cond_columns (
 
-      $self->_build_unique_cond (
-        $constraint_name,
-        $call_cond,
+      $self->result_source->_minimal_valueset_satisfying_constraint(
+        constraint_name => $constraint_name,
+        values => ($self->_merge_with_rscond($call_cond))[0],
+        carp_on_nulls => 1,
       ),
 
       $alias,
@@ -832,23 +862,42 @@ sub find {
     # relationship
   }
   else {
+    my (@unique_queries, %seen_column_combinations, $ci, @fc_exceptions);
+
     # no key was specified - fall down to heuristics mode:
     # run through all unique queries registered on the resultset, and
     # 'OR' all qualifying queries together
-    my (@unique_queries, %seen_column_combinations);
-    for my $c_name ($rsrc->unique_constraint_names) {
+    #
+    # always start from 'primary' if it exists at all
+    for my $c_name ( sort {
+        $a eq 'primary' ? -1
+      : $b eq 'primary' ? 1
+      : $a cmp $b
+    } $rsrc->unique_constraint_names) {
+
       next if $seen_column_combinations{
         join "\x00", sort $rsrc->unique_constraint_columns($c_name)
       }++;
 
-      push @unique_queries, try {
-        $self->_build_unique_cond ($c_name, $call_cond, 'croak_on_nulls')
-      } || ();
+      dbic_internal_try {
+        push @unique_queries, $self->_qualify_cond_columns(
+          $self->result_source->_minimal_valueset_satisfying_constraint(
+            constraint_name => $c_name,
+            values => ($self->_merge_with_rscond($call_cond))[0],
+            columns_info => ($ci ||= $self->result_source->columns_info),
+          ),
+          $alias
+        );
+      }
+      catch {
+        push @fc_exceptions, $_ if $_ =~ /\bFilterColumn\b/;
+      };
     }
 
-    $final_cond = @unique_queries
-      ? [ map { $self->_qualify_cond_columns($_, $alias) } @unique_queries ]
-      : $self->_non_unique_find_fallback ($call_cond, $attrs)
+    $final_cond =
+        @unique_queries   ? \@unique_queries
+      : @fc_exceptions    ? $self->throw_exception(join "; ", map { $_ =~ /(.*) at .+ line \d+$/s } @fc_exceptions )
+      :                     $self->_non_unique_find_fallback ($call_cond, $attrs)
     ;
   }
 
@@ -901,51 +950,20 @@ sub _qualify_cond_columns {
 }
 
 sub _build_unique_cond {
-  my ($self, $constraint_name, $extra_cond, $croak_on_null) = @_;
-
-  my @c_cols = $self->result_source->unique_constraint_columns($constraint_name);
-
-  # combination may fail if $self->{cond} is non-trivial
-  my ($final_cond) = try {
-    $self->_merge_with_rscond ($extra_cond)
-  } catch {
-    +{ %$extra_cond }
-  };
-
-  # trim out everything not in $columns
-  $final_cond = { map {
-    exists $final_cond->{$_}
-      ? ( $_ => $final_cond->{$_} )
-      : ()
-  } @c_cols };
-
-  if (my @missing = grep
-    { ! ($croak_on_null ? defined $final_cond->{$_} : exists $final_cond->{$_}) }
-    (@c_cols)
-  ) {
-    $self->throw_exception( sprintf ( "Unable to satisfy requested constraint '%s', no values for column(s): %s",
-      $constraint_name,
-      join (', ', map { "'$_'" } @missing),
-    ) );
-  }
-
-  if (
-    !$croak_on_null
-      and
-    !$ENV{DBIC_NULLABLE_KEY_NOWARN}
-      and
-    my @undefs = sort grep { ! defined $final_cond->{$_} } (keys %$final_cond)
-  ) {
-    carp_unique ( sprintf (
-      "NULL/undef values supplied for requested unique constraint '%s' (NULL "
-    . 'values in column(s): %s). This is almost certainly not what you wanted, '
-    . 'though you can set DBIC_NULLABLE_KEY_NOWARN to disable this warning.',
-      $constraint_name,
-      join (', ', map { "'$_'" } @undefs),
-    ));
-  }
-
-  return $final_cond;
+  carp_unique sprintf
+    '_build_unique_cond is a private method, and moreover is about to go '
+  . 'away. Please contact the development team at %s if you believe you '
+  . 'have a genuine use for this method, in order to discuss alternatives.',
+    DBIx::Class::_ENV_::HELP_URL,
+  ;
+
+  my ($self, $constraint_name, $cond, $croak_on_null) = @_;
+
+  $self->result_source->_minimal_valueset_satisfying_constraint(
+    constraint_name => $constraint_name,
+    values => $cond,
+    carp_on_nulls => !$croak_on_null
+  );
 }
 
 =head2 search_related
@@ -1090,39 +1108,6 @@ sub single {
   $self->_construct_results->[0];
 }
 
-
-# _collapse_query
-#
-# Recursively collapse the query, accumulating values for each column.
-
-sub _collapse_query {
-  my ($self, $query, $collapsed) = @_;
-
-  $collapsed ||= {};
-
-  if (ref $query eq 'ARRAY') {
-    foreach my $subquery (@$query) {
-      next unless ref $subquery;  # -or
-      $collapsed = $self->_collapse_query($subquery, $collapsed);
-    }
-  }
-  elsif (ref $query eq 'HASH') {
-    if (keys %$query and (keys %$query)[0] eq '-and') {
-      foreach my $subquery (@{$query->{-and}}) {
-        $collapsed = $self->_collapse_query($subquery, $collapsed);
-      }
-    }
-    else {
-      foreach my $col (keys %$query) {
-        my $value = $query->{$col};
-        $collapsed->{$col}{$value}++;
-      }
-    }
-  }
-
-  return $collapsed;
-}
-
 =head2 get_column
 
 =over 4
@@ -1164,7 +1149,7 @@ 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()>
+This method is deprecated and will be removed in 0.09. Use L<search()|/search>
 instead. An example conversion is:
 
   ->search_like({ foo => 'bar' });
@@ -1323,7 +1308,7 @@ sub _construct_results {
           and
         $rsrc->schema
               ->storage
-               ->_main_source_order_by_portion_is_stable($rsrc, $attrs->{order_by}, $attrs->{where})
+               ->_extract_colinfo_of_stable_main_source_order_by_portion($attrs)
       ) ? 1 : 0
     ) unless defined $attrs->{_ordered_for_collapse};
 
@@ -1399,16 +1384,12 @@ sub _construct_results {
   $self->{_result_inflator}{is_hri} = ( (
     ! $self->{_result_inflator}{is_core_row}
       and
-    $inflator_cref == (
-      require DBIx::Class::ResultClass::HashRefInflator
-        &&
-      DBIx::Class::ResultClass::HashRefInflator->can('inflate_result')
-    )
+    $inflator_cref == \&DBIx::Class::ResultClass::HashRefInflator::inflate_result
   ) ? 1 : 0 ) unless defined $self->{_result_inflator}{is_hri};
 
 
-  if (! $attrs->{_related_results_construction}) {
-    # construct a much simpler array->hash folder for the one-table cases right here
+  if ($attrs->{_simple_passthrough_construction}) {
+    # construct a much simpler array->hash folder for the one-table HRI cases right here
     if ($self->{_result_inflator}{is_hri}) {
       for my $r (@$rows) {
         $r = { map { $infmap->[$_] => $r->[$_] } 0..$#$infmap };
@@ -1421,15 +1402,19 @@ sub _construct_results {
     #
     # crude unscientific benchmarking indicated the shortcut eval is not worth it for
     # this particular resultset size
-    elsif (@$rows < 60) {
+    elsif ( $self->{_result_inflator}{is_core_row} and @$rows < 60 ) {
       for my $r (@$rows) {
         $r = $inflator_cref->($res_class, $rsrc, { map { $infmap->[$_] => $r->[$_] } (0..$#$infmap) } );
       }
     }
     else {
       eval sprintf (
-        '$_ = $inflator_cref->($res_class, $rsrc, { %s }) for @$rows',
-        join (', ', map { "\$infmap->[$_] => \$_->[$_]" } 0..$#$infmap )
+        ( $self->{_result_inflator}{is_core_row}
+          ? '$_ = $inflator_cref->($res_class, $rsrc, { %s }) for @$rows'
+          # a custom inflator may be a multiplier/reductor - put it in direct list ctx
+          : '@$rows = map { $inflator_cref->($res_class, $rsrc, { %s } ) } @$rows'
+        ),
+        ( join (', ', map { "\$infmap->[$_] => \$_->[$_]" } 0..$#$infmap ) )
       );
     }
   }
@@ -1476,6 +1461,9 @@ sub _construct_results {
         if @violating_idx;
 
       $unrolled_non_null_cols_to_check = join (',', @$check_non_null_cols);
+
+      utf8::upgrade($unrolled_non_null_cols_to_check)
+        if DBIx::Class::_ENV_::STRESSTEST_UTF8_UPGRADE_GENERATED_COLLAPSER_SOURCE;
     }
 
     my $next_cref =
@@ -1502,10 +1490,15 @@ EOS
       $next_cref ? ( $next_cref, $self->{_stashed_rows} = [] ) : (),
     );
 
-    # Special-case multi-object HRI - there is no $inflator_cref pass
-    unless ($self->{_result_inflator}{is_hri}) {
+    # simple in-place substitution, does not regrow $rows
+    if ($self->{_result_inflator}{is_core_row}) {
       $_ = $inflator_cref->($res_class, $rsrc, @$_) for @$rows
     }
+    # Special-case multi-object HRI - there is no $inflator_cref pass at all
+    elsif ( ! $self->{_result_inflator}{is_hri} ) {
+      # the inflator may be a multiplier/reductor - put it in list ctx
+      @$rows = map { $inflator_cref->($res_class, $rsrc, @$_) } @$rows;
+    }
   }
 
   # The @$rows check seems odd at first - why wouldn't we want to warn
@@ -1550,8 +1543,8 @@ 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.
+L<load_components|Class::C3::Componentised/load_components( @comps )>.
+Any overloaded methods in the original source class will not run.
 
 =cut
 
@@ -2003,7 +1996,6 @@ sub _rs_update_delete {
 
       $guard = $storage->txn_scope_guard;
 
-      $cond = [];
       for my $row ($subrs->cursor->all) {
         push @$cond, { map
           { $idcols->[$_] => $row->[$_] }
@@ -2013,11 +2005,11 @@ sub _rs_update_delete {
     }
   }
 
-  my $res = $storage->$op (
+  my $res = $cond ? $storage->$op (
     $rsrc,
     $op eq 'update' ? $values : (),
     $cond,
-  );
+  ) : '0E0';
 
   $guard->commit if $guard;
 
@@ -2227,127 +2219,275 @@ case there are obviously no benefits to using this method over L</create>.
 sub populate {
   my $self = shift;
 
-  # cruft placed in standalone method
-  my $data = $self->_normalize_populate_args(@_);
+  # this is naive and just a quick check
+  # the types will need to be checked more thoroughly when the
+  # multi-source populate gets added
+  my $data = (
+    ref $_[0] eq 'ARRAY'
+      and
+    ( @{$_[0]} or return )
+      and
+    ( ref $_[0][0] eq 'HASH' or ref $_[0][0] eq 'ARRAY' )
+      and
+    $_[0]
+  ) or $self->throw_exception('Populate expects an arrayref of hashrefs or arrayref of arrayrefs');
 
-  return unless @$data;
+  # FIXME - no cref handling
+  # At this point assume either hashes or arrays
 
   if(defined wantarray) {
-    my @created = map { $self->create($_) } @$data;
-    return wantarray ? @created : \@created;
-  }
-  else {
-    my $first = $data->[0];
+    my (@results, $guard);
 
-    # if a column is a registered relationship, and is a non-blessed hash/array, consider
-    # it relationship data
-    my (@rels, @columns);
-    my $rsrc = $self->result_source;
-    my $rels = { map { $_ => $rsrc->relationship_info($_) } $rsrc->relationships };
-    for (keys %$first) {
-      my $ref = ref $first->{$_};
-      $rels->{$_} && ($ref eq 'ARRAY' or $ref eq 'HASH')
-        ? push @rels, $_
-        : push @columns, $_
+    if (ref $data->[0] eq 'ARRAY') {
+      # column names only, nothing to do
+      return if @$data == 1;
+
+      $guard = $self->result_source->schema->storage->txn_scope_guard
+        if @$data > 2;
+
+      @results = map
+        { my $vals = $_; $self->new_result({ map { $data->[0][$_] => $vals->[$_] } 0..$#{$data->[0]} })->insert }
+        @{$data}[1 .. $#$data]
       ;
     }
+    else {
+
+      $guard = $self->result_source->schema->storage->txn_scope_guard
+        if @$data > 1;
+
+      @results = map { $self->new_result($_)->insert } @$data;
+    }
+
+    $guard->commit if $guard;
+    return wantarray ? @results : \@results;
+  }
+
+  # we have to deal with *possibly incomplete* related data
+  # this means we have to walk the data structure twice
+  # whether we want this or not
+  # jnap, I hate you ;)
+  my $rsrc = $self->result_source;
+  my $rel_info = { map { $_ => $rsrc->relationship_info($_) } $rsrc->relationships };
+
+  my ($colinfo, $colnames, $slices_with_rels);
+  my $data_start = 0;
+
+  DATA_SLICE:
+  for my $i (0 .. $#$data) {
 
-    my @pks = $rsrc->primary_columns;
+    my $current_slice_seen_rel_infos;
 
-    ## do the belongs_to relationships
-    foreach my $index (0..$#$data) {
+### Determine/Supplement collists
+### BEWARE - This is a hot piece of code, a lot of weird idioms were used
+    if( ref $data->[$i] eq 'ARRAY' ) {
 
-      # delegate to create() for any dataset without primary keys with specified relationships
-      if (grep { !defined $data->[$index]->{$_} } @pks ) {
-        for my $r (@rels) {
-          if (grep { ref $data->[$index]{$r} eq $_ } qw/HASH ARRAY/) {  # a related set must be a HASH or AoH
-            my @ret = $self->populate($data);
-            return;
+      # positional(!) explicit column list
+      if ($i == 0) {
+        # column names only, nothing to do
+        return if @$data == 1;
+
+        $colinfo->{$data->[0][$_]} = { pos => $_, name => $data->[0][$_] } and push @$colnames, $data->[0][$_]
+          for 0 .. $#{$data->[0]};
+
+        $data_start = 1;
+
+        next DATA_SLICE;
+      }
+      else {
+        for (values %$colinfo) {
+          if ($_->{is_rel} ||= (
+            $rel_info->{$_->{name}}
+              and
+            (
+              ref $data->[$i][$_->{pos}] eq 'ARRAY'
+                or
+              ref $data->[$i][$_->{pos}] eq 'HASH'
+                or
+              ( defined blessed $data->[$i][$_->{pos}] and $data->[$i][$_->{pos}]->isa('DBIx::Class::Row') )
+            )
+              and
+            1
+          )) {
+
+            # moar sanity check... sigh
+            for ( ref $data->[$i][$_->{pos}] eq 'ARRAY' ? @{$data->[$i][$_->{pos}]} : $data->[$i][$_->{pos}] ) {
+              if ( defined blessed $_ and $_->isa('DBIx::Class::Row' ) ) {
+                carp_unique("Fast-path populate() with supplied related objects is not possible - falling back to regular create()");
+                return my $throwaway = $self->populate(@_);
+              }
+            }
+
+            push @$current_slice_seen_rel_infos, $rel_info->{$_->{name}};
           }
         }
       }
 
-      foreach my $rel (@rels) {
-        next unless ref $data->[$index]->{$rel} eq "HASH";
-        my $result = $self->related_resultset($rel)->create($data->[$index]->{$rel});
-        my ($reverse_relname, $reverse_relinfo) = %{$rsrc->reverse_relationship_info($rel)};
-        my $related = $result->result_source->_resolve_condition(
-          $reverse_relinfo->{cond},
-          $self,
-          $result,
-          $rel,
-        );
+     if ($current_slice_seen_rel_infos) {
+        push @$slices_with_rels, { map { $colnames->[$_] => $data->[$i][$_] } 0 .. $#$colnames };
 
-        delete $data->[$index]->{$rel};
-        $data->[$index] = {%{$data->[$index]}, %$related};
-
-        push @columns, keys %$related if $index == 0;
+        # this is needed further down to decide whether or not to fallback to create()
+        $colinfo->{$colnames->[$_]}{seen_null} ||= ! defined $data->[$i][$_]
+          for 0 .. $#$colnames;
       }
     }
+    elsif( ref $data->[$i] eq 'HASH' ) {
 
-    ## inherit the data locked in the conditions of the resultset
-    my ($rs_data) = $self->_merge_with_rscond({});
-    delete @{$rs_data}{@columns};
+      for ( sort keys %{$data->[$i]} ) {
 
-    ## do bulk insert on current row
-    $rsrc->storage->insert_bulk(
-      $rsrc,
-      [@columns, keys %$rs_data],
-      [ map { [ @$_{@columns}, values %$rs_data ] } @$data ],
-    );
+        $colinfo->{$_} ||= do {
 
-    ## do the has_many relationships
-    foreach my $item (@$data) {
+          $self->throw_exception("Column '$_' must be present in supplied explicit column list")
+            if $data_start; # it will be 0 on AoH, 1 on AoA
 
-      my $main_row;
+          push @$colnames, $_;
 
-      foreach my $rel (@rels) {
-        next unless ref $item->{$rel} eq "ARRAY" && @{ $item->{$rel} };
-
-        $main_row ||= $self->new_result({map { $_ => $item->{$_} } @pks});
+          # RV
+          { pos => $#$colnames, name => $_ }
+        };
 
-        my $child = $main_row->$rel;
+        if ($colinfo->{$_}{is_rel} ||= (
+          $rel_info->{$_}
+            and
+          (
+            ref $data->[$i]{$_} eq 'ARRAY'
+              or
+            ref $data->[$i]{$_} eq 'HASH'
+              or
+            ( defined blessed $data->[$i]{$_} and $data->[$i]{$_}->isa('DBIx::Class::Row') )
+          )
+            and
+          1
+        )) {
+
+          # moar sanity check... sigh
+          for ( ref $data->[$i]{$_} eq 'ARRAY' ? @{$data->[$i]{$_}} : $data->[$i]{$_} ) {
+            if ( defined blessed $_ and $_->isa('DBIx::Class::Row' ) ) {
+              carp_unique("Fast-path populate() with supplied related objects is not possible - falling back to regular create()");
+              return my $throwaway = $self->populate(@_);
+            }
+          }
 
-        my $related = $child->result_source->_resolve_condition(
-          $rels->{$rel}{cond},
-          $child,
-          $main_row,
-          $rel,
-        );
+          push @$current_slice_seen_rel_infos, $rel_info->{$_};
+        }
+      }
 
-        my @rows_to_add = ref $item->{$rel} eq 'ARRAY' ? @{$item->{$rel}} : ($item->{$rel});
-        my @populate = map { {%$_, %$related} } @rows_to_add;
+      if ($current_slice_seen_rel_infos) {
+        push @$slices_with_rels, $data->[$i];
 
-        $child->populate( \@populate );
+        # this is needed further down to decide whether or not to fallback to create()
+        $colinfo->{$_}{seen_null} ||= ! defined $data->[$i]{$_}
+          for keys %{$data->[$i]};
       }
     }
+    else {
+      $self->throw_exception('Unexpected populate() data structure member type: ' . ref $data->[$i] );
+    }
+
+    if ( grep
+      { $_->{attrs}{is_depends_on} }
+      @{ $current_slice_seen_rel_infos || [] }
+    ) {
+      carp_unique("Fast-path populate() of belongs_to relationship data is not possible - falling back to regular create()");
+      return my $throwaway = $self->populate(@_);
+    }
   }
-}
 
+  if( $slices_with_rels ) {
 
-# populate() arguments went over several incarnations
-# What we ultimately support is AoH
-sub _normalize_populate_args {
-  my ($self, $arg) = @_;
+    # need to exclude the rel "columns"
+    $colnames = [ grep { ! $colinfo->{$_}{is_rel} } @$colnames ];
 
-  if (ref $arg eq 'ARRAY') {
-    if (!@$arg) {
-      return [];
-    }
-    elsif (ref $arg->[0] eq 'HASH') {
-      return $arg;
+    # extra sanity check - ensure the main source is in fact identifiable
+    # the localizing of nullability is insane, but oh well... the use-case is legit
+    my $ci = $rsrc->columns_info($colnames);
+
+    $ci->{$_} = { %{$ci->{$_}}, is_nullable => 0 }
+      for grep { ! $colinfo->{$_}{seen_null} } keys %$ci;
+
+    unless( $rsrc->_identifying_column_set($ci) ) {
+      carp_unique("Fast-path populate() of non-uniquely identifiable rows with related data is not possible - falling back to regular create()");
+      return my $throwaway = $self->populate(@_);
     }
-    elsif (ref $arg->[0] eq 'ARRAY') {
-      my @ret;
-      my @colnames = @{$arg->[0]};
-      foreach my $values (@{$arg}[1 .. $#$arg]) {
-        push @ret, { map { $colnames[$_] => $values->[$_] } (0 .. $#colnames) };
+  }
+
+### inherit the data locked in the conditions of the resultset
+  my ($rs_data) = $self->_merge_with_rscond({});
+  delete @{$rs_data}{@$colnames};  # passed-in stuff takes precedence
+
+  # if anything left - decompose rs_data
+  my $rs_data_vals;
+  if (keys %$rs_data) {
+     push @$rs_data_vals, $rs_data->{$_}
+      for sort keys %$rs_data;
+  }
+
+### start work
+  my $guard;
+  $guard = $rsrc->schema->storage->txn_scope_guard
+    if $slices_with_rels;
+
+### main source data
+  # FIXME - need to switch entirely to a coderef-based thing,
+  # so that large sets aren't copied several times... I think
+  $rsrc->storage->_insert_bulk(
+    $rsrc,
+    [ @$colnames, sort keys %$rs_data ],
+    [ map {
+      ref $data->[$_] eq 'ARRAY'
+      ? (
+          $slices_with_rels ? [ @{$data->[$_]}[0..$#$colnames], @{$rs_data_vals||[]} ]  # the collist changed
+        : $rs_data_vals     ? [ @{$data->[$_]}, @$rs_data_vals ]
+        :                     $data->[$_]
+      )
+      : [ @{$data->[$_]}{@$colnames}, @{$rs_data_vals||[]} ]
+    } $data_start .. $#$data ],
+  );
+
+### do the children relationships
+  if ( $slices_with_rels ) {
+    my @rels = grep { $colinfo->{$_}{is_rel} } keys %$colinfo
+      or die 'wtf... please report a bug with DBIC_TRACE=1 output (stacktrace)';
+
+    for my $sl (@$slices_with_rels) {
+
+      my ($main_proto, $main_proto_rs);
+      for my $rel (@rels) {
+        next unless defined $sl->{$rel};
+
+        $main_proto ||= {
+          %$rs_data,
+          (map { $_ => $sl->{$_} } @$colnames),
+        };
+
+        unless (defined $colinfo->{$rel}{rs}) {
+
+          $colinfo->{$rel}{rs} = $rsrc->related_source($rel)->resultset;
+
+          $colinfo->{$rel}{fk_map} = { reverse %{ $rsrc->_resolve_relationship_condition(
+            rel_name => $rel,
+            self_alias => "\xFE", # irrelevant
+            foreign_alias => "\xFF", # irrelevant
+          )->{identity_map} || {} } };
+
+        }
+
+        $colinfo->{$rel}{rs}->search({ map # only so that we inherit them values properly, no actual search
+          {
+            $_ => { '=' =>
+              ( $main_proto_rs ||= $rsrc->resultset->search($main_proto) )
+                ->get_column( $colinfo->{$rel}{fk_map}{$_} )
+                 ->as_query
+            }
+          }
+          keys %{$colinfo->{$rel}{fk_map}}
+        })->populate( ref $sl->{$rel} eq 'ARRAY' ? $sl->{$rel} : [ $sl->{$rel} ] );
+
+        1;
       }
-      return \@ret;
     }
   }
 
-  $self->throw_exception('Populate expects an arrayref of hashrefs or arrayref of arrayrefs');
+  $guard->commit if $guard;
 }
 
 =head2 pager
@@ -2443,7 +2583,7 @@ sub new_result {
   $self->throw_exception( "new_result takes only one argument - a hashref of values" )
     if @_ > 2;
 
-  $self->throw_exception( "new_result expects a hashref" )
+  $self->throw_exception( "Result object instantiation requires a hashref as argument" )
     unless (ref $values eq 'HASH');
 
   my ($merged_cond, $cols_from_relations) = $self->_merge_with_rscond($values);
@@ -2482,51 +2622,33 @@ sub new_result {
 sub _merge_with_rscond {
   my ($self, $data) = @_;
 
-  my (%new_data, @cols_from_relations);
+  my ($implied_data, @cols_from_relations);
 
   my $alias = $self->{attrs}{alias};
 
   if (! defined $self->{cond}) {
     # just massage $data below
   }
-  elsif ($self->{cond} eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION) {
-    %new_data = %{ $self->{attrs}{related_objects} || {} };  # nothing might have been inserted yet
-    @cols_from_relations = keys %new_data;
-  }
-  elsif (ref $self->{cond} ne 'HASH') {
-    $self->throw_exception(
-      "Can't abstract implicit construct, resultset condition not a hash"
-    );
+  elsif ($self->{cond} eq UNRESOLVABLE_CONDITION) {
+    $implied_data = $self->{attrs}{related_objects};  # nothing might have been inserted yet
+    @cols_from_relations = keys %{ $implied_data || {} };
   }
   else {
-    # precedence must be given to passed values over values inherited from
-    # the cond, so the order here is important.
-    my $collapsed_cond = $self->_collapse_cond($self->{cond});
-    my %implied = %{$self->_remove_alias($collapsed_cond, $alias)};
-
-    while ( my($col, $value) = each %implied ) {
-      my $vref = ref $value;
-      if (
-        $vref eq 'HASH'
-          and
-        keys(%$value) == 1
-          and
-        (keys %$value)[0] eq '='
-      ) {
-        $new_data{$col} = $value->{'='};
-      }
-      elsif( !$vref or $vref eq 'SCALAR' or blessed($value) ) {
-        $new_data{$col} = $value;
-      }
-    }
+    my $eqs = $self->result_source->schema->storage->_extract_fixed_condition_columns($self->{cond}, 'consider_nulls');
+    $implied_data = { map {
+      ( ($eqs->{$_}||'') eq UNRESOLVABLE_CONDITION ) ? () : ( $_ => $eqs->{$_} )
+    } keys %$eqs };
   }
 
-  %new_data = (
-    %new_data,
-    %{ $self->_remove_alias($data, $alias) },
+  return (
+    { map
+      { %{ $self->_remove_alias($_, $alias) } }
+      # precedence must be given to passed values over values inherited from
+      # the cond, so the order here is important.
+      ( $implied_data||(), $data)
+    },
+    \@cols_from_relations
   );
-
-  return (\%new_data, \@cols_from_relations);
 }
 
 # _has_resolved_attr
@@ -2582,38 +2704,6 @@ sub _has_resolved_attr {
   return 0;
 }
 
-# _collapse_cond
-#
-# Recursively collapse the condition.
-
-sub _collapse_cond {
-  my ($self, $cond, $collapsed) = @_;
-
-  $collapsed ||= {};
-
-  if (ref $cond eq 'ARRAY') {
-    foreach my $subcond (@$cond) {
-      next unless ref $subcond;  # -or
-      $collapsed = $self->_collapse_cond($subcond, $collapsed);
-    }
-  }
-  elsif (ref $cond eq 'HASH') {
-    if (keys %$cond and (keys %$cond)[0] eq '-and') {
-      foreach my $subcond (@{$cond->{-and}}) {
-        $collapsed = $self->_collapse_cond($subcond, $collapsed);
-      }
-    }
-    else {
-      foreach my $col (keys %$cond) {
-        my $value = $cond->{$col};
-        $collapsed->{$col} = $value;
-      }
-    }
-  }
-
-  return $collapsed;
-}
-
 # _remove_alias
 #
 # Remove the specified alias from the specified query hash. A copy is made so
@@ -2794,10 +2884,9 @@ L</new>.
 =cut
 
 sub create {
-  my ($self, $col_data) = @_;
-  $self->throw_exception( "create needs a hashref" )
-    unless ref $col_data eq 'HASH';
-  return $self->new_result($col_data)->insert;
+  #my ($self, $col_data) = @_;
+  DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and fail_on_internal_call;
+  return shift->new_result(shift)->insert;
 }
 
 =head2 find_or_create
@@ -2879,7 +2968,7 @@ sub find_or_create {
   if (keys %$hash and my $row = $self->find($hash, $attrs) ) {
     return $row;
   }
-  return $self->create($hash);
+  return $self->new_result($hash)->insert;
 }
 
 =head2 update_or_create
@@ -2949,7 +3038,7 @@ sub update_or_create {
     return $row;
   }
 
-  return $self->create($cond);
+  return $self->new_result($cond)->insert;
 }
 
 =head2 update_or_new
@@ -3132,10 +3221,16 @@ Returns a related resultset for the supplied relationship name.
 =cut
 
 sub related_resultset {
-  my ($self, $rel) = @_;
+  $_[0]->throw_exception(
+    'Extra arguments to $rs->related_resultset() were always quietly '
+  . 'discarded without consideration, you need to switch to '
+  . '...->related_resultset( $relname )->search_rs( $search, $args ) instead.'
+  ) if @_ > 2;
 
-  return $self->{related_resultsets}{$rel}
-    if defined $self->{related_resultsets}{$rel};
+  return $_[0]->{related_resultsets}{$_[1]}
+    if defined $_[0]->{related_resultsets}{$_[1]};
+
+  my ($self, $rel) = @_;
 
   return $self->{related_resultsets}{$rel} = do {
     my $rsrc = $self->result_source;
@@ -3148,22 +3243,25 @@ sub related_resultset {
 
     my $attrs = $self->_chain_relationship($rel);
 
-    my $join_count = $attrs->{seen_join}{$rel};
+    my $storage = $rsrc->schema->storage;
 
-    my $alias = $self->result_source->storage
-        ->relname_to_table_alias($rel, $join_count);
+    # Previously this atribute was deleted (instead of being set as it is now)
+    # Doing so seems to be harmless in all available test permutations
+    # See also 01d59a6a6 and mst's comment below
+    #
+    $attrs->{alias} = $storage->relname_to_table_alias(
+      $rel,
+      $attrs->{seen_join}{$rel}
+    );
 
     # since this is search_related, and we already slid the select window inwards
     # (the select/as attrs were deleted in the beginning), we need to flip all
     # left joins to inner, so we get the expected results
     # read the comment on top of the actual function to see what this does
-    $attrs->{from} = $rsrc->schema->storage->_inner_join_to_node ($attrs->{from}, $alias);
-
+    $attrs->{from} = $storage->_inner_join_to_node( $attrs->{from}, $attrs->{alias} );
 
     #XXX - temp fix for result_class bug. There likely is a more elegant fix -groditi
-    delete @{$attrs}{qw(result_class alias)};
-
-    my $rel_source = $rsrc->related_source($rel);
+    delete $attrs->{result_class};
 
     my $new = do {
 
@@ -3172,16 +3270,19 @@ sub related_resultset {
       # source you need to know what alias it's -going- to have for things
       # to work sanely (e.g. RestrictWithObject wants to be able to add
       # extra query restrictions, and these may need to be $alias.)
-
-      my $rel_attrs = $rel_source->resultset_attributes;
-      local $rel_attrs->{alias} = $alias;
-
-      $rel_source->resultset
-                 ->search_rs(
-                     undef, {
-                       %$attrs,
-                       where => $attrs->{where},
-                   });
+      #                                       -- mst ~ 2007 (01d59a6a6)
+      #
+      # FIXME - this seems to be no longer neccessary (perhaps due to the
+      # advances in relcond resolution. Testing DBIC::S::RWO and its only
+      # dependent (as of Jun 2015 ) does not yield any difference with or
+      # without this line. Nevertheless keep it as is for now, to minimize
+      # churn, there is enough potential for breakage in 0.0829xx as it is
+      #                                       -- ribasushi Jun 2015
+      #
+      my $rel_source = $rsrc->related_source($rel);
+      local $rel_source->resultset_attributes->{alias} = $attrs->{alias};
+
+      $rel_source->resultset->search_rs( undef, $attrs );
     };
 
     if (my $cache = $self->get_cache) {
@@ -3232,7 +3333,8 @@ source alias of the current result set:
     });
   }
 
-The current table alias can be altered with L</alias>.
+The alias of L<newly created resultsets|/search> can be altered by the
+L<alias attribute|/alias>.
 
 =cut
 
@@ -3424,12 +3526,25 @@ sub _resolved_attrs {
   return $self->{_attrs} if $self->{_attrs};
 
   my $attrs  = { %{ $self->{attrs} || {} } };
-  my $source = $self->result_source;
+  my $source = $attrs->{result_source} = $self->result_source;
   my $alias  = $attrs->{alias};
 
   $self->throw_exception("Specifying distinct => 1 in conjunction with collapse => 1 is unsupported")
     if $attrs->{collapse} and $attrs->{distinct};
 
+
+  # Sanity check the paging attributes
+  # SQLMaker does it too, but in case of a software_limit we'll never get there
+  if (defined $attrs->{offset}) {
+    $self->throw_exception('A supplied offset attribute must be a non-negative integer')
+      if ( $attrs->{offset} =~ /[^0-9]/ or $attrs->{offset} < 0 );
+  }
+  if (defined $attrs->{rows}) {
+    $self->throw_exception("The rows attribute must be a positive integer if present")
+      if ( $attrs->{rows} =~ /[^0-9]/ or $attrs->{rows} <= 0 );
+  }
+
+
   # default selection list
   $attrs->{columns} = [ $source->columns ]
     unless List::Util::first { exists $attrs->{$_} } qw/columns cols select as/;
@@ -3528,62 +3643,35 @@ sub _resolved_attrs {
       ];
   }
 
-  if ( defined $attrs->{order_by} ) {
-    $attrs->{order_by} = (
-      ref( $attrs->{order_by} ) eq 'ARRAY'
-      ? [ @{ $attrs->{order_by} } ]
-      : [ $attrs->{order_by} || () ]
-    );
-  }
-
-  if ($attrs->{group_by} and ref $attrs->{group_by} ne 'ARRAY') {
-    $attrs->{group_by} = [ $attrs->{group_by} ];
-  }
 
+  for my $attr (qw(order_by group_by)) {
 
-  # generate selections based on the prefetch helper
-  my ($prefetch, @prefetch_select, @prefetch_as);
-  $prefetch = $self->_merge_joinpref_attr( {}, delete $attrs->{prefetch} )
-    if defined $attrs->{prefetch};
+    if ( defined $attrs->{$attr} ) {
+      $attrs->{$attr} = (
+        ref( $attrs->{$attr} ) eq 'ARRAY'
+        ? [ @{ $attrs->{$attr} } ]
+        : [ $attrs->{$attr} || () ]
+      );
 
-  if ($prefetch) {
+      delete $attrs->{$attr} unless @{$attrs->{$attr}};
+    }
+  }
 
-    $self->throw_exception("Unable to prefetch, resultset contains an unnamed selector $attrs->{_dark_selector}{string}")
-      if $attrs->{_dark_selector};
 
+  # set collapse default based on presence of prefetch
+  my $prefetch;
+  if (
+    defined $attrs->{prefetch}
+      and
+    $prefetch = $self->_merge_joinpref_attr( {}, delete $attrs->{prefetch} )
+  ) {
     $self->throw_exception("Specifying prefetch in conjunction with an explicit collapse => 0 is unsupported")
       if defined $attrs->{collapse} and ! $attrs->{collapse};
 
     $attrs->{collapse} = 1;
-
-    # this is a separate structure (we don't look in {from} directly)
-    # as the resolver needs to shift things off the lists to work
-    # properly (identical-prefetches on different branches)
-    my $join_map = {};
-    if (ref $attrs->{from} eq 'ARRAY') {
-
-      my $start_depth = $attrs->{seen_join}{-relation_chain_depth} || 0;
-
-      for my $j ( @{$attrs->{from}}[1 .. $#{$attrs->{from}} ] ) {
-        next unless $j->[0]{-alias};
-        next unless $j->[0]{-join_path};
-        next if ($j->[0]{-relation_chain_depth} || 0) < $start_depth;
-
-        my @jpath = map { keys %$_ } @{$j->[0]{-join_path}};
-
-        my $p = $join_map;
-        $p = $p->{$_} ||= {} for @jpath[ ($start_depth/2) .. $#jpath]; #only even depths are actual jpath boundaries
-        push @{$p->{-join_aliases} }, $j->[0]{-alias};
-      }
-    }
-
-    my @prefetch = $source->_resolve_prefetch( $prefetch, $alias, $join_map );
-
-    # save these for after distinct resolution
-    @prefetch_select = map { $_->[0] } @prefetch;
-    @prefetch_as = map { $_->[1] } @prefetch;
   }
 
+
   # run through the resulting joinstructure (starting from our current slot)
   # and unset collapse if proven unnecessary
   #
@@ -3633,6 +3721,7 @@ sub _resolved_attrs {
     }
   }
 
+
   # generate the distinct induced group_by before injecting the prefetched select/as parts
   if (delete $attrs->{distinct}) {
     if ($attrs->{group_by}) {
@@ -3652,15 +3741,46 @@ sub _resolved_attrs {
     }
   }
 
-  # inject prefetch-bound selection (if any)
-  push @{$attrs->{select}}, @prefetch_select;
-  push @{$attrs->{as}}, @prefetch_as;
 
-  # whether we can get away with the dumbest (possibly DBI-internal) collapser
-  if ( List::Util::first { $_ =~ /\./ } @{$attrs->{as}} ) {
-    $attrs->{_related_results_construction} = 1;
+  # generate selections based on the prefetch helper
+  if ($prefetch) {
+
+    $self->throw_exception("Unable to prefetch, resultset contains an unnamed selector $attrs->{_dark_selector}{string}")
+      if $attrs->{_dark_selector};
+
+    # this is a separate structure (we don't look in {from} directly)
+    # as the resolver needs to shift things off the lists to work
+    # properly (identical-prefetches on different branches)
+    my $joined_node_aliases_map = {};
+    if (ref $attrs->{from} eq 'ARRAY') {
+
+      my $start_depth = $attrs->{seen_join}{-relation_chain_depth} || 0;
+
+      for my $j ( @{$attrs->{from}}[1 .. $#{$attrs->{from}} ] ) {
+        next unless $j->[0]{-alias};
+        next unless $j->[0]{-join_path};
+        next if ($j->[0]{-relation_chain_depth} || 0) < $start_depth;
+
+        my @jpath = map { keys %$_ } @{$j->[0]{-join_path}};
+
+        my $p = $joined_node_aliases_map;
+        $p = $p->{$_} ||= {} for @jpath[ ($start_depth/2) .. $#jpath]; #only even depths are actual jpath boundaries
+        push @{$p->{-join_aliases} }, $j->[0]{-alias};
+      }
+    }
+
+    ( push @{$attrs->{select}}, $_->[0] ) and ( push @{$attrs->{as}}, $_->[1] )
+      for $source->_resolve_selection_from_prefetch( $prefetch, $joined_node_aliases_map );
   }
 
+
+  $attrs->{_simple_passthrough_construction} = !(
+    $attrs->{collapse}
+      or
+    grep { $_ =~ /\./ } @{$attrs->{as}}
+  );
+
+
   # if both page and offset are specified, produce a combined offset
   # even though it doesn't make much sense, this is what pre 081xx has
   # been doing
@@ -3726,8 +3846,10 @@ sub _calculate_score {
 
   if (ref $b eq 'HASH') {
     my ($b_key) = keys %{$b};
+    $b_key = '' if ! defined $b_key;
     if (ref $a eq 'HASH') {
       my ($a_key) = keys %{$a};
+      $a_key = '' if ! defined $a_key;
       if ($a_key eq $b_key) {
         return (1 + $self->_calculate_score( $a->{$a_key}, $b->{$b_key} ));
       } else {
@@ -3979,46 +4101,65 @@ syntax as outlined above.
 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
+expression). Adds the L</current_source_alias> 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, but this is deprecated.)
+earlier versions of DBIC, but this is deprecated)
 
 Essentially C<columns> does the same as L</select> and L</as>.
 
-    columns => [ 'foo', { bar => 'baz' } ]
+    columns => [ 'some_column', { dbic_slot => 'another_column' } ]
 
 is the same as
 
-    select => [qw/foo baz/],
-    as => [qw/foo bar/]
+    select => [qw(some_column another_column)],
+    as     => [qw(some_column dbic_slot)]
+
+If you want to individually retrieve related columns (in essence perform
+manual L</prefetch>) you have to make sure to specify the correct inflation slot
+chain such that it matches existing relationships:
+
+    my $rs = $schema->resultset('Artist')->search({}, {
+        # required to tell DBIC to collapse has_many relationships
+        collapse => 1,
+        join     => { cds => 'tracks' },
+        '+columns'  => {
+          'cds.cdid'         => 'cds.cdid',
+          'cds.tracks.title' => 'tracks.title',
+        },
+    });
 
 Like elsewhere, literal SQL or literal values can be included by using a
 scalar reference or a literal bind value, and these values will be available
 in the result with C<get_column> (see also
-L<SQL::Abstract/Literal-SQL-and-value-type-operators>):
+L<SQL::Abstract/Literal SQL and value type operators>):
 
-    # equivalent SQL: SELECT 1, 'a string', IF(x,1,2) ...
+    # equivalent SQL: SELECT 1, 'a string', IF(my_column,?,?) ...
+    # bind values: $true_value, $false_value
     columns => [
         {
             foo => \1,
             bar => \q{'a string'},
-            baz => \[ '?', 'IF(x,1,2)' ],
+            baz => \[ 'IF(my_column,?,?)', $true_value, $false_value ],
         }
     ]
 
 =head2 +columns
 
+B<NOTE:> You B<MUST> explicitly quote C<'+columns'> when using this attribute.
+Not doing so causes Perl to incorrectly interpret C<+columns> as a bareword
+with a unary plus operator before it, which is the same as simply C<columns>.
+
 =over 4
 
-=item Value: \@columns
+=item Value: \@extra_columns
 
 =back
 
 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
+L</columns> but adds columns to the current selection. (You may also use the
 C<include_columns> attribute, as in earlier versions of DBIC, but this is
-deprecated). For example:-
+deprecated)
 
   $schema->resultset('CD')->search(undef, {
     '+columns' => ['artist.name'],
@@ -4030,20 +4171,6 @@ 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.
 
-B<NOTE:> You need to explicitly quote '+columns' when defining the attribute.
-Not doing so causes Perl to incorrectly interpret +columns as a bareword with a
-unary plus operator before it.
-
-=head2 include_columns
-
-=over 4
-
-=item Value: \@columns
-
-=back
-
-Deprecated.  Acts as a synonym for L</+columns> for backward compatibility.
-
 =head2 select
 
 =over 4
@@ -4069,25 +4196,28 @@ names:
 
 B<NOTE:> You will almost always need a corresponding L</as> attribute when you
 use L</select>, to instruct DBIx::Class how to store the result of the column.
-Also note that the L</as> attribute has nothing to do with the SQL-side 'AS'
-identifier aliasing. You can however alias a function, so you can use it in
-e.g. an C<ORDER BY> clause. This is done via the C<-as> B<select function
-attribute> supplied as shown in the example above.
 
-B<NOTE:> You need to explicitly quote '+select'/'+as' when defining the attributes.
-Not doing so causes Perl to incorrectly interpret them as a bareword with a
-unary plus operator before it.
+Also note that the L</as> attribute has B<nothing to do> with the SQL-side
+C<AS> identifier aliasing. You B<can> alias a function (so you can use it e.g.
+in an C<ORDER BY> clause), however this is done via the C<-as> B<select
+function attribute> supplied as shown in the example above.
 
 =head2 +select
 
+B<NOTE:> You B<MUST> explicitly quote C<'+select'> when using this attribute.
+Not doing so causes Perl to incorrectly interpret C<+select> as a bareword
+with a unary plus operator before it, which is the same as simply C<select>.
+
 =over 4
 
-Indicates additional columns to be selected from storage.  Works the same as
-L</select> but adds columns to the default selection, instead of specifying
-an explicit list.
+=item Value: \@extra_select_columns
 
 =back
 
+Indicates additional columns to be selected from storage.  Works the same as
+L</select> but adds columns to the current selection, instead of specifying
+a new explicit list.
+
 =head2 as
 
 =over 4
@@ -4096,12 +4226,14 @@ an explicit list.
 
 =back
 
-Indicates column names for object inflation. That is L</as> indicates the
+Indicates DBIC-side names for object inflation. That is L</as> indicates the
 slot name in which the column value will be stored within the
 L<Row|DBIx::Class::Row> object. The value will then be accessible via this
 identifier by the C<get_column> method (or via the object accessor B<if one
-with the same name already exists>) as shown below. The L</as> attribute has
-B<nothing to do> with the SQL-side C<AS>. See L</select> for details.
+with the same name already exists>) as shown below.
+
+The L</as> attribute has B<nothing to do> with the SQL-side identifier
+aliasing C<AS>. See L</select> for details.
 
   $rs = $schema->resultset('Employee')->search(undef, {
     select => [
@@ -4132,12 +4264,18 @@ L<DBIx::Class::Manual::Cookbook> for details.
 
 =head2 +as
 
+B<NOTE:> You B<MUST> explicitly quote C<'+as'> when using this attribute.
+Not doing so causes Perl to incorrectly interpret C<+as> as a bareword
+with a unary plus operator before it, which is the same as simply C<as>.
+
 =over 4
 
-Indicates additional column names for those added via L</+select>. See L</as>.
+=item Value: \@extra_inflation_names
 
 =back
 
+Indicates additional inflation names for selectors added via L</+select>. See L</as>.
+
 =head2 join
 
 =over 4
@@ -4271,8 +4409,10 @@ For a more in-depth discussion, see L</PREFETCHING>.
 
 This attribute is a shorthand for specifying a L</join> spec, adding all
 columns from the joined related sources as L</+columns> and setting
-L</collapse> to a true value. For example, the following two queries are
-equivalent:
+L</collapse> to a true value. It can be thought of as a rough B<superset>
+of the L</join> attribute.
+
+For example, the following two queries are equivalent:
 
   my $rs = $schema->resultset('Artist')->search({}, {
     prefetch => { cds => ['genre', 'tracks' ] },
@@ -4449,15 +4589,20 @@ A arrayref of columns to group by. Can include columns of joined tables.
 
 =back
 
-HAVING is a select statement attribute that is applied between GROUP BY and
-ORDER BY. It is applied to the after the grouping calculations have been
-done.
+The HAVING operator specifies a B<secondary> condition applied to the set
+after the grouping calculations have been done. In other words it is a
+constraint just like L</where> (and accepting the same
+L<SQL::Abstract syntax|SQL::Abstract/WHERE CLAUSES>) applied to the data
+as it exists after GROUP BY has taken place. Specifying L</having> without
+L</group_by> is a logical mistake, and a fatal error on most RDBMS engines.
+
+E.g.
 
   having => { 'count_employee' => { '>=', 100 } }
 
 or with an in-place function in which case literal SQL is required:
 
-  having => \[ 'count(employee) >= ?', [ count => 100 ] ]
+  having => \[ 'count(employee) >= ?', 100 ]
 
 =head2 distinct
 
@@ -4481,19 +4626,14 @@ setting is ignored and an appropriate warning is issued.
 
 =head2 where
 
-=over 4
-
-Adds to the WHERE clause.
+Adds extra conditions to the resultset, combined with the preexisting C<WHERE>
+conditions, same as the B<first> argument to the L<search operator|/search>
 
   # only return rows WHERE deleted IS NULL for all searches
   __PACKAGE__->resultset_attributes({ where => { deleted => undef } });
 
-Can be overridden by passing C<< { where => undef } >> as an attribute
-to a resultset.
-
-For more complicated where clauses see L<SQL::Abstract/WHERE CLAUSES>.
-
-=back
+Note that the above example is
+L<strongly discouraged|DBIx::Class::ResultSource/resultset_attributes>.
 
 =head2 cache
 
@@ -4506,7 +4646,7 @@ revisit rows in your ResultSet:
     ... do stuff ...
   }
 
-  $rs->first; # without cache, this would issue a query
+  $resultset->first; # without cache, this would issue a query
 
 By default, searches are not cached.
 
@@ -4699,11 +4839,15 @@ supported:
   [ undef,   $val ] === [ {}, $val ]
   $val              === [ {}, $val ]
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
+=cut
index e8510c3..e606f8a 100644 (file)
@@ -4,6 +4,9 @@ package # hide from pause
 use warnings;
 use strict;
 
+# temporary, to load MRO::Compat, will be soon entirely rewritten anyway
+use DBIx::Class::_Util;
+
 use base 'Data::Page';
 use mro 'c3';
 
index 1e2a0eb..e26b6c2 100644 (file)
@@ -406,7 +406,7 @@ sub func {
   my $cursor = $self->func_rs($function)->cursor;
 
   if( wantarray ) {
-    DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and my $sog = fail_on_internal_wantarray($self);
+    DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_WANTARRAY and my $sog = fail_on_internal_wantarray;
     return map { $_->[ 0 ] } $cursor->all;
   }
 
@@ -487,14 +487,14 @@ sub _resultset {
       unless( $cols{$select} ) {
         carp_unique(
           'Use of distinct => 1 while selecting anything other than a column '
-        . 'declared on the primary ResultSource is deprecated - please supply '
-        . 'an explicit group_by instead'
+        . 'declared on the primary ResultSource is deprecated (you selected '
+        . "'$self->{_as}') - please supply an explicit group_by instead"
         );
 
         # collapse the selector to a literal so that it survives the distinct parse
         # if it turns out to be an aggregate - at least the user will get a proper exception
         # instead of silent drop of the group_by altogether
-        $select = \ $rsrc->storage->sql_maker->_recurse_fields($select);
+        $select = \[ $rsrc->storage->sql_maker->_recurse_fields($select) ];
       }
     }
 
@@ -504,14 +504,18 @@ sub _resultset {
   };
 }
 
-1;
-
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
index d2746e5..bb9f3bf 100644 (file)
@@ -88,4 +88,17 @@ sub _register_resultset_class {
     }
 }
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
+=cut
+
 1;
index 5cb4036..0940e0d 100644 (file)
@@ -9,9 +9,9 @@ use DBIx::Class::ResultSet;
 use DBIx::Class::ResultSourceHandle;
 
 use DBIx::Class::Carp;
+use DBIx::Class::_Util qw( UNRESOLVABLE_CONDITION dbic_internal_try );
+use SQL::Abstract 'is_literal_value';
 use Devel::GlobalDestruction;
-use Try::Tiny;
-use List::Util 'first';
 use Scalar::Util qw/blessed weaken isweak/;
 
 use namespace::clean;
@@ -75,7 +75,7 @@ More specifically, the L<DBIx::Class::Core> base class pulls in the
 L<DBIx::Class::ResultSourceProxy::Table> component, which defines
 the L<table|DBIx::Class::ResultSourceProxy::Table/table> method.
 When called, C<table> creates and stores an instance of
-L<DBIx::Class::ResultSoure::Table>. Luckily, to use tables as result
+L<DBIx::Class::ResultSource::Table>. Luckily, to use tables as result
 sources, you don't need to remember any of this.
 
 Result sources representing select queries, or views, can also be
@@ -84,7 +84,8 @@ created, see L<DBIx::Class::ResultSource::View> for full details.
 =head2 Finding result source objects
 
 As mentioned above, a result source instance is created and stored for
-you when you define a L<result class|DBIx::Class::Manual::Glossary/Result class>.
+you when you define a
+L<Result Class|DBIx::Class::Manual::Glossary/Result Class>.
 
 You can retrieve the result source at runtime in the following ways:
 
@@ -106,7 +107,13 @@ You can retrieve the result source at runtime in the following ways:
 
 =head1 METHODS
 
-=pod
+=head2 new
+
+  $class->new();
+
+  $class->new({attribute_name => value});
+
+Creates a new ResultSource object.  Not normally called directly by end users.
 
 =cut
 
@@ -141,6 +148,11 @@ sub new {
 
   $source->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...);
 
+  $source->add_columns(
+    'col1' => { data_type => 'integer', is_nullable => 1, ... },
+    'col2' => { data_type => 'text',    is_auto_increment => 1, ... },
+  );
+
 Adds columns to the result source. If supplied colname => hashref
 pairs, uses the hashref as the L</column_info> for that column. Repeated
 calls of this method will add more columns, not replace them.
@@ -197,11 +209,17 @@ The length of your column, if it is a column type that can have a size
 restriction. This is currently only used to create tables from your
 schema, see L<DBIx::Class::Schema/deploy>.
 
+   { size => [ 9, 6 ] }
+
+For decimal or float values you can specify an ArrayRef in order to
+control precision, assuming your database's
+L<SQL::Translator::Producer> supports it.
+
 =item is_nullable
 
    { is_nullable => 1 }
 
-Set this to a true value for a columns that is allowed to contain NULL
+Set this to a true value for a column that is allowed to contain NULL
 values, default is false. This is currently only used to create tables
 from your schema, see L<DBIx::Class::Schema/deploy>.
 
@@ -384,12 +402,12 @@ sub column_info {
   if ( ! $self->_columns->{$column}{data_type}
        and ! $self->{_columns_info_loaded}
        and $self->column_info_from_storage
-       and my $stor = try { $self->storage } )
+       and my $stor = dbic_internal_try { $self->storage } )
   {
     $self->{_columns_info_loaded}++;
 
     # try for the case of storage without table
-    try {
+    dbic_internal_try {
       my $info = $stor->columns_info_for( $self->from );
       my $lc_info = { map
         { (lc $_) => $info->{$_} }
@@ -456,18 +474,18 @@ sub columns_info {
   my $colinfo = $self->_columns;
 
   if (
-    first { ! $_->{data_type} } values %$colinfo
-      and
     ! $self->{_columns_info_loaded}
       and
     $self->column_info_from_storage
       and
-    my $stor = try { $self->storage }
+    grep { ! $_->{data_type} } values %$colinfo
+      and
+    my $stor = dbic_internal_try { $self->storage }
   ) {
     $self->{_columns_info_loaded}++;
 
     # try for the case of storage without table
-    try {
+    dbic_internal_try {
       my $info = $stor->columns_info_for( $self->from );
       my $lc_info = { map
         { (lc $_) => $info->{$_} }
@@ -575,7 +593,7 @@ sub remove_column { shift->remove_columns(@_); } # DO NOT CHANGE THIS TO GLOB
 Defines one or more columns as primary key for this source. Must be
 called after L</add_columns>.
 
-Additionally, defines a L<unique constraint|add_unique_constraint>
+Additionally, defines a L<unique constraint|/add_unique_constraint>
 named C<primary>.
 
 Note: you normally do want to define a primary key on your sources
@@ -783,7 +801,7 @@ sub add_unique_constraints {
   my $self = shift;
   my @constraints = @_;
 
-  if ( !(@constraints % 2) && first { ref $_ ne 'ARRAY' } @constraints ) {
+  if ( !(@constraints % 2) && grep { ref $_ ne 'ARRAY' } @constraints ) {
     # with constraint name
     while (my ($name, $constraint) = splice @constraints, 0, 2) {
       $self->add_unique_constraint($name => $constraint);
@@ -829,6 +847,7 @@ sub name_unique_constraint {
 
   my $name = $self->name;
   $name = $$name if (ref $name eq 'SCALAR');
+  $name =~ s/ ^ [^\.]+ \. //x;  # strip possible schema qualifier
 
   return join '_', $name, @$cols;
 }
@@ -1110,7 +1129,7 @@ sub resultset {
   $self->resultset_class->new(
     $self,
     {
-      try { %{$self->schema->default_resultset_attributes} },
+      ( dbic_internal_try { %{$self->schema->default_resultset_attributes} } ),
       %{$self->{resultset_attributes}},
     },
   );
@@ -1172,6 +1191,17 @@ clause contents.
 
 sub from { die 'Virtual method!' }
 
+=head2 source_info
+
+Stores a hashref of per-source metadata.  No specific key names
+have yet been standardized, the examples below are purely hypothetical
+and don't actually accomplish anything on their own:
+
+  __PACKAGE__->source_info({
+    "_tablespace" => 'fast_disk_array_3',
+    "_engine" => 'InnoDB',
+  });
+
 =head2 schema
 
 =over 4
@@ -1313,10 +1343,11 @@ sub add_relationship {
 
   # Check foreign and self are right in cond
   if ( (ref $cond ||'') eq 'HASH') {
-    for (keys %$cond) {
-      $self->throw_exception("Keys of condition should be of form 'foreign.col', not '$_'")
-        if /\./ && !/^foreign\./;
-    }
+    $_ =~ /^foreign\./ or $self->throw_exception("Malformed relationship condition key '$_': must be prefixed with 'foreign.'")
+      for keys %$cond;
+
+    $_ =~ /^self\./ or $self->throw_exception("Malformed relationship condition value '$_': must be prefixed with 'self.'")
+      for values %$cond;
   }
 
   my %rels = %{ $self->_relationships };
@@ -1327,29 +1358,6 @@ sub add_relationship {
   $self->_relationships(\%rels);
 
   return $self;
-
-# XXX disabled. doesn't work properly currently. skip in tests.
-
-  my $f_source = $self->schema->source($f_source_name);
-  unless ($f_source) {
-    $self->ensure_class_loaded($f_source_name);
-    $f_source = $f_source_name->result_source;
-    #my $s_class = ref($self->schema);
-    #$f_source_name =~ m/^${s_class}::(.*)$/;
-    #$self->schema->register_class(($1 || $f_source_name), $f_source_name);
-    #$f_source = $self->schema->source($f_source_name);
-  }
-  return unless $f_source; # Can't test rel without f_source
-
-  try { $self->_resolve_join($rel, 'me', {}, []) }
-  catch {
-    # If the resolve failed, back out and re-throw the error
-    delete $rels{$rel};
-    $self->_relationships(\%rels);
-    $self->throw_exception("Error creating relationship $rel: $_");
-  };
-
-  1;
 }
 
 =head2 relationships
@@ -1362,7 +1370,7 @@ sub add_relationship {
 
 =back
 
-  my @relnames = $source->relationships();
+  my @rel_names = $source->relationships();
 
 Returns all relationship names for this source.
 
@@ -1462,7 +1470,7 @@ sub reverse_relationship_info {
     # to use the source_names, otherwise we will use the actual classes
 
     # the schema may be partial
-    my $roundtrip_rsrc = try { $other_rsrc->related_source($other_rel) }
+    my $roundtrip_rsrc = dbic_internal_try { $other_rsrc->related_source($other_rel) }
       or next;
 
     if ($registered_source_name) {
@@ -1545,6 +1553,67 @@ sub _identifying_column_set {
   return undef;
 }
 
+sub _minimal_valueset_satisfying_constraint {
+  my $self = shift;
+  my $args = { ref $_[0] eq 'HASH' ? %{ $_[0] } : @_ };
+
+  $args->{columns_info} ||= $self->columns_info;
+
+  my $vals = $self->storage->_extract_fixed_condition_columns(
+    $args->{values},
+    ($args->{carp_on_nulls} ? 'consider_nulls' : undef ),
+  );
+
+  my $cols;
+  for my $col ($self->unique_constraint_columns($args->{constraint_name}) ) {
+    if( ! exists $vals->{$col} or ( $vals->{$col}||'' ) eq UNRESOLVABLE_CONDITION ) {
+      $cols->{missing}{$col} = undef;
+    }
+    elsif( ! defined $vals->{$col} ) {
+      $cols->{$args->{carp_on_nulls} ? 'undefined' : 'missing'}{$col} = undef;
+    }
+    else {
+      # we need to inject back the '=' as _extract_fixed_condition_columns
+      # will strip it from literals and values alike, resulting in an invalid
+      # condition in the end
+      $cols->{present}{$col} = { '=' => $vals->{$col} };
+    }
+
+    $cols->{fc}{$col} = 1 if (
+      ( ! $cols->{missing} or ! exists $cols->{missing}{$col} )
+        and
+      keys %{ $args->{columns_info}{$col}{_filter_info} || {} }
+    );
+  }
+
+  $self->throw_exception( sprintf ( "Unable to satisfy requested constraint '%s', missing values for column(s): %s",
+    $args->{constraint_name},
+    join (', ', map { "'$_'" } sort keys %{$cols->{missing}} ),
+  ) ) if $cols->{missing};
+
+  $self->throw_exception( sprintf (
+    "Unable to satisfy requested constraint '%s', FilterColumn values not usable for column(s): %s",
+    $args->{constraint_name},
+    join (', ', map { "'$_'" } sort keys %{$cols->{fc}}),
+  )) if $cols->{fc};
+
+  if (
+    $cols->{undefined}
+      and
+    !$ENV{DBIC_NULLABLE_KEY_NOWARN}
+  ) {
+    carp_unique ( sprintf (
+      "NULL/undef values supplied for requested unique constraint '%s' (NULL "
+    . 'values in column(s): %s). This is almost certainly not what you wanted, '
+    . 'though you can set DBIC_NULLABLE_KEY_NOWARN to disable this warning.',
+      $args->{constraint_name},
+      join (', ', map { "'$_'" } sort keys %{$cols->{undefined}}),
+    ));
+  }
+
+  return { map { %{ $cols->{$_}||{} } } qw(present undefined) };
+}
+
 # Returns the {from} structure used to express JOIN conditions
 sub _resolve_join {
   my ($self, $join, $alias, $seen, $jpath, $parent_force_left) = @_;
@@ -1614,14 +1683,20 @@ sub _resolve_join {
                 ,
                -join_path => [@$jpath, { $join => $as } ],
                -is_single => (
-                  (! $rel_info->{attrs}{accessor})
+                  ! $rel_info->{attrs}{accessor}
+                    or
+                  $rel_info->{attrs}{accessor} eq 'single'
                     or
-                  first { $rel_info->{attrs}{accessor} eq $_ } (qw/single filter/)
+                  $rel_info->{attrs}{accessor} eq 'filter'
                 ),
                -alias => $as,
                -relation_chain_depth => ( $seen->{-relation_chain_depth} || 0 ) + 1,
              },
-             scalar $self->_resolve_condition($rel_info->{cond}, $as, $alias, $join)
+             $self->_resolve_relationship_condition(
+               rel_name => $join,
+               self_alias => $alias,
+               foreign_alias => $as,
+             )->{condition},
           ];
   }
 }
@@ -1670,150 +1745,504 @@ sub _pk_depends_on {
 
 sub resolve_condition {
   carp 'resolve_condition is a private method, stop calling it';
-  my $self = shift;
-  $self->_resolve_condition (@_);
+  shift->_resolve_condition (@_);
 }
 
-our $UNRESOLVABLE_CONDITION = \ '1 = 0';
-
-# Resolves the passed condition to a concrete query fragment and a flag
-# indicating whether this is a cross-table condition. Also an optional
-# list of non-trivial values (normally conditions) returned as a part
-# of a joinfree condition hash
 sub _resolve_condition {
-  my ($self, $cond, $as, $for, $rel_name) = @_;
+#  carp_unique sprintf
+#    '_resolve_condition is a private method, and moreover is about to go '
+#  . 'away. Please contact the development team at %s if you believe you '
+#  . 'have a genuine use for this method, in order to discuss alternatives.',
+#    DBIx::Class::_ENV_::HELP_URL,
+#  ;
+
+#######################
+### API Design? What's that...? (a backwards compatible shim, kill me now)
+
+  my ($self, $cond, @res_args, $rel_name);
+
+  # we *SIMPLY DON'T KNOW YET* which arg is which, yay
+  ($self, $cond, $res_args[0], $res_args[1], $rel_name) = @_;
+
+  # assume that an undef is an object-like unset (set_from_related(undef))
+  my @is_objlike = map { ! defined $_ or length ref $_ } (@res_args);
+
+  # turn objlike into proper objects for saner code further down
+  for (0,1) {
+    next unless $is_objlike[$_];
+
+    if ( defined blessed $res_args[$_] ) {
+
+      # but wait - there is more!!! WHAT THE FUCK?!?!?!?!
+      if ($res_args[$_]->isa('DBIx::Class::ResultSet')) {
+        carp('Passing a resultset for relationship resolution makes no sense - invoking __gremlins__');
+        $is_objlike[$_] = 0;
+        $res_args[$_] = '__gremlins__';
+      }
+    }
+    else {
+      $res_args[$_] ||= {};
+
+      # hate everywhere - have to pass in as a plain hash
+      # pretending to be an object at least for now
+      $self->throw_exception("Unsupported object-like structure encountered: $res_args[$_]")
+        unless ref $res_args[$_] eq 'HASH';
+    }
+  }
+
+  my $args = {
+    # where-is-waldo block guesses relname, then further down we override it if available
+    (
+      $is_objlike[1] ? ( rel_name => $res_args[0], self_alias => $res_args[0], foreign_alias => 'me',         self_result_object  => $res_args[1] )
+    : $is_objlike[0] ? ( rel_name => $res_args[1], self_alias => 'me',         foreign_alias => $res_args[1], foreign_values      => $res_args[0] )
+    :                  ( rel_name => $res_args[0], self_alias => $res_args[1], foreign_alias => $res_args[0]                                      )
+    ),
+
+    ( $rel_name ? ( rel_name => $rel_name ) : () ),
+  };
+
+  # Allowing passing relconds different than the relationshup itself is cute,
+  # but likely dangerous. Remove that from the (still unofficial) API of
+  # _resolve_relationship_condition, and instead make it "hard on purpose"
+  local $self->relationship_info( $args->{rel_name} )->{cond} = $cond if defined $cond;
+
+#######################
+
+  # now it's fucking easy isn't it?!
+  my $rc = $self->_resolve_relationship_condition( $args );
+
+  my @res = (
+    ( $rc->{join_free_condition} || $rc->{condition} ),
+    ! $rc->{join_free_condition},
+  );
+
+  # _resolve_relationship_condition always returns qualified cols even in the
+  # case of join_free_condition, but nothing downstream expects this
+  if ($rc->{join_free_condition} and ref $res[0] eq 'HASH') {
+    $res[0] = { map
+      { ($_ =~ /\.(.+)/) => $res[0]{$_} }
+      keys %{$res[0]}
+    };
+  }
+
+  # and more legacy
+  return wantarray ? @res : $res[0];
+}
+
+# Keep this indefinitely. There is evidence of both CPAN and
+# darkpan using it, and there isn't much harm in an extra var
+# anyway.
+our $UNRESOLVABLE_CONDITION = UNRESOLVABLE_CONDITION;
+# YES I KNOW THIS IS EVIL
+# it is there to save darkpan from themselves, since internally
+# we are moving to a constant
+Internals::SvREADONLY($UNRESOLVABLE_CONDITION => 1);
+
+# Resolves the passed condition to a concrete query fragment and extra
+# metadata
+#
+## self-explanatory API, modeled on the custom cond coderef:
+# rel_name              => (scalar)
+# foreign_alias         => (scalar)
+# foreign_values        => (either not supplied, or a hashref, or a foreign ResultObject (to be ->get_columns()ed), or plain undef )
+# self_alias            => (scalar)
+# self_result_object    => (either not supplied or a result object)
+# require_join_free_condition => (boolean, throws on failure to construct a JF-cond)
+# infer_values_based_on => (either not supplied or a hashref, implies require_join_free_condition)
+#
+## returns a hash
+# condition           => (a valid *likely fully qualified* sqla cond structure)
+# identity_map        => (a hashref of foreign-to-self *unqualified* column equality names)
+# join_free_condition => (a valid *fully qualified* sqla cond structure, maybe unset)
+# inferred_values     => (in case of an available join_free condition, this is a hashref of
+#                         *unqualified* column/value *EQUALITY* pairs, representing an amalgamation
+#                         of the JF-cond parse and infer_values_based_on
+#                         always either complete or unset)
+#
+sub _resolve_relationship_condition {
+  my $self = shift;
+
+  my $args = { ref $_[0] eq 'HASH' ? %{ $_[0] } : @_ };
+
+  for ( qw( rel_name self_alias foreign_alias ) ) {
+    $self->throw_exception("Mandatory argument '$_' to _resolve_relationship_condition() is not a plain string")
+      if !defined $args->{$_} or length ref $args->{$_};
+  }
+
+  $self->throw_exception("Arguments 'self_alias' and 'foreign_alias' may not be identical")
+    if $args->{self_alias} eq $args->{foreign_alias};
+
+# TEMP
+  my $exception_rel_id = "relationship '$args->{rel_name}' on source '@{[ $self->source_name ]}'";
+
+  my $rel_info = $self->relationship_info($args->{rel_name})
+# TEMP
+#    or $self->throw_exception( "No such $exception_rel_id" );
+    or carp_unique("Requesting resolution on non-existent relationship '$args->{rel_name}' on source '@{[ $self->source_name ]}': fix your code *soon*, as it will break with the next major version");
+
+# TEMP
+  $exception_rel_id = "relationship '$rel_info->{_original_name}' on source '@{[ $self->source_name ]}'"
+    if $rel_info and exists $rel_info->{_original_name};
+
+  $self->throw_exception("No practical way to resolve $exception_rel_id between two data structures")
+    if exists $args->{self_result_object} and exists $args->{foreign_values};
+
+  $self->throw_exception( "Argument to infer_values_based_on must be a hash" )
+    if exists $args->{infer_values_based_on} and ref $args->{infer_values_based_on} ne 'HASH';
+
+  $args->{require_join_free_condition} ||= !!$args->{infer_values_based_on};
+
+  $self->throw_exception( "Argument 'self_result_object' must be an object inheriting from DBIx::Class::Row" )
+    if (
+      exists $args->{self_result_object}
+        and
+      ( ! defined blessed $args->{self_result_object} or ! $args->{self_result_object}->isa('DBIx::Class::Row') )
+    )
+  ;
+
+  my $rel_rsrc = $self->related_source($args->{rel_name});
+  my $storage = $self->schema->storage;
+
+  if (exists $args->{foreign_values}) {
+
+    if (! defined $args->{foreign_values} ) {
+      # fallback: undef => {}
+      $args->{foreign_values} = {};
+    }
+    elsif (defined blessed $args->{foreign_values}) {
+
+      $self->throw_exception( "Objects supplied as 'foreign_values' ($args->{foreign_values}) must inherit from DBIx::Class::Row" )
+        unless $args->{foreign_values}->isa('DBIx::Class::Row');
+
+      carp_unique(
+        "Objects supplied as 'foreign_values' ($args->{foreign_values}) "
+      . "usually should inherit from the related ResultClass ('@{[ $rel_rsrc->result_class ]}'), "
+      . "perhaps you've made a mistake invoking the condition resolver?"
+      ) unless $args->{foreign_values}->isa($rel_rsrc->result_class);
+
+      $args->{foreign_values} = { $args->{foreign_values}->get_columns };
+    }
+    elsif ( ref $args->{foreign_values} eq 'HASH' ) {
+
+      # re-build {foreign_values} excluding identically named rels
+      if( keys %{$args->{foreign_values}} ) {
+
+        my ($col_idx, $rel_idx) = map
+          { { map { $_ => 1 } $rel_rsrc->$_ } }
+          qw( columns relationships )
+        ;
+
+        my $equivalencies = $storage->_extract_fixed_condition_columns(
+          $args->{foreign_values},
+          'consider nulls',
+        );
+
+        $args->{foreign_values} = { map {
+          # skip if relationship *and* a non-literal ref
+          # this means a multicreate stub was passed in
+          (
+            $rel_idx->{$_}
+              and
+            length ref $args->{foreign_values}{$_}
+              and
+            ! is_literal_value($args->{foreign_values}{$_})
+          )
+            ? ()
+            : ( $_ => (
+                ! $col_idx->{$_}
+                  ? $self->throw_exception( "Key '$_' supplied as 'foreign_values' is not a column on related source '@{[ $rel_rsrc->source_name ]}'" )
+              : ( !exists $equivalencies->{$_} or ($equivalencies->{$_}||'') eq UNRESOLVABLE_CONDITION )
+                  ? $self->throw_exception( "Value supplied for '...{foreign_values}{$_}' is not a direct equivalence expression" )
+              : $args->{foreign_values}{$_}
+            ))
+        } keys %{$args->{foreign_values}} };
+      }
+    }
+    else {
+      $self->throw_exception(
+        "Argument 'foreign_values' must be either an object inheriting from '@{[ $rel_rsrc->result_class ]}', "
+      . "or a hash reference, or undef"
+      );
+    }
+  }
 
-  my $obj_rel = defined blessed $for;
+  my $ret;
 
-  if (ref $cond eq 'CODE') {
-    my $relalias = $obj_rel ? 'me' : $as;
+  if (ref $rel_info->{cond} eq 'CODE') {
 
-    my ($crosstable_cond, $joinfree_cond) = $cond->({
-      self_alias => $obj_rel ? $as : $for,
-      foreign_alias => $relalias,
+    my $cref_args = {
+      rel_name => $args->{rel_name},
       self_resultsource => $self,
-      foreign_relname => $rel_name || ($obj_rel ? $as : $for),
-      self_rowobj => $obj_rel ? $for : undef
-    });
+      self_alias => $args->{self_alias},
+      foreign_alias => $args->{foreign_alias},
+      ( map
+        { (exists $args->{$_}) ? ( $_ => $args->{$_} ) : () }
+        qw( self_result_object foreign_values )
+      ),
+    };
+
+    # legacy - never remove these!!!
+    $cref_args->{foreign_relname} = $cref_args->{rel_name};
 
-    my $cond_cols;
-    if ($joinfree_cond) {
+    $cref_args->{self_rowobj} = $cref_args->{self_result_object}
+      if exists $cref_args->{self_result_object};
+
+    ($ret->{condition}, $ret->{join_free_condition}, my @extra) = $rel_info->{cond}->($cref_args);
+
+    # sanity check
+    $self->throw_exception("A custom condition coderef can return at most 2 conditions, but $exception_rel_id returned extra values: @extra")
+      if @extra;
+
+    if (my $jfc = $ret->{join_free_condition}) {
+
+      $self->throw_exception (
+        "The join-free condition returned for $exception_rel_id must be a hash reference"
+      ) unless ref $jfc eq 'HASH';
+
+      my ($joinfree_alias, $joinfree_source);
+      if (defined $args->{self_result_object}) {
+        $joinfree_alias = $args->{foreign_alias};
+        $joinfree_source = $rel_rsrc;
+      }
+      elsif (defined $args->{foreign_values}) {
+        $joinfree_alias = $args->{self_alias};
+        $joinfree_source = $self;
+      }
 
       # FIXME sanity check until things stabilize, remove at some point
       $self->throw_exception (
-        "A join-free condition returned for relationship '$rel_name' without a row-object to chain from"
-      ) unless $obj_rel;
-
-      # FIXME another sanity check
-      if (
-        ref $joinfree_cond ne 'HASH'
-          or
-        first { $_ !~ /^\Q$relalias.\E.+/ } keys %$joinfree_cond
-      ) {
+        "A join-free condition returned for $exception_rel_id without a result object to chain from"
+      ) unless $joinfree_alias;
+
+      my $fq_col_list = { map
+        { ( "$joinfree_alias.$_" => 1 ) }
+        $joinfree_source->columns
+      };
+
+      exists $fq_col_list->{$_} or $self->throw_exception (
+        "The join-free condition returned for $exception_rel_id may only "
+      . 'contain keys that are fully qualified column names of the corresponding source '
+      . "'$joinfree_alias' (instead it returned '$_')"
+      ) for keys %$jfc;
+
+      (
+        length ref $_
+          and
+        defined blessed($_)
+          and
+        $_->isa('DBIx::Class::Row')
+          and
         $self->throw_exception (
-          "The join-free condition returned for relationship '$rel_name' must be a hash "
-         .'reference with all keys being valid columns on the related result source'
-        );
-      }
+          "The join-free condition returned for $exception_rel_id may not "
+        . 'contain result objects as values - perhaps instead of invoking '
+        . '->$something you meant to return ->get_column($something)'
+        )
+      ) for values %$jfc;
 
-      # normalize
-      for (values %$joinfree_cond) {
-        $_ = $_->{'='} if (
-          ref $_ eq 'HASH'
-            and
-          keys %$_ == 1
-            and
-          exists $_->{'='}
-        );
-      }
+    }
+  }
+  elsif (ref $rel_info->{cond} eq 'HASH') {
 
-      # see which parts of the joinfree cond are conditionals
-      my $relcol_list = { map { $_ => 1 } $self->related_source($rel_name)->columns };
+    # the condition is static - use parallel arrays
+    # for a "pivot" depending on which side of the
+    # rel did we get as an object
+    my (@f_cols, @l_cols);
+    for my $fc (keys %{ $rel_info->{cond} }) {
+      my $lc = $rel_info->{cond}{$fc};
 
-      for my $c (keys %$joinfree_cond) {
-        my ($colname) = $c =~ /^ (?: \Q$relalias.\E )? (.+)/x;
+      # FIXME STRICTMODE should probably check these are valid columns
+      $fc =~ s/^foreign\.// ||
+        $self->throw_exception("Invalid rel cond key '$fc'");
 
-        unless ($relcol_list->{$colname}) {
-          push @$cond_cols, $colname;
-          next;
-        }
+      $lc =~ s/^self\.// ||
+        $self->throw_exception("Invalid rel cond val '$lc'");
 
-        if (
-          ref $joinfree_cond->{$c}
-            and
-          ref $joinfree_cond->{$c} ne 'SCALAR'
-            and
-          ref $joinfree_cond->{$c} ne 'REF'
-        ) {
-          push @$cond_cols, $colname;
-          next;
+      push @f_cols, $fc;
+      push @l_cols, $lc;
+    }
+
+    # construct the crosstable condition and the identity map
+    for  (0..$#f_cols) {
+      $ret->{condition}{"$args->{foreign_alias}.$f_cols[$_]"} = { -ident => "$args->{self_alias}.$l_cols[$_]" };
+      $ret->{identity_map}{$l_cols[$_]} = $f_cols[$_];
+    };
+
+    if ($args->{foreign_values}) {
+      $ret->{join_free_condition}{"$args->{self_alias}.$l_cols[$_]"} = $args->{foreign_values}{$f_cols[$_]}
+        for 0..$#f_cols;
+    }
+    elsif (defined $args->{self_result_object}) {
+
+      for my $i (0..$#l_cols) {
+        if ( $args->{self_result_object}->has_column_loaded($l_cols[$i]) ) {
+          $ret->{join_free_condition}{"$args->{foreign_alias}.$f_cols[$i]"} = $args->{self_result_object}->get_column($l_cols[$i]);
+        }
+        else {
+          $self->throw_exception(sprintf
+            "Unable to resolve relationship '%s' from object '%s': column '%s' not "
+          . 'loaded from storage (or not passed to new() prior to insert()). You '
+          . 'probably need to call ->discard_changes to get the server-side defaults '
+          . 'from the database.',
+            $args->{rel_name},
+            $args->{self_result_object},
+            $l_cols[$i],
+          ) if $args->{self_result_object}->in_storage;
+
+          # FIXME - temporarly force-override
+          delete $args->{require_join_free_condition};
+          $ret->{join_free_condition} = UNRESOLVABLE_CONDITION;
+          last;
         }
       }
-
-      return wantarray ? ($joinfree_cond, 0, $cond_cols) : $joinfree_cond;
+    }
+  }
+  elsif (ref $rel_info->{cond} eq 'ARRAY') {
+    if (@{ $rel_info->{cond} } == 0) {
+      $ret = {
+        condition => UNRESOLVABLE_CONDITION,
+        join_free_condition => UNRESOLVABLE_CONDITION,
+      };
     }
     else {
-      return wantarray ? ($crosstable_cond, 1) : $crosstable_cond;
+      my @subconds = map {
+        local $rel_info->{cond} = $_;
+        $self->_resolve_relationship_condition( $args );
+      } @{ $rel_info->{cond} };
+
+      if( @{ $rel_info->{cond} } == 1 ) {
+        $ret = $subconds[0];
+      }
+      else {
+        # we are discarding inferred values here... likely incorrect...
+        # then again - the entire thing is an OR, so we *can't* use them anyway
+        for my $subcond ( @subconds ) {
+          $self->throw_exception('Either all or none of the OR-condition members must resolve to a join-free condition')
+            if ( $ret and ( $ret->{join_free_condition} xor $subcond->{join_free_condition} ) );
+
+          $subcond->{$_} and push @{$ret->{$_}}, $subcond->{$_} for (qw(condition join_free_condition));
+        }
+      }
     }
   }
-  elsif (ref $cond eq 'HASH') {
-    my %ret;
-    foreach my $k (keys %{$cond}) {
-      my $v = $cond->{$k};
-      # XXX should probably check these are valid columns
-      $k =~ s/^foreign\.// ||
-        $self->throw_exception("Invalid rel cond key ${k}");
-      $v =~ s/^self\.// ||
-        $self->throw_exception("Invalid rel cond val ${v}");
-      if (ref $for) { # Object
-        #warn "$self $k $for $v";
-        unless ($for->has_column_loaded($v)) {
-          if ($for->in_storage) {
-            $self->throw_exception(sprintf
-              "Unable to resolve relationship '%s' from object %s: column '%s' not "
-            . 'loaded from storage (or not passed to new() prior to insert()). You '
-            . 'probably need to call ->discard_changes to get the server-side defaults '
-            . 'from the database.',
-              $as,
-              $for,
-              $v,
-            );
-          }
-          return $UNRESOLVABLE_CONDITION;
+  else {
+    $self->throw_exception ("Can't handle condition $rel_info->{cond} for $exception_rel_id yet :(");
+  }
+
+  if (
+    $args->{require_join_free_condition}
+      and
+    ( ! $ret->{join_free_condition} or $ret->{join_free_condition} eq UNRESOLVABLE_CONDITION )
+  ) {
+    $self->throw_exception(
+      ucfirst sprintf "$exception_rel_id does not resolve to a %sjoin-free condition fragment",
+        exists $args->{foreign_values}
+          ? "'foreign_values'-based reversed-"
+          : ''
+    );
+  }
+
+  # we got something back - sanity check and infer values if we can
+  my @nonvalues;
+  if (
+    $ret->{join_free_condition}
+      and
+    $ret->{join_free_condition} ne UNRESOLVABLE_CONDITION
+      and
+    my $jfc = $storage->_collapse_cond( $ret->{join_free_condition} )
+  ) {
+
+    my $jfc_eqs = $storage->_extract_fixed_condition_columns($jfc, 'consider_nulls');
+
+    if (keys %$jfc_eqs) {
+
+      for (keys %$jfc) {
+        # $jfc is fully qualified by definition
+        my ($col) = $_ =~ /\.(.+)/;
+
+        if (exists $jfc_eqs->{$_} and ($jfc_eqs->{$_}||'') ne UNRESOLVABLE_CONDITION) {
+          $ret->{inferred_values}{$col} = $jfc_eqs->{$_};
+        }
+        elsif ( !$args->{infer_values_based_on} or ! exists $args->{infer_values_based_on}{$col} ) {
+          push @nonvalues, $col;
         }
-        $ret{$k} = $for->get_column($v);
-        #$ret{$k} = $for->get_column($v) if $for->has_column_loaded($v);
-        #warn %ret;
-      } elsif (!defined $for) { # undef, i.e. "no object"
-        $ret{$k} = undef;
-      } elsif (ref $as eq 'HASH') { # reverse hashref
-        $ret{$v} = $as->{$k};
-      } elsif (ref $as) { # reverse object
-        $ret{$v} = $as->get_column($k);
-      } elsif (!defined $as) { # undef, i.e. "no reverse object"
-        $ret{$v} = undef;
-      } else {
-        $ret{"${as}.${k}"} = { -ident => "${for}.${v}" };
       }
+
+      # all or nothing
+      delete $ret->{inferred_values} if @nonvalues;
     }
+  }
+
+  # did the user explicitly ask
+  if ($args->{infer_values_based_on}) {
+
+    $self->throw_exception(sprintf (
+      "Unable to complete value inferrence - custom $exception_rel_id returns conditions instead of values for column(s): %s",
+      map { "'$_'" } @nonvalues
+    )) if @nonvalues;
 
-    return wantarray
-      ? ( \%ret, ($obj_rel || !defined $as || ref $as) ? 0 : 1 )
-      : \%ret
-    ;
+
+    $ret->{inferred_values} ||= {};
+
+    $ret->{inferred_values}{$_} = $args->{infer_values_based_on}{$_}
+      for keys %{$args->{infer_values_based_on}};
   }
-  elsif (ref $cond eq 'ARRAY') {
-    my (@ret, $crosstable);
-    for (@$cond) {
-      my ($cond, $crosstab) = $self->_resolve_condition($_, $as, $for, $rel_name);
-      push @ret, $cond;
-      $crosstable ||= $crosstab;
+
+  # add the identities based on the main condition
+  # (may already be there, since easy to calculate on the fly in the HASH case)
+  if ( ! $ret->{identity_map} ) {
+
+    my $col_eqs = $storage->_extract_fixed_condition_columns($ret->{condition});
+
+    my $colinfos;
+    for my $lhs (keys %$col_eqs) {
+
+      next if $col_eqs->{$lhs} eq UNRESOLVABLE_CONDITION;
+
+      # there is no way to know who is right and who is left in a cref
+      # therefore a full blown resolution call, and figure out the
+      # direction a bit further below
+      $colinfos ||= $storage->_resolve_column_info([
+        { -alias => $args->{self_alias}, -rsrc => $self },
+        { -alias => $args->{foreign_alias}, -rsrc => $rel_rsrc },
+      ]);
+
+      next unless $colinfos->{$lhs};  # someone is engaging in witchcraft
+
+      if ( my $rhs_ref = is_literal_value( $col_eqs->{$lhs} ) ) {
+
+        if (
+          $colinfos->{$rhs_ref->[0]}
+            and
+          $colinfos->{$lhs}{-source_alias} ne $colinfos->{$rhs_ref->[0]}{-source_alias}
+        ) {
+          ( $colinfos->{$lhs}{-source_alias} eq $args->{self_alias} )
+            ? ( $ret->{identity_map}{$colinfos->{$lhs}{-colname}} = $colinfos->{$rhs_ref->[0]}{-colname} )
+            : ( $ret->{identity_map}{$colinfos->{$rhs_ref->[0]}{-colname}} = $colinfos->{$lhs}{-colname} )
+          ;
+        }
+      }
+      elsif (
+        $col_eqs->{$lhs} =~ /^ ( \Q$args->{self_alias}\E \. .+ ) /x
+          and
+        ($colinfos->{$1}||{})->{-result_source} == $rel_rsrc
+      ) {
+        my ($lcol, $rcol) = map
+          { $colinfos->{$_}{-colname} }
+          ( $lhs, $1 )
+        ;
+        carp_unique(
+          "The $exception_rel_id specifies equality of column '$lcol' and the "
+        . "*VALUE* '$rcol' (you did not use the { -ident => ... } operator)"
+        );
+      }
     }
-    return wantarray ? (\@ret, $crosstable) : \@ret;
-  }
-  else {
-    $self->throw_exception ("Can't handle condition $cond for relationship '$rel_name' yet :(");
   }
+
+  # FIXME - temporary, to fool the idiotic check in SQLMaker::_join_condition
+  $ret->{condition} = { -and => [ $ret->{condition} ] }
+    unless $ret->{condition} eq UNRESOLVABLE_CONDITION;
+
+  $ret;
 }
 
 =head2 related_source
@@ -1839,7 +2268,7 @@ sub related_source {
   # if we are not registered with a schema - just use the prototype
   # however if we do have a schema - ask for the source by name (and
   # throw in the process if all fails)
-  if (my $schema = try { $self->schema }) {
+  if (my $schema = dbic_internal_try { $self->schema }) {
     $schema->source($self->relationship_info($rel)->{source});
   }
   else {
@@ -1902,6 +2331,9 @@ sub handle {
 
 my $global_phase_destroy;
 sub DESTROY {
+  ### NO detected_reinvoked_destructor check
+  ### This code very much relies on being called multuple times
+
   return if $global_phase_destroy ||= in_global_destruction;
 
 ######
@@ -1944,7 +2376,10 @@ sub DESTROY {
     $global_phase_destroy = 1;
   };
 
-  return;
+  # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+  # collected before leaving this scope. Depending on the code above, this
+  # may very well be just a preventive measure guarding future modifications
+  undef;
 }
 
 sub STORABLE_freeze { Storable::nfreeze($_[0]->handle) }
@@ -1969,25 +2404,6 @@ sub throw_exception {
   ;
 }
 
-=head2 source_info
-
-Stores a hashref of per-source metadata.  No specific key names
-have yet been standardized, the examples below are purely hypothetical
-and don't actually accomplish anything on their own:
-
-  __PACKAGE__->source_info({
-    "_tablespace" => 'fast_disk_array_3',
-    "_engine" => 'InnoDB',
-  });
-
-=head2 new
-
-  $class->new();
-
-  $class->new({attribute_name => value});
-
-Creates a new ResultSource object.  Not normally called directly by end users.
-
 =head2 column_info_from_storage
 
 =over
@@ -2004,14 +2420,16 @@ Enables the on-demand automatic loading of the above column
 metadata from storage as necessary.  This is *deprecated*, and
 should not be used.  It will be removed before 1.0.
 
+=head1 FURTHER QUESTIONS?
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 1c84b3c..83be406 100644 (file)
@@ -14,32 +14,32 @@ use DBIx::Class::ResultSource::RowParser::Util qw(
   assemble_collapsing_parser
 );
 
+use DBIx::Class::Carp;
+
 use namespace::clean;
 
-# Accepts one or more relationships for the current source and returns an
-# array of column names for each of those relationships. Column names are
-# prefixed relative to the current source, in accordance with where they appear
-# in the supplied relationships.
-sub _resolve_prefetch {
-  my ($self, $pre, $alias, $alias_map, $order, $pref_path) = @_;
+# Accepts a prefetch map (one or more relationships for the current source),
+# returns a set of select/as pairs for each of those relationships. Columns
+# are fully qualified inflation_slot names
+sub _resolve_selection_from_prefetch {
+  my ($self, $pre, $alias_map, $pref_path) = @_;
+
+  # internal recursion marker
   $pref_path ||= [];
 
   if (not defined $pre or not length $pre) {
     return ();
   }
   elsif( ref $pre eq 'ARRAY' ) {
-    return
-      map { $self->_resolve_prefetch( $_, $alias, $alias_map, $order, [ @$pref_path ] ) }
-        @$pre;
+    map { $self->_resolve_selection_from_prefetch( $_, $alias_map, [ @$pref_path ] ) }
+      @$pre;
   }
   elsif( ref $pre eq 'HASH' ) {
-    my @ret =
     map {
-      $self->_resolve_prefetch($_, $alias, $alias_map, $order, [ @$pref_path ] ),
-      $self->related_source($_)->_resolve_prefetch(
-         $pre->{$_}, "${alias}.$_", $alias_map, $order, [ @$pref_path, $_] )
+      $self->_resolve_selection_from_prefetch($_, $alias_map, [ @$pref_path ] ),
+      $self->related_source($_)->_resolve_selection_from_prefetch(
+         $pre->{$_}, $alias_map, [ @$pref_path, $_] )
     } keys %$pre;
-    return @ret;
   }
   elsif( ref $pre ) {
     $self->throw_exception(
@@ -47,26 +47,40 @@ sub _resolve_prefetch {
   }
   else {
     my $p = $alias_map;
-    $p = $p->{$_} for (@$pref_path, $pre);
+    $p = $p->{$_} for @$pref_path, $pre;
 
     $self->throw_exception (
       "Unable to resolve prefetch '$pre' - join alias map does not contain an entry for path: "
       . join (' -> ', @$pref_path, $pre)
     ) if (ref $p->{-join_aliases} ne 'ARRAY' or not @{$p->{-join_aliases}} );
 
-    my $as = shift @{$p->{-join_aliases}};
-
-    my $rel_info = $self->relationship_info( $pre );
-    $self->throw_exception( $self->source_name . " has no such relationship '$pre'" )
-      unless $rel_info;
+    # this shift() is critical - it is what allows prefetch => [ (foo) x 2 ] to work
+    my $src_alias = shift @{$p->{-join_aliases}};
+
+    # ordered [select => as] pairs
+    map { [
+      "${src_alias}.$_" => join ( '.',
+        @$pref_path,
+        $pre,
+        $_,
+      )
+    ] } $self->related_source($pre)->columns;
+  }
+}
 
-    my $as_prefix = ($alias =~ /^.*?\.(.+)$/ ? $1.'.' : '');
+sub _resolve_prefetch {
+  carp_unique(
+    'There is no good reason to call this internal deprecated method - '
+  . 'please open a ticket detailing your usage, so that a better plan can '
+  . 'be devised for your case. In either case _resolve_prefetch() is '
+  . 'deprecated in favor of _resolve_selection_from_prefetch(), which has '
+  . 'a greatly simplified arglist.'
+  );
 
-    return map { [ "${as}.$_", "${as_prefix}${pre}.$_", ] }
-      $self->related_source($pre)->columns;
-  }
+  $_[0]->_resolve_selection_from_prefetch( $_[1], $_[3] );
 }
 
+
 # Takes an arrayref of {as} dbic column aliases and the collapse and select
 # attributes from the same $rs (the selector requirement is a temporary
 # workaround... I hope), and returns a coderef capable of:
@@ -136,6 +150,9 @@ sub _mk_row_parser {
     });
   };
 
+  utf8::upgrade($src)
+    if DBIx::Class::_ENV_::STRESSTEST_UTF8_UPGRADE_GENERATED_COLLAPSER_SOURCE;
+
   return (
     $args->{eval} ? ( eval "sub $src" || die $@ ) : $src,
     $check_null_columns,
@@ -180,26 +197,12 @@ sub _resolve_collapse {
       is_single => ( $inf->{attrs}{accessor} && $inf->{attrs}{accessor} ne 'multi' ),
       is_inner => ( ( $inf->{attrs}{join_type} || '' ) !~ /^left/i),
       rsrc => $self->related_source($rel),
+      fk_map => $self->_resolve_relationship_condition(
+        rel_name => $rel,
+        self_alias => "\xFE", # irrelevant
+        foreign_alias => "\xFF", # irrelevant
+      )->{identity_map},
     };
-
-    # FIME - need to use _resolve_cond here instead
-    my $cond = $inf->{cond};
-
-    if (
-      ref $cond eq 'HASH'
-        and
-      keys %$cond
-        and
-      ! defined first { $_ !~ /^foreign\./ } (keys %$cond)
-        and
-      ! defined first { $_ !~ /^self\./ } (values %$cond)
-    ) {
-      for my $f (keys %$cond) {
-        my $s = $cond->{$f};
-        $_ =~ s/^ (?: foreign | self ) \.//x for ($f, $s);
-        $relinfo->{$rel}{fk_map}{$s} = $f;
-      }
-    }
   }
 
   # inject non-left fk-bridges from *INNER-JOINED* children (if any)
index d1c1e3b..a20d07c 100644 (file)
@@ -5,9 +5,9 @@ use strict;
 use warnings;
 
 use List::Util 'first';
-use B 'perlstring';
+use DBIx::Class::_Util 'perlstring';
 
-use constant HAS_DOR => ( $] < 5.010 ? 0 : 1 );
+use constant HAS_DOR => ( "$]" < 5.010 ? 0 : 1 );
 
 use base 'Exporter';
 our @EXPORT_OK = qw(
@@ -18,6 +18,10 @@ our @EXPORT_OK = qw(
 # working title - we are hoping to extract this eventually...
 our $null_branch_class = 'DBIx::ResultParser::RelatedNullBranch';
 
+sub __wrap_in_strictured_scope {
+  "  { use strict; use warnings; use warnings FATAL => 'uninitialized';\n$_[0]\n  }"
+}
+
 sub assemble_simple_parser {
   #my ($args) = @_;
 
@@ -30,12 +34,11 @@ sub assemble_simple_parser {
   #   the data structure, then to fetch the data do:
   # push @rows, dclone($row_data_struct) while ($sth->fetchrow);
   #
-  my $parser_src = sprintf('$_ = %s for @{$_[0]}', __visit_infmap_simple($_[0]) );
 
-  # change the quoted placeholders to unquoted alias-references
-  $parser_src =~ s/ \' \xFF__VALPOS__(\d+)__\xFF \' /"\$_->[$1]"/gex;
-
-  $parser_src = "  { use strict; use warnings FATAL => 'all';\n$parser_src\n  }";
+  __wrap_in_strictured_scope( sprintf
+    '$_ = %s for @{$_[0]}',
+    __visit_infmap_simple( $_[0] )
+  );
 }
 
 # the simple non-collapsing nested structure recursor
@@ -63,7 +66,7 @@ sub __visit_infmap_simple {
     if (keys %$my_cols) {
 
       my $branch_null_checks = join ' && ', map
-        { "( ! defined '\xFF__VALPOS__${_}__\xFF' )" }
+        { "( ! defined \$_->[$_] )" }
         sort { $a <=> $b } values %{$rel_cols->{$rel}}
       ;
 
@@ -110,30 +113,27 @@ sub __visit_infmap_simple {
 sub assemble_collapsing_parser {
   my $args = shift;
 
-  # it may get unset further down
-  my $no_rowid_container = $args->{prune_null_branches};
-
-  my ($top_node_key, $top_node_key_assembler);
+  my ($top_node_key, $top_node_key_assembler, $variant_idcols);
 
   if (scalar @{$args->{collapse_map}{-identifying_columns}}) {
     $top_node_key = join ('', map
-      { "{'\xFF__IDVALPOS__${_}__\xFF'}" }
+      { "{ \$cur_row_ids{$_} }" }
       @{$args->{collapse_map}{-identifying_columns}}
     );
   }
   elsif( my @variants = @{$args->{collapse_map}{-identifying_columns_variants}} ) {
 
     my @path_parts = map { sprintf
-      "( ( defined '\xFF__VALPOS__%d__\xFF' ) && (join qq(\xFF), '', %s, '') )",
+      "( ( defined \$cur_row_data->[%d] ) && (join qq(\xFF), '', %s, '') )",
       $_->[0],  # checking just first is enough - one ID defined, all defined
-      ( join ', ', map { "'\xFF__VALPOS__${_}__\xFF'" } @$_ ),
+      ( join ', ', map { ++$variant_idcols->{$_} and " \$cur_row_ids{$_} " } @$_ ),
     } @variants;
 
     my $virtual_column_idx = (scalar keys %{$args->{val_index}} ) + 1;
 
-    $top_node_key = "{'\xFF__IDVALPOS__${virtual_column_idx}__\xFF'}";
+    $top_node_key = "{ \$cur_row_ids{$virtual_column_idx} }";
 
-    $top_node_key_assembler = sprintf "'\xFF__IDVALPOS__%d__\xFF' = (%s);",
+    $top_node_key_assembler = sprintf "( \$cur_row_ids{%d} = (%s) ),",
       $virtual_column_idx,
       "\n" . join( "\n  or\n", @path_parts, qq{"\0\$rows_pos\0"} )
     ;
@@ -142,8 +142,6 @@ sub assemble_collapsing_parser {
       %{$args->{collapse_map}},
       -custom_node_key => $top_node_key,
     };
-
-    $no_rowid_container = 0;
   }
   else {
     die('Unexpected collapse map contents');
@@ -151,20 +149,30 @@ sub assemble_collapsing_parser {
 
   my ($data_assemblers, $stats) = __visit_infmap_collapse ($args);
 
-  my @idcol_args = $no_rowid_container ? ('', '') : (
-    ', %cur_row_ids', # only declare the variable if we'll use it
-    join ("\n", map { qq(\$cur_row_ids{$_} = ) . (
-      # in case we prune - we will never hit these undefs
-      $args->{prune_null_branches} ? qq(\$cur_row_data->[$_];)
-      : HAS_DOR                    ? qq(\$cur_row_data->[$_] // "\0NULL\xFF\$rows_pos\xFF$_\0";)
-      :                              qq(defined(\$cur_row_data->[$_]) ? \$cur_row_data->[$_] : "\0NULL\xFF\$rows_pos\xFF$_\0";)
-    ) } sort { $a <=> $b } keys %{ $stats->{idcols_seen} } ),
-  );
-
-  my $parser_src = sprintf (<<'EOS', @idcol_args, $top_node_key_assembler||'', $top_node_key, join( "\n", @{$data_assemblers||[]} ) );
+  # variants do not necessarily overlap with true idcols
+  my @row_ids = sort { $a <=> $b } keys %{ {
+    %{ $variant_idcols || {} },
+    %{ $stats->{idcols_seen} },
+  } };
+
+  my $row_id_defs = sprintf "( \@cur_row_ids{( %s )} = (\n%s\n ) ),",
+    join (', ', @row_ids ),
+    # in case we prune - we will never hit undefs/NULLs as pigeon-hole-criteria
+    ( $args->{prune_null_branches}
+      ? sprintf( '@{$cur_row_data}[( %s )]', join ', ', @row_ids )
+      : join (",\n", map {
+        my $quoted_null_val = qq("\0NULL\xFF\${rows_pos}\xFF${_}\0");
+        HAS_DOR
+          ? qq!( \$cur_row_data->[$_] // $quoted_null_val )!
+          : qq!( defined(\$cur_row_data->[$_]) ? \$cur_row_data->[$_] : $quoted_null_val )!
+      } @row_ids)
+    )
+  ;
+
+  my $parser_src = sprintf (<<'EOS', $row_id_defs, $top_node_key_assembler||'', $top_node_key, join( "\n", @{$data_assemblers||[]} ) );
 ### BEGIN LITERAL STRING EVAL
   my $rows_pos = 0;
-  my ($result_pos, @collapse_idx, $cur_row_data %1$s);
+  my ($result_pos, @collapse_idx, $cur_row_data, %%cur_row_ids );
 
   # this loop is a bit arcane - the rationale is that the passed in
   # $_[0] will either have only one row (->next) or will have all
@@ -173,32 +181,47 @@ sub assemble_collapsing_parser {
   # array, since the collapsed prefetch is smaller by definition.
   # At the end we cut the leftovers away and move on.
   while ($cur_row_data = (
-    ( $rows_pos >= 0 and $_[0][$rows_pos++] )
+    (
+      $rows_pos >= 0
+        and
+      (
+        $_[0][$rows_pos++]
+          or
+        # It may be tempting to drop the -1 and undef $rows_pos instead
+        # thus saving the >= comparison above as well
+        # However NULL-handlers and underdefined root markers both use
+        # $rows_pos as a last-resort-uniqueness marker (it either is
+        # monotonically increasing while we parse ->all, or is set at
+        # a steady -1 when we are dealing with a single root node). For
+        # the time being the complication of changing all callsites seems
+        # overkill, for what is going to be a very modest saving of ops
+        ( ($rows_pos = -1), undef )
+      )
+    )
       or
-    ( $_[1] and $rows_pos = -1 and $_[1]->() )
+    ( $_[1] and $_[1]->() )
   ) ) {
 
-    # this code exists only when we are using a cur_row_ids
-    # furthermore the undef checks may or may not be there
+    # the undef checks may or may not be there
     # depending on whether we prune or not
     #
     # due to left joins some of the ids may be NULL/undef, and
     # won't play well when used as hash lookups
     # we also need to differentiate NULLs on per-row/per-col basis
     # (otherwise folding of optional 1:1s will be greatly confused
-%2$s
+%1$s
 
     # in the case of an underdefined root - calculate the virtual id (otherwise no code at all)
-%3$s
+%2$s
 
     # if we were supplied a coderef - we are collapsing lazily (the set
     # is ordered properly)
     # as long as we have a result already and the next result is new we
     # return the pre-read data and bail
-$_[1] and $result_pos and ! $collapse_idx[0]%4$s and (unshift @{$_[2]}, $cur_row_data) and last;
+( $_[1] and $result_pos and ! $collapse_idx[0]%3$s and (unshift @{$_[2]}, $cur_row_data) and last ),
 
     # the rel assemblers
-%5$s
+%4$s
 
   }
 
@@ -206,16 +229,7 @@ $_[1] and $result_pos and ! $collapse_idx[0]%4$s and (unshift @{$_[2]}, $cur_row
 ### END LITERAL STRING EVAL
 EOS
 
-  # !!! note - different var than the one above
-  # change the quoted placeholders to unquoted alias-references
-  $parser_src =~ s/ \' \xFF__VALPOS__(\d+)__\xFF \' /"\$cur_row_data->[$1]"/gex;
-  $parser_src =~ s/
-    \' \xFF__IDVALPOS__(\d+)__\xFF \'
-  /
-    $no_rowid_container ? "\$cur_row_data->[$1]" : "\$cur_row_ids{$1}"
-  /gex;
-
-  $parser_src = "  { use strict; use warnings FATAL => 'all';\n$parser_src\n  }";
+  __wrap_in_strictured_scope($parser_src);
 }
 
 
@@ -241,14 +255,14 @@ sub __visit_infmap_collapse {
   }
 
   my $me_struct;
-  $me_struct = __result_struct_to_source($my_cols) if keys %$my_cols;
+  $me_struct = __result_struct_to_source($my_cols, 1) if keys %$my_cols;
 
   $me_struct = sprintf( '[ %s ]', $me_struct||'' )
     unless $args->{hri_style};
 
 
   my $node_key = $args->{collapse_map}->{-custom_node_key} || join ('', map
-    { "{'\xFF__IDVALPOS__${_}__\xFF'}" }
+    { "{ \$cur_row_ids{$_} }" }
     @{$args->{collapse_map}->{-identifying_columns}}
   );
   my $node_idx_slot = sprintf '$collapse_idx[%d]%s', $cur_node_idx, $node_key;
@@ -257,7 +271,7 @@ sub __visit_infmap_collapse {
   my @src;
 
   if ($cur_node_idx == 0) {
-    push @src, sprintf( '%s %s $_[0][$result_pos++] = %s;',
+    push @src, sprintf( '( %s %s $_[0][$result_pos++] = %s ),',
       $node_idx_slot,
       (HAS_DOR ? '//=' : '||='),
       $me_struct || '{}',
@@ -267,11 +281,11 @@ sub __visit_infmap_collapse {
     my $parent_attach_slot = sprintf( '$collapse_idx[%d]%s%s{%s}',
       @{$args}{qw/-parent_node_idx -parent_node_key/},
       $args->{hri_style} ? '' : '[1]',
-      perlstring($args->{-node_relname}),
+      perlstring($args->{-node_rel_name}),
     );
 
     if ($args->{collapse_map}->{-is_single}) {
-      push @src, sprintf ( '%s %s %s%s;',
+      push @src, sprintf ( '( %s %s %s%s ),',
         $parent_attach_slot,
         (HAS_DOR ? '//=' : '||='),
         $node_idx_slot,
@@ -279,7 +293,7 @@ sub __visit_infmap_collapse {
       );
     }
     else {
-      push @src, sprintf('(! %s) and push @{%s}, %s%s;',
+      push @src, sprintf('( (! %s) and push @{%s}, %s%s ),',
         $node_idx_slot,
         $parent_attach_slot,
         $node_idx_slot,
@@ -300,7 +314,7 @@ sub __visit_infmap_collapse {
       collapse_map => $relinfo,
       -parent_node_idx => $cur_node_idx,
       -parent_node_key => $node_key,
-      -node_relname => $rel,
+      -node_rel_name => $rel,
     });
 
     my $rel_src_pos = $#src + 1;
@@ -318,8 +332,8 @@ sub __visit_infmap_collapse {
       if ($args->{prune_null_branches}) {
 
         # start of wrap of the entire chain in a conditional
-        splice @src, $rel_src_pos, 0, sprintf "( ! defined %s )\n  ? %s%s{%s} = %s\n  : do {",
-          "'\xFF__VALPOS__${first_distinct_child_idcol}__\xFF'",
+        splice @src, $rel_src_pos, 0, sprintf "( ( ! defined %s )\n  ? %s%s{%s} = %s\n  : do {",
+          "\$cur_row_data->[$first_distinct_child_idcol]",
           $node_idx_slot,
           $args->{hri_style} ? '' : '[1]',
           perlstring($rel),
@@ -327,12 +341,12 @@ sub __visit_infmap_collapse {
         ;
 
         # end of wrap
-        push @src, '};'
+        push @src, '} ),'
       }
       else {
 
-        splice @src, $rel_src_pos + 1, 0, sprintf ( '(defined %s) or bless (%s[1]{%s}, %s);',
-          "'\xFF__VALPOS__${first_distinct_child_idcol}__\xFF'",
+        splice @src, $rel_src_pos + 1, 0, sprintf ( '( (defined %s) or bless (%s[1]{%s}, %s) ),',
+          "\$cur_row_data->[$first_distinct_child_idcol]",
           $node_idx_slot,
           perlstring($rel),
           perlstring($null_branch_class),
@@ -353,10 +367,19 @@ sub __visit_infmap_collapse {
 }
 
 sub __result_struct_to_source {
-  sprintf( '{ %s }', join (', ', map
-    { sprintf "%s => '\xFF__VALPOS__%d__\xFF'", perlstring($_), $_[0]{$_} }
-    sort keys %{$_[0]}
-  ));
+  my ($data, $is_collapsing) = @_;
+
+  sprintf( '{ %s }',
+    join (', ', map {
+      sprintf ( "%s => %s",
+        perlstring($_),
+        $is_collapsing
+          ? "\$cur_row_data->[$data->{$_}]"
+          : "\$_->[ $data->{$_} ]"
+      )
+    } sort keys %{$data}
+    )
+  );
 }
 
 1;
index 7c8dbe7..ac7d308 100644 (file)
@@ -28,15 +28,17 @@ Returns the FROM entry for the table (i.e. the table name)
 
 sub from { shift->name; }
 
-1;
-
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
+1;
index 232cc2f..4694c87 100644 (file)
@@ -66,12 +66,12 @@ case replaces the view name in a FROM clause in a subselect.
 Having created the MyApp::Schema::Year2000CDs schema as shown in the SYNOPSIS
 above, you can then:
 
-  $2000_cds = $schema->resultset('Year2000CDs')
-                     ->search()
-                     ->all();
-  $count    = $schema->resultset('Year2000CDs')
-                     ->search()
-                     ->count();
+  $y2000_cds = $schema->resultset('Year2000CDs')
+                      ->search()
+                      ->all();
+  $count     = $schema->resultset('Year2000CDs')
+                      ->search()
+                      ->count();
 
 If you modified the schema to include a placeholder
 
@@ -85,12 +85,12 @@ and ensuring you have is_virtual set to true:
 
 You could now say:
 
-  $2001_cds = $schema->resultset('Year2000CDs')
-                     ->search({}, { bind => [2001] })
-                     ->all();
-  $count    = $schema->resultset('Year2000CDs')
-                     ->search({}, { bind => [2001] })
-                     ->count();
+  $y2001_cds = $schema->resultset('Year2000CDs')
+                      ->search({}, { bind => [2001] })
+                      ->all();
+  $count     = $schema->resultset('Year2000CDs')
+                      ->search({}, { bind => [2001] })
+                      ->count();
 
 =head1 SQL EXAMPLES
 
@@ -171,15 +171,17 @@ sub new {
     return $new;
 }
 
-1;
-
-=head1 AUTHORS
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
+1;
index 733db83..b9b54bf 100644 (file)
@@ -5,7 +5,7 @@ use warnings;
 
 use base qw/DBIx::Class/;
 
-use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 use namespace::clean;
 
 use overload
@@ -68,7 +68,7 @@ sub resolve {
     # vague error message as this is never supposed to happen
     "Unable to resolve moniker '%s' - please contact the dev team at %s",
     $_[0]->source_moniker,
-    'http://search.cpan.org/dist/DBIx-Class/lib/DBIx/Class.pm#GETTING_HELP/SUPPORT',
+    DBIx::Class::_ENV_::HELP_URL,
   ), 'full_stacktrace');
 }
 
@@ -112,7 +112,7 @@ sub STORABLE_thaw {
   }
   elsif( my $rs = $from_class->result_source_instance ) {
     # in the off-chance we are using CDBI-compat and have leaked $schema already
-    if( my $s = try { $rs->schema } ) {
+    if( my $s = dbic_internal_try { $rs->schema } ) {
       $self->schema( $s );
     }
     else {
@@ -128,9 +128,16 @@ sub STORABLE_thaw {
   }
 }
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-Ash Berlin C<< <ash@cpan.org> >>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index c3bef15..1e1f307 100644 (file)
@@ -4,9 +4,10 @@ package # hide from PAUSE
 use strict;
 use warnings;
 
-use base qw/DBIx::Class/;
-use Scalar::Util qw/blessed/;
-use Sub::Name qw/subname/;
+use base 'DBIx::Class';
+
+use Scalar::Util 'blessed';
+use DBIx::Class::_Util 'quote_sub';
 use namespace::clean;
 
 __PACKAGE__->mk_group_accessors('inherited_ro_instance' => 'source_name');
@@ -80,10 +81,11 @@ for my $method_to_proxy (qw/
   relationship_info
   has_relationship
 /) {
-  no strict qw/refs/;
-  *{__PACKAGE__."::$method_to_proxy"} = subname $method_to_proxy => sub {
-    shift->result_source_instance->$method_to_proxy (@_);
-  };
+  quote_sub __PACKAGE__."::$method_to_proxy", sprintf( <<'EOC', $method_to_proxy );
+    DBIx::Class::_ENV_::ASSERT_NO_INTERNAL_INDIRECT_CALLS and DBIx::Class::_Util::fail_on_internal_call;
+    shift->result_source_instance->%s (@_);
+EOC
+
 }
 
 1;
index fe72d4d..647a408 100644 (file)
@@ -110,16 +110,12 @@ sub table {
 
 Gets or sets the table class used for construction and validation.
 
-=cut
-
 =head2 has_column
 
   if ($obj->has_column($col)) { ... }
 
 Returns 1 if the class has a column of this name, 0 otherwise.
 
-=cut
-
 =head2 column_info
 
   my $info = $obj->column_info($col);
@@ -128,23 +124,23 @@ Returns the column metadata hashref for a column. For a description of
 the various types of column data in this hashref, see
 L<DBIx::Class::ResultSource/add_column>
 
-=cut
-
 =head2 columns
 
   my @column_names = $obj->columns;
 
-=cut
+=head1 FURTHER QUESTIONS?
 
-1;
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 COPYRIGHT AND LICENSE
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
-=head1 LICENSE
+=cut
 
-You may distribute this code under the same terms as Perl itself.
+1;
 
-=cut
 
index 000498a..daf5885 100644 (file)
@@ -7,8 +7,9 @@ use base qw/DBIx::Class/;
 
 use Scalar::Util 'blessed';
 use List::Util 'first';
-use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 use DBIx::Class::Carp;
+use SQL::Abstract qw( is_literal_value is_plain_value );
 
 ###
 ### Internal method
@@ -51,7 +52,7 @@ All "Row objects" derived from a Schema-attached L<DBIx::Class::ResultSet>
 object (such as a typical C<< L<search|DBIx::Class::ResultSet/search>->
 L<next|DBIx::Class::ResultSet/next> >> call) are actually Result
 instances, based on your application's
-L<Result class|DBIx::Class::Manual::Glossary/Result_class>.
+L<Result Class|DBIx::Class::Manual::Glossary/Result Class>.
 
 L<DBIx::Class::Row> implements most of the row-based communication with the
 underlying storage, but a Result class B<should not inherit from it directly>.
@@ -125,26 +126,26 @@ with NULL as the default, and save yourself a SELECT.
 ## tests!
 
 sub __new_related_find_or_new_helper {
-  my ($self, $relname, $values) = @_;
+  my ($self, $rel_name, $values) = @_;
 
   my $rsrc = $self->result_source;
 
   # create a mock-object so all new/set_column component overrides will run:
-  my $rel_rs = $rsrc->related_source($relname)->resultset;
+  my $rel_rs = $rsrc->related_source($rel_name)->resultset;
   my $new_rel_obj = $rel_rs->new_result($values);
   my $proc_data = { $new_rel_obj->get_columns };
 
-  if ($self->__their_pk_needs_us($relname)) {
-    MULTICREATE_DEBUG and print STDERR "MC $self constructing $relname via new_result\n";
+  if ($self->__their_pk_needs_us($rel_name)) {
+    MULTICREATE_DEBUG and print STDERR "MC $self constructing $rel_name via new_result\n";
     return $new_rel_obj;
   }
-  elsif ($rsrc->_pk_depends_on($relname, $proc_data )) {
+  elsif ($rsrc->_pk_depends_on($rel_name, $proc_data )) {
     if (! keys %$proc_data) {
       # there is nothing to search for - blind create
-      MULTICREATE_DEBUG and print STDERR "MC $self constructing default-insert $relname\n";
+      MULTICREATE_DEBUG and print STDERR "MC $self constructing default-insert $rel_name\n";
     }
     else {
-      MULTICREATE_DEBUG and print STDERR "MC $self constructing $relname via find_or_new\n";
+      MULTICREATE_DEBUG and print STDERR "MC $self constructing $rel_name via find_or_new\n";
       # this is not *really* find or new, as we don't want to double-new the
       # data (thus potentially double encoding or whatever)
       my $exists = $rel_rs->find ($proc_data);
@@ -155,17 +156,17 @@ sub __new_related_find_or_new_helper {
   else {
     my $us = $rsrc->source_name;
     $self->throw_exception (
-      "Unable to determine relationship '$relname' direction from '$us', "
-    . "possibly due to a missing reverse-relationship on '$relname' to '$us'."
+      "Unable to determine relationship '$rel_name' direction from '$us', "
+    . "possibly due to a missing reverse-relationship on '$rel_name' to '$us'."
     );
   }
 }
 
 sub __their_pk_needs_us { # this should maybe be in resultsource.
-  my ($self, $relname) = @_;
+  my ($self, $rel_name) = @_;
   my $rsrc = $self->result_source;
-  my $reverse = $rsrc->reverse_relationship_info($relname);
-  my $rel_source = $rsrc->related_source($relname);
+  my $reverse = $rsrc->reverse_relationship_info($rel_name);
+  my $rel_source = $rsrc->related_source($rel_name);
   my $us = { $self->get_columns };
   foreach my $key (keys %$reverse) {
     # if their primary key depends on us, then we have to
@@ -199,7 +200,7 @@ sub new {
     my ($related,$inflated);
 
     foreach my $key (keys %$attrs) {
-      if (ref $attrs->{$key}) {
+      if (ref $attrs->{$key} and ! is_literal_value($attrs->{$key}) ) {
         ## Can we extract this lot to use with update(_or .. ) ?
         $new->throw_exception("Can't do multi-create without result source")
           unless $rsrc;
@@ -256,14 +257,16 @@ sub new {
           }
           $inflated->{$key} = $rel_obj;
           next;
-        } elsif ($class->has_column($key)
-            && $class->column_info($key)->{_inflate_info}) {
+        }
+        elsif (
+          $rsrc->has_column($key)
+            and
+          $rsrc->column_info($key)->{_inflate_info}
+        ) {
           $inflated->{$key} = $attrs->{$key};
           next;
         }
       }
-      $new->throw_exception("No such column '$key' on $class")
-        unless $class->has_column($key);
       $new->store_column($key => $attrs->{$key});
     }
 
@@ -351,27 +354,27 @@ sub insert {
 
   # insert what needs to be inserted before us
   my %pre_insert;
-  for my $relname (keys %related_stuff) {
-    my $rel_obj = $related_stuff{$relname};
+  for my $rel_name (keys %related_stuff) {
+    my $rel_obj = $related_stuff{$rel_name};
 
-    if (! $self->{_rel_in_storage}{$relname}) {
+    if (! $self->{_rel_in_storage}{$rel_name}) {
       next unless (blessed $rel_obj && $rel_obj->isa('DBIx::Class::Row'));
 
       next unless $rsrc->_pk_depends_on(
-                    $relname, { $rel_obj->get_columns }
+                    $rel_name, { $rel_obj->get_columns }
                   );
 
       # The guard will save us if we blow out of this scope via die
       $rollback_guard ||= $storage->txn_scope_guard;
 
-      MULTICREATE_DEBUG and print STDERR "MC $self pre-reconstructing $relname $rel_obj\n";
+      MULTICREATE_DEBUG and print STDERR "MC $self pre-reconstructing $rel_name $rel_obj\n";
 
       my $them = { %{$rel_obj->{_relationship_data} || {} }, $rel_obj->get_columns };
       my $existing;
 
       # if there are no keys - nothing to search for
       if (keys %$them and $existing = $self->result_source
-                                           ->related_source($relname)
+                                           ->related_source($rel_name)
                                            ->resultset
                                            ->find($them)
       ) {
@@ -381,11 +384,11 @@ sub insert {
         $rel_obj->insert;
       }
 
-      $self->{_rel_in_storage}{$relname} = 1;
+      $self->{_rel_in_storage}{$rel_name} = 1;
     }
 
-    $self->set_from_related($relname, $rel_obj);
-    delete $related_stuff{$relname};
+    $self->set_from_related($rel_name, $rel_obj);
+    delete $related_stuff{$rel_name};
   }
 
   # start a transaction here if not started yet and there is more stuff
@@ -426,25 +429,25 @@ sub insert {
   $self->{_dirty_columns} = {};
   $self->{related_resultsets} = {};
 
-  foreach my $relname (keys %related_stuff) {
-    next unless $rsrc->has_relationship ($relname);
+  foreach my $rel_name (keys %related_stuff) {
+    next unless $rsrc->has_relationship ($rel_name);
 
-    my @cands = ref $related_stuff{$relname} eq 'ARRAY'
-      ? @{$related_stuff{$relname}}
-      : $related_stuff{$relname}
+    my @cands = ref $related_stuff{$rel_name} eq 'ARRAY'
+      ? @{$related_stuff{$rel_name}}
+      : $related_stuff{$rel_name}
     ;
 
     if (@cands && blessed $cands[0] && $cands[0]->isa('DBIx::Class::Row')
     ) {
-      my $reverse = $rsrc->reverse_relationship_info($relname);
+      my $reverse = $rsrc->reverse_relationship_info($rel_name);
       foreach my $obj (@cands) {
         $obj->set_from_related($_, $self) for keys %$reverse;
-        if ($self->__their_pk_needs_us($relname)) {
-          if (exists $self->{_ignore_at_insert}{$relname}) {
-            MULTICREATE_DEBUG and print STDERR "MC $self skipping post-insert on $relname\n";
+        if ($self->__their_pk_needs_us($rel_name)) {
+          if (exists $self->{_ignore_at_insert}{$rel_name}) {
+            MULTICREATE_DEBUG and print STDERR "MC $self skipping post-insert on $rel_name\n";
           }
           else {
-            MULTICREATE_DEBUG and print STDERR "MC $self inserting $relname $obj\n";
+            MULTICREATE_DEBUG and print STDERR "MC $self inserting $rel_name $obj\n";
             $obj->insert;
           }
         } else {
@@ -477,8 +480,8 @@ sub insert {
 
 Indicates whether the object exists as a row in the database or
 not. This is set to true when L<DBIx::Class::ResultSet/find>,
-L<DBIx::Class::ResultSet/create> or L<DBIx::Class::ResultSet/insert>
-are used.
+L<DBIx::Class::ResultSet/create> or L<DBIx::Class::Row/insert>
+are invoked.
 
 Creating a result object using L<DBIx::Class::ResultSet/new_result>, or
 calling L</delete> on one, sets it to false.
@@ -618,7 +621,7 @@ sub delete {
     $self->in_storage(0);
   }
   else {
-    my $rsrc = try { $self->result_source_instance }
+    my $rsrc = dbic_internal_try { $self->result_source_instance }
       or $self->throw_exception("Can't do class delete without a ResultSource instance");
 
     my $attrs = @_ > 1 && ref $_[$#_] eq 'HASH' ? { %{pop(@_)} } : {};
@@ -661,12 +664,20 @@ To retrieve all loaded column values as a hash, use L</get_columns>.
 sub get_column {
   my ($self, $column) = @_;
   $self->throw_exception( "Can't fetch data as class method" ) unless ref $self;
-  return $self->{_column_data}{$column} if exists $self->{_column_data}{$column};
+
+  return $self->{_column_data}{$column}
+    if exists $self->{_column_data}{$column};
+
   if (exists $self->{_inflated_column}{$column}) {
-    return $self->store_column($column,
-      $self->_deflated_column($column, $self->{_inflated_column}{$column}));
+    # deflate+return cycle
+    return $self->store_column($column, $self->_deflated_column(
+      $column, $self->{_inflated_column}{$column}
+    ));
   }
-  $self->throw_exception( "No such column '${column}'" ) unless $self->has_column($column);
+
+  $self->throw_exception( "No such column '${column}' on " . ref $self )
+    unless $self->result_source->has_column($column);
+
   return undef;
 }
 
@@ -692,8 +703,12 @@ database (or set locally).
 sub has_column_loaded {
   my ($self, $column) = @_;
   $self->throw_exception( "Can't call has_column data as class method" ) unless ref $self;
-  return 1 if exists $self->{_inflated_column}{$column};
-  return exists $self->{_column_data}{$column};
+
+  return (
+    exists $self->{_inflated_column}{$column}
+      or
+    exists $self->{_column_data}{$column}
+  ) ? 1 : 0;
 }
 
 =head2 get_columns
@@ -718,6 +733,7 @@ See L</get_inflated_columns> to get the inflated values.
 sub get_columns {
   my $self = shift;
   if (exists $self->{_inflated_column}) {
+    # deflate cycle for each inflation, including filter rels
     foreach my $col (keys %{$self->{_inflated_column}}) {
       unless (exists $self->{_column_data}{$col}) {
 
@@ -787,8 +803,8 @@ really changed.
 sub make_column_dirty {
   my ($self, $column) = @_;
 
-  $self->throw_exception( "No such column '${column}'" )
-    unless exists $self->{_column_data}{$column} || $self->has_column($column);
+  $self->throw_exception( "No such column '${column}' on " . ref $self )
+    unless exists $self->{_column_data}{$column} || $self->result_source->has_column($column);
 
   # the entire clean/dirty code relies on exists, not on true/false
   return 1 if exists $self->{_dirty_columns}{$column};
@@ -830,9 +846,9 @@ See L<DBIx::Class::InflateColumn> for how to setup inflation.
 sub get_inflated_columns {
   my $self = shift;
 
-  my $loaded_colinfo = $self->columns_info ([
-    grep { $self->has_column_loaded($_) } $self->columns
-  ]);
+  my $loaded_colinfo = $self->result_source->columns_info;
+  $self->has_column_loaded($_) or delete $loaded_colinfo->{$_}
+    for keys %$loaded_colinfo;
 
   my %cols_to_return = ( %{$self->{_column_data}}, %$loaded_colinfo );
 
@@ -874,14 +890,17 @@ sub get_inflated_columns {
 }
 
 sub _is_column_numeric {
-   my ($self, $column) = @_;
-    my $colinfo = $self->column_info ($column);
+    my ($self, $column) = @_;
+
+    return undef unless $self->result_source->has_column($column);
+
+    my $colinfo = $self->result_source->column_info ($column);
 
     # cache for speed (the object may *not* have a resultsource instance)
     if (
       ! defined $colinfo->{is_numeric}
         and
-      my $storage = try { $self->result_source->schema->storage }
+      my $storage = dbic_internal_try { $self->result_source->schema->storage }
     ) {
       $colinfo->{is_numeric} =
         $storage->is_datatype_numeric ($colinfo->{data_type})
@@ -919,17 +938,17 @@ sub set_column {
   my ($self, $column, $new_value) = @_;
 
   my $had_value = $self->has_column_loaded($column);
-  my ($old_value, $in_storage) = ($self->get_column($column), $self->in_storage)
-    if $had_value;
+  my $old_value = $self->get_column($column);
 
   $new_value = $self->store_column($column, $new_value);
 
   my $dirty =
     $self->{_dirty_columns}{$column}
       ||
-    $in_storage # no point tracking dirtyness on uninserted data
+    ( $self->in_storage # no point tracking dirtyness on uninserted data
       ? ! $self->_eq_column_values ($column, $old_value, $new_value)
       : 1
+    )
   ;
 
   if ($dirty) {
@@ -940,20 +959,20 @@ sub set_column {
     #
     # FIXME - this is a quick *largely incorrect* hack, pending a more
     # serious rework during the merge of single and filter rels
-    my $relnames = $self->result_source->{_relationships};
-    for my $relname (keys %$relnames) {
+    my $rel_names = $self->result_source->{_relationships};
+    for my $rel_name (keys %$rel_names) {
 
-      my $acc = $relnames->{$relname}{attrs}{accessor} || '';
+      my $acc = $rel_names->{$rel_name}{attrs}{accessor} || '';
 
-      if ( $acc eq 'single' and $relnames->{$relname}{attrs}{fk_columns}{$column} ) {
-        delete $self->{related_resultsets}{$relname};
-        delete $self->{_relationship_data}{$relname};
-        #delete $self->{_inflated_column}{$relname};
+      if ( $acc eq 'single' and $rel_names->{$rel_name}{attrs}{fk_columns}{$column} ) {
+        delete $self->{related_resultsets}{$rel_name};
+        delete $self->{_relationship_data}{$rel_name};
+        #delete $self->{_inflated_column}{$rel_name};
       }
-      elsif ( $acc eq 'filter' and $relname eq $column) {
-        delete $self->{related_resultsets}{$relname};
-        #delete $self->{_relationship_data}{$relname};
-        delete $self->{_inflated_column}{$relname};
+      elsif ( $acc eq 'filter' and $rel_name eq $column) {
+        delete $self->{related_resultsets}{$rel_name};
+        #delete $self->{_relationship_data}{$rel_name};
+        delete $self->{_inflated_column}{$rel_name};
       }
     }
 
@@ -962,7 +981,7 @@ sub set_column {
       $had_value
         and
       # no storage - no storage-value
-      $in_storage
+      $self->in_storage
         and
       # no value already stored (multiple changes before commit to storage)
       ! exists $self->{_column_data_in_storage}{$column}
@@ -985,6 +1004,13 @@ sub _eq_column_values {
   elsif (not defined $old) {  # both undef
     return 1;
   }
+  elsif (
+    is_literal_value $old
+      or
+    is_literal_value $new
+  ) {
+    return 0;
+  }
   elsif ($old eq $new) {
     return 1;
   }
@@ -1000,7 +1026,7 @@ sub _eq_column_values {
 # value tracked between column changes and commitment to storage
 sub _track_storage_value {
   my ($self, $col) = @_;
-  return defined first { $col eq $_ } ($self->primary_columns);
+  return defined first { $col eq $_ } ($self->result_source->primary_columns);
 }
 
 =head2 set_columns
@@ -1029,7 +1055,7 @@ sub set_columns {
 
 =head2 set_inflated_columns
 
-  $result->set_inflated_columns({ $col => $val, $relname => $obj, ... });
+  $result->set_inflated_columns({ $col => $val, $rel_name => $obj, ... });
 
 =over
 
@@ -1062,10 +1088,13 @@ See also L<DBIx::Class::Relationship::Base/set_from_related>.
 
 sub set_inflated_columns {
   my ( $self, $upd ) = @_;
+  my $rsrc;
   foreach my $key (keys %$upd) {
     if (ref $upd->{$key}) {
-      my $info = $self->relationship_info($key);
+      $rsrc ||= $self->result_source;
+      my $info = $rsrc->relationship_info($key);
       my $acc_type = $info->{attrs}{accessor} || '';
+
       if ($acc_type eq 'single') {
         my $rel_obj = delete $upd->{$key};
         $self->set_from_related($key => $rel_obj);
@@ -1076,7 +1105,11 @@ sub set_inflated_columns {
           "Recursive update is not supported over relationships of type '$acc_type' ($key)"
         );
       }
-      elsif ($self->has_column($key) && exists $self->column_info($key)->{_inflate_info}) {
+      elsif (
+        $rsrc->has_column($key)
+          and
+        exists $rsrc->column_info($key)->{_inflate_info}
+      ) {
         $self->set_inflated_column($key, delete $upd->{$key});
       }
     }
@@ -1114,43 +1147,48 @@ is set by default on C<has_many> relationships and unset on all others.
 sub copy {
   my ($self, $changes) = @_;
   $changes ||= {};
-  my $col_data = { %{$self->{_column_data}} };
+  my $col_data = { $self->get_columns };
+
+  my $rsrc = $self->result_source;
 
-  my $colinfo = $self->columns_info([ keys %$col_data ]);
+  my $colinfo = $rsrc->columns_info;
   foreach my $col (keys %$col_data) {
     delete $col_data->{$col}
-      if $colinfo->{$col}{is_auto_increment};
+      if ( ! $colinfo->{$col} or $colinfo->{$col}{is_auto_increment} );
   }
 
   my $new = { _column_data => $col_data };
   bless $new, ref $self;
 
-  $new->result_source($self->result_source);
+  $new->result_source($rsrc);
   $new->set_inflated_columns($changes);
   $new->insert;
 
   # Its possible we'll have 2 relations to the same Source. We need to make
   # sure we don't try to insert the same row twice else we'll violate unique
   # constraints
-  my $relnames_copied = {};
+  my $rel_names_copied = {};
 
-  foreach my $relname ($self->result_source->relationships) {
-    my $rel_info = $self->result_source->relationship_info($relname);
+  foreach my $rel_name ($rsrc->relationships) {
+    my $rel_info = $rsrc->relationship_info($rel_name);
 
     next unless $rel_info->{attrs}{cascade_copy};
 
-    my $resolved = $self->result_source->_resolve_condition(
-      $rel_info->{cond}, $relname, $new, $relname
-    );
+    my $foreign_vals;
+    my $copied = $rel_names_copied->{ $rel_info->{source} } ||= {};
 
-    my $copied = $relnames_copied->{ $rel_info->{source} } ||= {};
-    foreach my $related ($self->search_related($relname)->all) {
-      my $id_str = join("\0", $related->id);
-      next if $copied->{$id_str};
-      $copied->{$id_str} = 1;
-      my $rel_copy = $related->copy($resolved);
-    }
+    $copied->{$_->ID}++ or $_->copy(
+
+      $foreign_vals ||= $rsrc->_resolve_relationship_condition(
+        infer_values_based_on => {},
+        rel_name => $rel_name,
+        self_result_object => $new,
 
+        self_alias => "\xFE", # irrelevant
+        foreign_alias => "\xFF", # irrelevant,
+      )->{inferred_values}
+
+    ) for $self->search_related($rel_name)->all;
   }
   return $new;
 }
@@ -1178,11 +1216,22 @@ extend this method to catch all data setting methods.
 
 sub store_column {
   my ($self, $column, $value) = @_;
-  $self->throw_exception( "No such column '${column}'" )
-    unless exists $self->{_column_data}{$column} || $self->has_column($column);
+  $self->throw_exception( "No such column '${column}' on " . ref $self )
+    unless exists $self->{_column_data}{$column} || $self->result_source->has_column($column);
   $self->throw_exception( "set_column called for ${column} without value" )
     if @_ < 3;
-  return $self->{_column_data}{$column} = $value;
+
+  return $self->{_column_data}{$column} = $value
+    unless length ref $value and my $vref = is_plain_value( $value );
+
+  # if we are dealing with a value/ref - there are a couple possibilities
+  # unpack the underlying piece of data and stringify all objects explicitly
+  # ( to accomodate { -value => ... } and guard against overloaded objects
+  # with defined stringification AND fallback => 0 (ugh!)
+  $self->{_column_data}{$column} = defined blessed $$vref
+    ? "$$vref"
+    : $$vref
+  ;
 }
 
 =head2 inflate_result
@@ -1220,61 +1269,59 @@ sub inflate_result {
   ;
 
   if ($prefetch) {
-    for my $relname ( keys %$prefetch ) {
+    for my $rel_name ( keys %$prefetch ) {
 
-      my $relinfo = $rsrc->relationship_info($relname) or do {
+      my $relinfo = $rsrc->relationship_info($rel_name) or do {
         my $err = sprintf
           "Inflation into non-existent relationship '%s' of '%s' requested",
-          $relname,
+          $rel_name,
           $rsrc->source_name,
         ;
-        if (my ($colname) = sort { length($a) <=> length ($b) } keys %{$prefetch->{$relname}[0] || {}} ) {
+        if (my ($colname) = sort { length($a) <=> length ($b) } keys %{$prefetch->{$rel_name}[0] || {}} ) {
           $err .= sprintf ", check the inflation specification (columns/as) ending in '...%s.%s'",
-          $relname,
+          $rel_name,
           $colname,
         }
 
         $rsrc->throw_exception($err);
       };
 
-      $class->throw_exception("No accessor type declared for prefetched relationship '$relname'")
+      $class->throw_exception("No accessor type declared for prefetched relationship '$rel_name'")
         unless $relinfo->{attrs}{accessor};
 
+      my $rel_rs = $new->related_resultset($rel_name);
+
       my @rel_objects;
       if (
-        $prefetch->{$relname}
-          and
-        @{$prefetch->{$relname}}
+        @{ $prefetch->{$rel_name} || [] }
           and
-        ref($prefetch->{$relname}) ne $DBIx::Class::ResultSource::RowParser::Util::null_branch_class
+        ref($prefetch->{$rel_name}) ne $DBIx::Class::ResultSource::RowParser::Util::null_branch_class
       ) {
 
-        my $rel_rs = $new->related_resultset($relname);
-
-        if (ref $prefetch->{$relname}[0] eq 'ARRAY') {
+        if (ref $prefetch->{$rel_name}[0] eq 'ARRAY') {
           my $rel_rsrc = $rel_rs->result_source;
           my $rel_class = $rel_rs->result_class;
           my $rel_inflator = $rel_class->can('inflate_result');
           @rel_objects = map
             { $rel_class->$rel_inflator ( $rel_rsrc, @$_ ) }
-            @{$prefetch->{$relname}}
+            @{$prefetch->{$rel_name}}
           ;
         }
         else {
           @rel_objects = $rel_rs->result_class->inflate_result(
-            $rel_rs->result_source, @{$prefetch->{$relname}}
+            $rel_rs->result_source, @{$prefetch->{$rel_name}}
           );
         }
       }
 
       if ($relinfo->{attrs}{accessor} eq 'single') {
-        $new->{_relationship_data}{$relname} = $rel_objects[0];
+        $new->{_relationship_data}{$rel_name} = $rel_objects[0];
       }
       elsif ($relinfo->{attrs}{accessor} eq 'filter') {
-        $new->{_inflated_column}{$relname} = $rel_objects[0];
+        $new->{_inflated_column}{$rel_name} = $rel_objects[0];
       }
 
-      $new->related_resultset($relname)->set_cache(\@rel_objects);
+      $rel_rs->set_cache(\@rel_objects);
     }
   }
 
@@ -1294,7 +1341,7 @@ sub inflate_result {
 
 =back
 
-L</Update>s the object if it's already in the database, according to
+L</update>s the object if it's already in the database, according to
 L</in_storage>, else L</insert>s it.
 
 =head2 insert_or_update
@@ -1381,11 +1428,10 @@ sub result_source {
 
     # note this is a || not a ||=, the difference is important
     : $_[0]->{_result_source} || do {
-        my $class = ref $_[0];
         $_[0]->can('result_source_instance')
           ? $_[0]->result_source_instance
           : $_[0]->throw_exception(
-            "No result source instance registered for $class, did you forget to call $class->table(...) ?"
+            "No result source instance registered for @{[ ref $_[0] ]}, did you forget to call @{[ ref $_[0] ]}->table(...) ?"
           )
       }
   ;
@@ -1492,11 +1538,12 @@ $attrs, if supplied, is expected to be a hashref of attributes suitable for pass
 second argument to C<< $resultset->search($cond, $attrs) >>;
 
 Note: If you are using L<DBIx::Class::Storage::DBI::Replicated> as your
-storage, please kept in mind that if you L</discard_changes> on a row that you
-just updated or created, you should wrap the entire bit inside a transaction.
-Otherwise you run the risk that you insert or update to the master database
-but read from a replicant database that has not yet been updated from the
-master.  This will result in unexpected results.
+storage, a default of
+L<< C<< { force_pool => 'master' } >>
+|DBIx::Class::Storage::DBI::Replicated/SYNOPSIS >>  is automatically set for
+you. Prior to C<< DBIx::Class 0.08109 >> (before 2010) one would have been
+required to explicitly wrap the entire operation in a transaction to guarantee
+that up-to-date results are read from the master database.
 
 =cut
 
@@ -1533,8 +1580,12 @@ See L<DBIx::Class::Schema/throw_exception>.
 sub throw_exception {
   my $self=shift;
 
-  if (ref $self && ref $self->result_source ) {
-    $self->result_source->throw_exception(@_)
+  if (
+    ref $self
+      and
+    my $rsrc = dbic_internal_try { $self->result_source_instance }
+  ) {
+    $rsrc->throw_exception(@_)
   }
   else {
     DBIx::Class::Exception->throw(@_);
@@ -1556,13 +1607,16 @@ sub throw_exception {
 Returns the primary key(s) for a row. Can't be called as a class method.
 Actually implemented in L<DBIx::Class::PK>
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index e863a0f..31e39a7 100644 (file)
@@ -9,13 +9,13 @@ DBIx::Class::SQLMaker - An SQL::Abstract-based SQL maker class
 
 =head1 DESCRIPTION
 
-This module is a subclass of L<SQL::Abstract> and includes a number of
-DBIC-specific workarounds, not yet suitable for inclusion into the
+This module is currently a subclass of L<SQL::Abstract> and includes a number of
+DBIC-specific extensions/workarounds, not suitable for inclusion into the
 L<SQL::Abstract> core. It also provides all (and more than) the functionality
 of L<SQL::Abstract::Limit>, see L<DBIx::Class::SQLMaker::LimitDialects> for
 more info.
 
-Currently the enhancements to L<SQL::Abstract> are:
+Currently the enhancements over L<SQL::Abstract> are:
 
 =over
 
@@ -25,10 +25,102 @@ Currently the enhancements to L<SQL::Abstract> are:
 
 =item * C<GROUP BY>/C<HAVING> support (via extensions to the order_by parameter)
 
+=item * A rudimentary multicolumn IN operator
+
 =item * Support of C<...FOR UPDATE> type of select statement modifiers
 
 =back
 
+=head1 ROADMAP
+
+Some maintainer musings on the current state of SQL generation within DBIC as
+of Oct 2015
+
+=head2 Folding of most (or all) of L<SQL::Abstract (SQLA)|SQL::Abstract> into DBIC
+
+The rise of complex prefetch use, and the general streamlining of result
+parsing within DBIC ended up pushing the actual SQL generation to the forefront
+of many casual performance profiles. While the idea behind SQLA's API is sound,
+the actual implementation is terribly inefficient (once again bumping into the
+ridiculously high overhead of perl function calls).
+
+Given that SQLA has a B<very> distinct life on its own, and is used within an
+order of magnitude more projects compared to DBIC, it is prudent to B<not>
+disturb the current call chains within SQLA itself. Instead in the near future
+an effort will be undertaken to seek a more thorough decoupling of DBIC SQL
+generation from reliance on SQLA, possibly to a point where B<DBIC will no
+longer depend on SQLA> at all.
+
+B<The L<SQL::Abstract> library itself will continue being maintained> although
+it is not likely to gain many extra features, notably dialect support, at least
+not within the base C<SQL::Abstract> namespace.
+
+This work (if undertaken) will take into consideration the following
+constraints:
+
+=over
+
+=item Main API compatibility
+
+The object returned by C<< $schema->storage->sqlmaker >> needs to be able to
+satisfy most of the basic tests found in the current-at-the-time SQLA dist.
+While things like L<case|SQL::Abstract/case> or L<logic|SQL::Abstract/logic>
+or even worse L<convert|SQL::Abstract/convert> will definitely remain
+unsupported, the rest of the tests should pass (within reason).
+
+=item Ability to plug back an SQL::Abstract (or derivative)
+
+During the initial work on L<Data::Query> the test suite of DBIC turned out to
+be an invaluable asset to iron out hard-to-reason-about corner cases. In
+addition the test suite is much more vast and intricate than the tests of SQLA
+itself. This state of affairs is way too valuable to sacrifice in order to gain
+faster SQL generation. Thus a compile-time-ENV-check will be introduced along
+with an extra CI configuration to ensure that DBIC is used with an off-the-CPAN
+SQLA and that it continues to flawlessly run its entire test suite. While this
+will undoubtedly complicate the implementation of the better performing SQL
+generator, it will preserve both the usability of the test suite for external
+projects and will keep L<SQL::Abstract> from regressions in the future.
+
+=back
+
+Aside from these constraints it is becoming more and more practical to simply
+stop using SQLA in day-to-day production deployments of DBIC. The flexibility
+of the internals is simply not worth the performance cost.
+
+=head2 Relationship to L<Data::Query (DQ)|Data::Query>
+
+When initial work on DQ was taking place, the tools in L<::Storage::DBIHacks
+|http://github.com/dbsrgits/dbix-class/blob/master/lib/DBIx/Class/Storage/DBIHacks.pm>
+were only beginning to take shape, and it wasn't clear how important they will
+become further down the road. In fact the I<regexing all over the place> was
+considered an ugly stop-gap, and even a couple of highly entertaining talks
+were given to that effect. As the use-cases of DBIC were progressing, and
+evidence for the importance of supporting arbitrary SQL was mounting, it became
+clearer that DBIC itself would not really benefit in any way from an
+integration with DQ, but on the contrary is likely to lose functionality while
+the corners of the brand new DQ codebase are sanded off.
+
+The current status of DBIC/DQ integration is that the only benefit is for DQ by
+having access to the very extensive "early adopter" test suite, in the same
+manner as early DBIC benefitted tremendously from usurping the Class::DBI test
+suite. As far as the DBIC user-base - there are no immediate practical upsides
+to DQ integration, neither in terms of API nor in performance.
+
+So (as described higher up) the DBIC development effort will in the foreseable
+future ignore the existence of DQ, and will continue optimizing the preexisting
+SQLA-based solution, potentially "organically growing" its own compatible
+implementation. Also (again, as described higher up) the ability to plug a
+separate SQLA-compatible class providing the necessary surface API will remain
+possible, and will be protected at all costs in order to continue providing DQ
+access to the test cases of DBIC.
+
+In the short term, after one more pass over the ResultSet internals is
+undertaken I<real soon now (tm)>, and before the SQLA/SQLMaker integration
+takes place, the preexisting DQ-based branches will be pulled/modified/rebased
+to get up-to-date with the current state of the codebase, which changed very
+substantially since the last migration effort, especially in the SQL
+classification meta-parsing codepath.
+
 =cut
 
 use base qw/
@@ -44,8 +136,16 @@ use namespace::clean;
 
 __PACKAGE__->mk_group_accessors (simple => qw/quote_char name_sep limit_dialect/);
 
+sub _quoting_enabled {
+  ( defined $_[0]->{quote_char} and length $_[0]->{quote_char} ) ? 1 : 0
+}
+
 # for when I need a normalized l/r pair
 sub _quote_chars {
+
+  # in case we are called in the old !!$sm->_quote_chars fashion
+  return () if !wantarray and ( ! defined $_[0]->{quote_char} or ! length $_[0]->{quote_char} );
+
   map
     { defined $_ ? $_ : '' }
     ( ref $_[0]->{quote_char} ? (@{$_[0]->{quote_char}}) : ( ($_[0]->{quote_char}) x 2 ) )
@@ -110,17 +210,17 @@ sub select {
   my ($self, $table, $fields, $where, $rs_attrs, $limit, $offset) = @_;
 
 
-  $fields = $self->_recurse_fields($fields);
+  ($fields, @{$self->{select_bind}}) = $self->_recurse_fields($fields);
 
   if (defined $offset) {
     $self->throw_exception('A supplied offset must be a non-negative integer')
-      if ( $offset =~ /\D/ or $offset < 0 );
+      if ( $offset =~ /[^0-9]/ or $offset < 0 );
   }
   $offset ||= 0;
 
   if (defined $limit) {
     $self->throw_exception('A supplied limit must be a positive integer')
-      if ( $limit =~ /\D/ or $limit <= 0 );
+      if ( $limit =~ /[^0-9]/ or $limit <= 0 );
   }
   elsif ($offset) {
     $limit = $self->__max_int;
@@ -138,8 +238,9 @@ sub select {
     if( $limiter = $self->can ('emulate_limit') ) {
       carp_unique(
         'Support for the legacy emulate_limit() mechanism inherited from '
-      . 'SQL::Abstract::Limit has been deprecated, and will be removed when '
-      . 'DBIC transitions to Data::Query. If your code uses this type of '
+      . 'SQL::Abstract::Limit has been deprecated, and will be removed at '
+      . 'some future point, as it gets in the way of architectural and/or '
+      . 'performance advances within DBIC. If your code uses this type of '
       . 'limit specification please file an RT and provide the source of '
       . 'your emulate_limit() implementation, so an acceptable upgrade-path '
       . 'can be devised'
@@ -203,9 +304,9 @@ sub insert {
 # optimized due to hotttnesss
 #  my ($self, $table, $data, $options) = @_;
 
-  # SQLA will emit INSERT INTO $table ( ) VALUES ( )
+  # FIXME SQLA will emit INSERT INTO $table ( ) VALUES ( )
   # which is sadly understood only by MySQL. Change default behavior here,
-  # until SQLA2 comes with proper dialect support
+  # until we fold the extra pieces into SQLMaker properly
   if (! $_[2] or (ref $_[2] eq 'HASH' and !keys %{$_[2]} ) ) {
     my @bind;
     my $sql = sprintf(
@@ -231,42 +332,47 @@ sub _recurse_fields {
   return $$fields if $ref eq 'SCALAR';
 
   if ($ref eq 'ARRAY') {
-    return join(', ', map { $self->_recurse_fields($_) } @$fields);
+    my (@select, @bind);
+    for my $field (@$fields) {
+      my ($select, @new_bind) = $self->_recurse_fields($field);
+      push @select, $select;
+      push @bind, @new_bind;
+    }
+    return (join(', ', @select), @bind);
   }
   elsif ($ref eq 'HASH') {
     my %hash = %$fields;  # shallow copy
 
     my $as = delete $hash{-as};   # if supplied
 
-    my ($func, $args, @toomany) = %hash;
+    my ($func, $rhs, @toomany) = %hash;
 
     # there should be only one pair
     if (@toomany) {
       $self->throw_exception( "Malformed select argument - too many keys in hash: " . join (',', keys %$fields ) );
     }
 
-    if (lc ($func) eq 'distinct' && ref $args eq 'ARRAY' && @$args > 1) {
+    if (lc ($func) eq 'distinct' && ref $rhs eq 'ARRAY' && @$rhs > 1) {
       $self->throw_exception (
         'The select => { distinct => ... } syntax is not supported for multiple columns.'
-       .' Instead please use { group_by => [ qw/' . (join ' ', @$args) . '/ ] }'
-       .' or { select => [ qw/' . (join ' ', @$args) . '/ ], distinct => 1 }'
+       .' Instead please use { group_by => [ qw/' . (join ' ', @$rhs) . '/ ] }'
+       .' or { select => [ qw/' . (join ' ', @$rhs) . '/ ], distinct => 1 }'
       );
     }
 
+    my ($rhs_sql, @rhs_bind) = $self->_recurse_fields($rhs);
     my $select = sprintf ('%s( %s )%s',
       $self->_sqlcase($func),
-      $self->_recurse_fields($args),
+      $rhs_sql,
       $as
         ? sprintf (' %s %s', $self->_sqlcase('as'), $self->_quote ($as) )
         : ''
     );
 
-    return $select;
+    return ($select, @rhs_bind);
   }
-  # Is the second check absolutely necessary?
   elsif ( $ref eq 'REF' and ref($$fields) eq 'ARRAY' ) {
-    push @{$self->{select_bind}}, @{$$fields}[1..$#$$fields];
-    return $$fields->[0];
+    return @{$$fields};
   }
   else {
     $self->throw_exception( $ref . qq{ unexpected in _recurse_fields()} );
@@ -281,28 +387,36 @@ sub _recurse_fields {
 # things in the SQLA space need to have more info about the $rs they
 # create SQL for. The alternative would be to keep expanding the
 # signature of _select with more and more positional parameters, which
-# is just gross. All hail SQLA2!
+# is just gross.
+#
+# FIXME - this will have to transition out to a subclass when the effort
+# of folding the SQLA machinery into SQLMaker takes place
 sub _parse_rs_attrs {
   my ($self, $arg) = @_;
 
   my $sql = '';
+  my @sqlbind;
 
-  if ($arg->{group_by}) {
-    # horrible horrible, waiting for refactor
-    local $self->{select_bind};
-    if (my $g = $self->_recurse_fields($arg->{group_by}) ) {
-      $sql .= $self->_sqlcase(' group by ') . $g;
-      push @{$self->{group_bind} ||= []}, @{$self->{select_bind}||[]};
-    }
+  if (
+    $arg->{group_by}
+      and
+    @sqlbind = $self->_recurse_fields($arg->{group_by})
+  ) {
+    $sql .= $self->_sqlcase(' group by ') . shift @sqlbind;
+    push @{$self->{group_bind}}, @sqlbind;
   }
 
-  if (defined $arg->{having}) {
-    my ($frag, @bind) = $self->_recurse_where($arg->{having});
-    push(@{$self->{having_bind}}, @bind);
-    $sql .= $self->_sqlcase(' having ') . $frag;
+  if (
+    $arg->{having}
+      and
+    @sqlbind = $self->_recurse_where($arg->{having})
+  ) {
+    $sql .= $self->_sqlcase(' having ') . shift @sqlbind;
+    push(@{$self->{having_bind}}, @sqlbind);
   }
 
-  if (defined $arg->{order_by}) {
+  if ($arg->{order_by}) {
+    # unlike the 2 above, _order_by injects into @{...bind...} for us
     $sql .= $self->_order_by ($arg->{order_by});
   }
 
@@ -313,14 +427,18 @@ sub _order_by {
   my ($self, $arg) = @_;
 
   # check that we are not called in legacy mode (order_by as 4th argument)
-  if (ref $arg eq 'HASH' and not grep { $_ =~ /^-(?:desc|asc)/i } keys %$arg ) {
-    return $self->_parse_rs_attrs ($arg);
-  }
-  else {
-    my ($sql, @bind) = $self->next::method($arg);
-    push @{$self->{order_bind}}, @bind;
-    return $sql;
-  }
+  (
+    ref $arg eq 'HASH'
+      and
+    not grep { $_ =~ /^-(?:desc|asc)/i } keys %$arg
+  )
+    ? $self->_parse_rs_attrs ($arg)
+    : do {
+      my ($sql, @bind) = $self->next::method($arg);
+      push @{$self->{order_bind}}, @bind;
+      $sql; # RV
+    }
+  ;
 }
 
 sub _split_order_chunk {
@@ -441,8 +559,6 @@ sub _join_condition {
 
   # Backcompat for the old days when a plain hashref
   # { 't1.col1' => 't2.col2' } meant ON t1.col1 = t2.col2
-  # Once things settle we should start warning here so that
-  # folks unroll their hacks
   if (
     ref $cond eq 'HASH'
       and
@@ -452,6 +568,12 @@ sub _join_condition {
       and
     ! ref ( (values %$cond)[0] )
   ) {
+    carp_unique(
+      "ResultSet {from} structures with conditions not conforming to the "
+    . "SQL::Abstract syntax are deprecated: you either need to stop abusing "
+    . "{from} altogether, or express the condition properly using the "
+    . "{ -ident => ... } operator"
+    );
     $cond = { keys %$cond => { -ident => values %$cond } }
   }
   elsif ( ref $cond eq 'ARRAY' ) {
@@ -469,9 +591,14 @@ sub _join_condition {
   return $self->_recurse_where($cond);
 }
 
-# This is hideously ugly, but SQLA does not understand multicol IN expressions
-# FIXME TEMPORARY - DQ should have native syntax for this
-# moved here to raise API questions
+# !!! EXPERIMENTAL API !!! WILL CHANGE !!!
+#
+# This is rather odd, but vanilla SQLA does not have support for multicolumn IN
+# expressions
+# Currently has only one callsite in ResultSet, body moved into this subclass
+# of SQLA to raise API questions like:
+# - how do we convey a list of idents...?
+# - can binds reside on lhs?
 #
 # !!! EXPERIMENTAL API !!! WILL CHANGE !!!
 sub _where_op_multicolumn_in {
@@ -518,14 +645,17 @@ sub _where_op_multicolumn_in {
   \[ join( ' IN ', shift @$$lhs, shift @$$rhs ), @$$lhs, @$$rhs ];
 }
 
-1;
-
-=head1 AUTHORS
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
index ec9300a..0cfcd2b 100644 (file)
@@ -61,7 +61,7 @@ sub _LimitOffset {
 
 =head2 LimitXY
 
- SELECT ... LIMIT $offset $limit
+ SELECT ... LIMIT $offset, $limit
 
 Supported by B<MySQL> and any L<SQL::Statement> based DBD
 
@@ -157,7 +157,7 @@ sub _rno_default_order {
 
  SELECT SKIP $offset FIRST $limit * FROM ...
 
-Suported by B<Informix>, almost like LimitOffset. According to
+Supported by B<Informix>, almost like LimitOffset. According to
 L<SQL::Abstract::Limit> C<... SKIP $offset LIMIT $limit ...> is also supported.
 
 =cut
@@ -221,7 +221,7 @@ sub _FirstSkip {
 Depending on the resultset attributes one of:
 
  SELECT * FROM (
-  SELECT *, ROWNUM rownum__index FROM (
+  SELECT *, ROWNUM AS rownum__index FROM (
    SELECT ...
   ) WHERE ROWNUM <= ($limit+$offset)
  ) WHERE rownum__index >= ($offset+1)
@@ -229,7 +229,7 @@ Depending on the resultset attributes one of:
 or
 
  SELECT * FROM (
-  SELECT *, ROWNUM rownum__index FROM (
+  SELECT *, ROWNUM AS rownum__index FROM (
     SELECT ...
   )
  ) WHERE rownum__index BETWEEN ($offset+1) AND ($limit+$offset)
@@ -273,12 +273,12 @@ EOS
   # method, and the slower BETWEEN query is used instead
   #
   # FIXME - this is quite expensive, and does not perform caching of any sort
-  # as soon as some of the DQ work becomes viable consider switching this
-  # over
+  # as soon as some of the SQLA-inlining work becomes viable consider adding
+  # some rudimentary caching support
   if (
     $rs_attrs->{order_by}
       and
-    $rs_attrs->{_rsroot_rsrc}->storage->_order_by_is_stable(
+    $rs_attrs->{result_source}->storage->_order_by_is_stable(
       @{$rs_attrs}{qw/from order_by where/}
     )
   ) {
@@ -286,7 +286,7 @@ EOS
 
     return <<EOS;
 SELECT $sq_attrs->{selection_outer} FROM (
-  SELECT $sq_attrs->{selection_outer}, ROWNUM $idx_name FROM (
+  SELECT $sq_attrs->{selection_outer}, ROWNUM AS $idx_name FROM (
     SELECT $sq_attrs->{selection_inner} $sq_attrs->{query_leftover}${order_group_having}
   ) $qalias WHERE ROWNUM <= ?
 ) $qalias WHERE $idx_name >= ?
@@ -297,7 +297,7 @@ EOS
 
     return <<EOS;
 SELECT $sq_attrs->{selection_outer} FROM (
-  SELECT $sq_attrs->{selection_outer}, ROWNUM $idx_name FROM (
+  SELECT $sq_attrs->{selection_outer}, ROWNUM AS $idx_name FROM (
     SELECT $sq_attrs->{selection_inner} $sq_attrs->{query_leftover}${order_group_having}
   ) $qalias
 ) $qalias WHERE $idx_name BETWEEN ? AND ?
@@ -331,7 +331,7 @@ sub _prep_for_skimming_limit {
     if ($sq_attrs->{order_by_requested}) {
       $self->throw_exception (
         'Unable to safely perform "skimming type" limit with supplied unstable order criteria'
-      ) unless ($rs_attrs->{_rsroot_rsrc}->schema->storage->_order_by_is_stable(
+      ) unless ($rs_attrs->{result_source}->schema->storage->_order_by_is_stable(
         $rs_attrs->{from},
         $requested_order,
         $rs_attrs->{where},
@@ -343,11 +343,11 @@ sub _prep_for_skimming_limit {
       $inner_order = [ map
         { "$rs_attrs->{alias}.$_" }
         ( @{
-          $rs_attrs->{_rsroot_rsrc}->_identifying_column_set
+          $rs_attrs->{result_source}->_identifying_column_set
             ||
           $self->throw_exception(sprintf(
             'Unable to auto-construct stable order criteria for "skimming type" limit '
-          . "dialect based on source '%s'", $rs_attrs->{_rsroot_rsrc}->name) );
+          . "dialect based on source '%s'", $rs_attrs->{result_source}->name) );
         } )
       ];
     }
@@ -532,29 +532,37 @@ Currently used by B<Sybase ASE>, due to lack of any other option.
 sub _GenericSubQ {
   my ($self, $sql, $rs_attrs, $rows, $offset) = @_;
 
-  my $root_rsrc = $rs_attrs->{_rsroot_rsrc};
+  my $main_rsrc = $rs_attrs->{result_source};
 
   # Explicitly require an order_by
   # GenSubQ is slow enough as it is, just emulating things
   # like in other cases is not wise - make the user work
   # to shoot their DBA in the foot
-  my $supplied_order = delete $rs_attrs->{order_by} or $self->throw_exception (
+  $self->throw_exception (
     'Generic Subquery Limit does not work on resultsets without an order. Provide a stable, '
-  . 'root-table-based order criteria.'
+  . 'main-table-based order criteria.'
+  ) unless $rs_attrs->{order_by};
+
+  my $usable_order_colinfo = $main_rsrc->storage->_extract_colinfo_of_stable_main_source_order_by_portion(
+    $rs_attrs
   );
 
-  my $usable_order_ci = $root_rsrc->storage->_main_source_order_by_portion_is_stable(
-    $root_rsrc,
-    $supplied_order,
-    $rs_attrs->{where},
-  ) or $self->throw_exception(
-    'Generic Subquery Limit can not work with order criteria based on sources other than the current one'
+  $self->throw_exception(
+    'Generic Subquery Limit can not work with order criteria based on sources other than the main one'
+  ) if (
+    ! keys %{$usable_order_colinfo||{}}
+      or
+    grep
+      { $_->{-source_alias} ne $rs_attrs->{alias} }
+      (values %$usable_order_colinfo)
   );
 
 ###
 ###
 ### we need to know the directions after we figured out the above - reextract *again*
 ### this is eyebleed - trying to get it to work at first
+  my $supplied_order = delete $rs_attrs->{order_by};
+
   my @order_bits = do {
     local $self->{quote_char};
     local $self->{order_bind};
@@ -562,20 +570,20 @@ sub _GenericSubQ {
   };
 
   # truncate to what we'll use
-  $#order_bits = ( (keys %$usable_order_ci) - 1 );
+  $#order_bits = ( (keys %$usable_order_colinfo) - 1 );
 
   # @order_bits likely will come back quoted (due to how the prefetch
   # rewriter operates
   # Hence supplement the column_info lookup table with quoted versions
   if ($self->quote_char) {
-    $usable_order_ci->{$self->_quote($_)} = $usable_order_ci->{$_}
-      for keys %$usable_order_ci;
+    $usable_order_colinfo->{$self->_quote($_)} = $usable_order_colinfo->{$_}
+      for keys %$usable_order_colinfo;
   }
 
 # calculate the condition
   my $count_tbl_alias = 'rownum__emulation';
-  my $root_alias = $rs_attrs->{alias};
-  my $root_tbl_name = $root_rsrc->name;
+  my $main_alias = $rs_attrs->{alias};
+  my $main_tbl_name = $main_rsrc->name;
 
   my (@unqualified_names, @qualified_names, @is_desc, @new_order_by);
 
@@ -584,17 +592,17 @@ sub _GenericSubQ {
     ($bit, my $is_desc) = $self->_split_order_chunk($bit);
 
     push @is_desc, $is_desc;
-    push @unqualified_names, $usable_order_ci->{$bit}{-colname};
-    push @qualified_names, $usable_order_ci->{$bit}{-fq_colname};
+    push @unqualified_names, $usable_order_colinfo->{$bit}{-colname};
+    push @qualified_names, $usable_order_colinfo->{$bit}{-fq_colname};
 
-    push @new_order_by, { ($is_desc ? '-desc' : '-asc') => $usable_order_ci->{$bit}{-fq_colname} };
+    push @new_order_by, { ($is_desc ? '-desc' : '-asc') => $usable_order_colinfo->{$bit}{-fq_colname} };
   };
 
   my (@where_cond, @skip_colpair_stack);
   for my $i (0 .. $#order_bits) {
-    my $ci = $usable_order_ci->{$order_bits[$i]};
+    my $ci = $usable_order_colinfo->{$order_bits[$i]};
 
-    my ($subq_col, $main_col) = map { "$_.$ci->{-colname}" } ($count_tbl_alias, $root_alias);
+    my ($subq_col, $main_col) = map { "$_.$ci->{-colname}" } ($count_tbl_alias, $main_alias);
     my $cur_cond = { $subq_col => { ($is_desc[$i] ? '>' : '<') => { -ident => $main_col } } };
 
     push @skip_colpair_stack, [
@@ -683,7 +691,7 @@ WHERE ( SELECT COUNT(*) FROM %s %s $counted_where ) $rownum_cond
 $inner_order_sql
   ", map { $self->_quote ($_) } (
     $rs_attrs->{alias},
-    $root_tbl_name,
+    $main_tbl_name,
     $count_tbl_alias,
   ));
 }
@@ -693,7 +701,7 @@ $inner_order_sql
 #
 # Generates inner/outer select lists for various limit dialects
 # which result in one or more subqueries (e.g. RNO, Top, RowNum)
-# Any non-root-table columns need to have their table qualifier
+# Any non-main-table columns need to have their table qualifier
 # turned into a column alias (otherwise names in subqueries clash
 # and/or lose their source table)
 #
@@ -725,23 +733,22 @@ sub _subqueried_limit_attrs {
 
   my ($re_sep, $re_alias) = map { quotemeta $_ } ( $self->{name_sep}, $rs_attrs->{alias} );
 
-  # insulate from the multiple _recurse_fields calls below
-  local $self->{select_bind};
-
   # correlate select and as, build selection index
   my (@sel, $in_sel_index);
   for my $i (0 .. $#{$rs_attrs->{select}}) {
 
     my $s = $rs_attrs->{select}[$i];
-    my $sql_sel = $self->_recurse_fields ($s);
     my $sql_alias = (ref $s) eq 'HASH' ? $s->{-as} : undef;
 
+    # we throw away the @bind here deliberately
+    my ($sql_sel) = $self->_recurse_fields ($s);
+
     push @sel, {
       arg => $s,
       sql => $sql_sel,
       unquoted_sql => do {
         local $self->{quote_char};
-        $self->_recurse_fields ($s);
+        ($self->_recurse_fields ($s))[0]; # ignore binds again
       },
       as =>
         $sql_alias
@@ -822,14 +829,17 @@ sub _unqualify_colname {
   return $fqcn;
 }
 
-1;
-
-=head1 AUTHORS
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
index d1ed9a2..b4c1584 100644 (file)
@@ -4,14 +4,16 @@ package # Hide from PAUSE
 use warnings;
 use strict;
 
-use base qw( DBIx::Class::SQLMaker );
-
 BEGIN {
-  use DBIx::Class::Optional::Dependencies;
-  die('The following extra modules are required for Oracle-based Storages ' . DBIx::Class::Optional::Dependencies->req_missing_for ('id_shortener') . "\n" )
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('id_shortener');
+  require DBIx::Class::Optional::Dependencies;
+  if (my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('id_shortener') ) {
+    die "The following extra modules are required for Oracle-based Storages: $missing\n";
+  }
+  require Digest::MD5;
 }
 
+use base 'DBIx::Class::SQLMaker';
+
 sub new {
   my $self = shift;
   my %opts = (ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
@@ -144,9 +146,6 @@ sub _shorten_identifier {
   @keywords = $to_shorten unless @keywords;
 
   # get a base36 md5 of the identifier
-  require Digest::MD5;
-  require Math::BigInt;
-  require Math::Base36;
   my $b36sum = Math::Base36::encode_base36(
     Math::BigInt->from_hex (
       '0x' . Digest::MD5::md5_hex ($to_shorten)
index b95c56e..0f50467 100644 (file)
@@ -80,13 +80,35 @@ sub _recurse_oracle_joins {
         && $jt !~ /inner/i;
     }
 
-    # sadly SQLA treats where($scalar) as literal, so we need to jump some hoops
-    push @where, map { \sprintf ('%s%s = %s%s',
-      ref $_ ? $self->_recurse_where($_) : $self->_quote($_),
-      $left_join,
-      ref $on->{$_} ? $self->_recurse_where($on->{$_}) : $self->_quote($on->{$_}),
-      $right_join,
-    )} keys %$on;
+    # FIXME - the code below *UTTERLY* doesn't work with custom conds... sigh
+    # for the time being do not do any processing with the likes of _collapse_cond
+    # instead only unroll the -and hack if present
+    $on = $on->{-and}[0] if (
+      ref $on eq 'HASH'
+        and
+      keys %$on == 1
+        and
+      ref $on->{-and} eq 'ARRAY'
+        and
+      @{$on->{-and}} == 1
+    );
+
+
+    push @where, map { \do {
+        my ($sql) = $self->_recurse_where({
+          # FIXME - more borkage, more or less a copy of the kludge in ::SQLMaker::_join_condition()
+          $_ => ( length ref $on->{$_}
+            ? $on->{$_}
+            : { -ident => $on->{$_} }
+          )
+        });
+
+        $sql =~ s/\s*\=/$left_join =/
+          if $left_join;
+
+        "$sql$right_join";
+      }
+    } sort keys %$on;
   }
 
   return { -and => \@where };
@@ -94,7 +116,7 @@ sub _recurse_oracle_joins {
 
 1;
 
-=pod
+__END__
 
 =head1 NAME
 
@@ -152,17 +174,13 @@ Does not support full outer joins (however neither really does DBIC itself)
 
 =back
 
-=head1 AUTHOR
-
-Justin Wheeler C<< <jwheeler@datademons.com> >>
-
-=head1 CONTRIBUTORS
-
-David Jack Olrik C<< <djo@cpan.org> >>
-
-=head1 LICENSE
+=head1 FURTHER QUESTIONS?
 
-This module is licensed under the same terms as Perl itself.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=cut
+=head1 COPYRIGHT AND LICENSE
 
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 4c3cce5..0be8919 100644 (file)
@@ -8,8 +8,10 @@ use base 'DBIx::Class';
 use DBIx::Class::Carp;
 use Try::Tiny;
 use Scalar::Util qw/weaken blessed/;
-use DBIx::Class::_Util 'refcount';
-use Sub::Name 'subname';
+use DBIx::Class::_Util qw(
+  refcount quote_sub scope_guard
+  is_exception dbic_internal_try
+);
 use Devel::GlobalDestruction;
 use namespace::clean;
 
@@ -109,11 +111,12 @@ are no matching Result classes like this:
 
   load_namespaces found ResultSet class $classname with no corresponding Result class
 
-If a Result class is found to already have a ResultSet class set using
-L</resultset_class> to some other class, you will be warned like this:
+If a ResultSource instance is found to already have a ResultSet class set
+using L<resultset_class|DBIx::Class::ResultSource/resultset_class> to some
+other class, you will be warned like this:
 
-  We found ResultSet class '$rs_class' for '$result', but it seems
-  that you had already set '$result' to use '$rs_set' instead
+  We found ResultSet class '$rs_class' for '$result_class', but it seems
+  that you had already set '$result_class' to use '$rs_set' instead
 
 =head3 Examples
 
@@ -191,7 +194,7 @@ sub _ns_get_rsrc_instance {
   my $me = shift;
   my $rs_class = ref ($_[0]) || $_[0];
 
-  return try {
+  return dbic_internal_try {
     $rs_class->result_source_instance
   } catch {
     $me->throw_exception (
@@ -803,7 +806,7 @@ sub connection {
 
   $storage_class =~ s/^::/DBIx::Class::Storage::/;
 
-  try {
+  dbic_internal_try {
     $self->ensure_class_loaded ($storage_class);
   }
   catch {
@@ -897,7 +900,6 @@ sub compose_namespace {
     local *Class::C3::reinitialize = sub { } if DBIx::Class::_ENV_::OLD_MRO;
     use warnings qw/redefine/;
 
-    no strict qw/refs/;
     foreach my $source_name ($self->sources) {
       my $orig_source = $self->source($source_name);
 
@@ -919,11 +921,8 @@ sub compose_namespace {
       }
     }
 
-    foreach my $meth (qw/class source resultset/) {
-      no warnings 'redefine';
-      *{"${target}::${meth}"} = subname "${target}::${meth}" =>
-        sub { shift->schema->$meth(@_) };
-    }
+    quote_sub "${target}::${_}" => "shift->schema->$_(\@_)"
+      for qw(class source resultset);
   }
 
   Class::C3->reinitialize() if DBIx::Class::_ENV_::OLD_MRO;
@@ -1059,26 +1058,71 @@ default behavior will provide a detailed stack trace.
 =cut
 
 sub throw_exception {
-  my $self = shift;
+  my ($self, @args) = @_;
+
+  if (
+    ! DBIx::Class::_Util::in_internal_try()
+      and
+    my $act = $self->exception_action
+  ) {
+
+    my $guard_disarmed;
+
+    my $guard = scope_guard {
+      return if $guard_disarmed;
+      local $SIG{__WARN__};
+      Carp::cluck("
+                    !!! DBIx::Class INTERNAL PANIC !!!
+
+The exception_action() handler installed on '$self'
+aborted the stacktrace below via a longjmp (either via Return::Multilevel or
+plain goto, or Scope::Upper or something equally nefarious). There currently
+is nothing safe DBIx::Class can do, aside from displaying this error. A future
+version ( 0.082900, when available ) will reduce the cases in which the
+handler is invoked, but this is neither a complete solution, nor can it do
+anything for other software that might be affected by a similar problem.
+
+                      !!! FIX YOUR ERROR HANDLING !!!
 
-  if (my $act = $self->exception_action) {
-    if ($act->(@_)) {
-      DBIx::Class::Exception->throw(
+This guard was activated beginning"
+      );
+    };
+
+    eval {
+      # if it throws - good, we'll assign to @args in the end
+      # if it doesn't - do different things depending on RV truthiness
+      if( $act->(@args) ) {
+        $args[0] = (
           "Invocation of the exception_action handler installed on $self did *not*"
         .' result in an exception. DBIx::Class is unable to function without a reliable'
-        .' exception mechanism, ensure that exception_action does not hide exceptions'
-        ." (original error: $_[0])"
-      );
+        .' exception mechanism, ensure your exception_action does not hide exceptions'
+        ." (original error: $args[0])"
+        );
+      }
+      else {
+        carp_unique (
+          "The exception_action handler installed on $self returned false instead"
+        .' of throwing an exception. This behavior has been deprecated, adjust your'
+        .' handler to always rethrow the supplied error'
+        );
+      }
+
+      1;
     }
 
-    carp_unique (
-      "The exception_action handler installed on $self returned false instead"
-    .' of throwing an exception. This behavior has been deprecated, adjust your'
-    .' handler to always rethrow the supplied error.'
+      or
+
+    # We call this to get the necessary warnings emitted and disregard the RV
+    # as it's definitely an exception if we got as far as this do{} block
+    is_exception(
+      $args[0] = $@
     );
+
+    # Done guarding against https://github.com/PerlDancer/Dancer2/issues/1125
+    $guard_disarmed = 1;
   }
 
-  DBIx::Class::Exception->throw($_[0], $self->stacktrace);
+  DBIx::Class::Exception->throw( $args[0], $self->stacktrace );
 }
 
 =head2 deploy
@@ -1122,8 +1166,8 @@ sub deploy {
 
 A convenient shortcut to
 C<< $self->storage->deployment_statements($self, @args) >>.
-Returns the SQL statements used by L</deploy> and
-L<DBIx::Class::Schema::Storage/deploy>.
+Returns the statements used by L</deploy> and
+L<DBIx::Class::Storage/deploy>.
 
 =cut
 
@@ -1217,19 +1261,17 @@ reference to any schema, so are rather useless.
 sub thaw {
   my ($self, $obj) = @_;
   local $DBIx::Class::ResultSourceHandle::thaw_schema = $self;
-  require Storable;
   return Storable::thaw($obj);
 }
 
 =head2 freeze
 
-This doesn't actually do anything more than call L<Storable/nfreeze>, it is just
-provided here for symmetry.
+This doesn't actually do anything beyond calling L<nfreeze|Storable/SYNOPSIS>,
+it is just provided here for symmetry.
 
 =cut
 
 sub freeze {
-  require Storable;
   return Storable::nfreeze($_[1]);
 }
 
@@ -1252,7 +1294,6 @@ objects so their references to the schema object
 sub dclone {
   my ($self, $obj) = @_;
   local $DBIx::Class::ResultSourceHandle::thaw_schema = $self;
-  require Storable;
   return Storable::dclone($obj);
 }
 
@@ -1367,7 +1408,7 @@ sub _register_source {
   return $source if $params->{extra};
 
   my $rs_class = $source->result_class;
-  if ($rs_class and my $rsrc = try { $rs_class->result_source_instance } ) {
+  if ($rs_class and my $rsrc = dbic_internal_try { $rs_class->result_source_instance } ) {
     my %map = %{$self->class_mappings};
     if (
       exists $map{$rs_class}
@@ -1392,6 +1433,9 @@ sub _register_source {
 
 my $global_phase_destroy;
 sub DESTROY {
+  ### NO detected_reinvoked_destructor check
+  ### This code very much relies on being called multuple times
+
   return if $global_phase_destroy ||= in_global_destruction;
 
   my $self = shift;
@@ -1418,6 +1462,11 @@ sub DESTROY {
       last;
     }
   }
+
+  # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+  # collected before leaving this scope. Depending on the code above, this
+  # may very well be just a preventive measure guarding future modifications
+  undef;
 }
 
 sub _unregister_source {
@@ -1474,13 +1523,12 @@ sub compose_connection {
   carp_once "compose_connection deprecated as of 0.08000"
     unless $INC{"DBIx/Class/CDBICompat.pm"};
 
-  my $base = 'DBIx::Class::ResultSetProxy';
-  try {
-    eval "require ${base};"
+  dbic_internal_try {
+    require DBIx::Class::ResultSetProxy;
   }
   catch {
     $self->throw_exception
-      ("No arguments to load_classes and couldn't load ${base} ($_)")
+      ("No arguments to load_classes and couldn't load DBIx::Class::ResultSetProxy ($_)")
   };
 
   if ($self eq $target) {
@@ -1488,7 +1536,7 @@ sub compose_connection {
     foreach my $source_name ($self->sources) {
       my $source = $self->source($source_name);
       my $class = $source->result_class;
-      $self->inject_base($class, $base);
+      $self->inject_base($class, 'DBIx::Class::ResultSetProxy');
       $class->mk_classdata(resultset_instance => $source->resultset);
       $class->mk_classdata(class_resolver => $self);
     }
@@ -1496,12 +1544,8 @@ sub compose_connection {
     return $self;
   }
 
-  my $schema = $self->compose_namespace($target, $base);
-  {
-    no strict 'refs';
-    my $name = join '::', $target, 'schema';
-    *$name = subname $name, sub { $schema };
-  }
+  my $schema = $self->compose_namespace($target, 'DBIx::Class::ResultSetProxy');
+  quote_sub "${target}::schema", '$s', { '$s' => \$schema };
 
   $schema->connection(@info);
   foreach my $source_name ($schema->sources) {
@@ -1515,14 +1559,17 @@ sub compose_connection {
   return $schema;
 }
 
-1;
-
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
index 114064a..d59961f 100644 (file)
@@ -202,8 +202,9 @@ use warnings;
 use base 'DBIx::Class::Schema';
 
 use DBIx::Class::Carp;
+use DBIx::Class::_Util 'dbic_internal_try';
 use Time::HiRes qw/gettimeofday/;
-use Try::Tiny;
+use Scalar::Util 'weaken';
 use namespace::clean;
 
 __PACKAGE__->mk_classdata('_filedata');
@@ -238,7 +239,7 @@ Call this to initialise a previously unversioned database. The table 'dbix_class
 
 Takes one argument which should be the version that the database is currently at. Defaults to the return value of L</schema_version>.
 
-See L</getting_started> for more details.
+See L</GETTING STARTED> for more details.
 
 =cut
 
@@ -526,7 +527,7 @@ sub get_db_version
     my ($self, $rs) = @_;
 
     my $vtable = $self->{vschema}->resultset('Table');
-    my $version = try {
+    my $version = dbic_internal_try {
       $vtable->search({}, { order_by => { -desc => 'installed' }, rows => 1 } )
               ->get_column ('version')
                ->next;
@@ -589,9 +590,10 @@ sub _on_connect
 {
   my ($self) = @_;
 
-  my $conn_info = $self->storage->connect_info;
-  $self->{vschema} = DBIx::Class::Version->connect(@$conn_info);
-  my $conn_attrs = $self->{vschema}->storage->_dbic_connect_attributes || {};
+  weaken (my $w_self = $self );
+
+  $self->{vschema} = DBIx::Class::Version->connect(sub { $w_self->storage->dbh });
+  my $conn_attrs = $self->storage->_dbic_connect_attributes || {};
 
   my $vtable = $self->{vschema}->resultset('Table');
 
@@ -600,10 +602,10 @@ sub _on_connect
 
   # check for legacy versions table and move to new if exists
   unless ($self->_source_exists($vtable)) {
-    my $vtable_compat = DBIx::Class::VersionCompat->connect(@$conn_info)->resultset('TableCompat');
+    my $vtable_compat = DBIx::Class::VersionCompat->connect(sub { $w_self->storage->dbh })->resultset('TableCompat');
     if ($self->_source_exists($vtable_compat)) {
       $self->{vschema}->deploy;
-      map { $vtable->create({ installed => $_->Installed, version => $_->Version }) } $vtable_compat->all;
+      map { $vtable->new_result({ installed => $_->Installed, version => $_->Version })->insert } $vtable_compat->all;
       $self->storage->_get_dbh->do("DROP TABLE " . $vtable_compat->result_source->from);
     }
   }
@@ -640,8 +642,8 @@ sub _create_db_to_schema_diff {
     return;
   }
 
-  unless (DBIx::Class::Optional::Dependencies->req_ok_for ('deploy')) {
-    $self->throw_exception("Unable to proceed without " . DBIx::Class::Optional::Dependencies->req_missing_for ('deploy') );
+  if ( my $missing = DBIx::Class::Optional::Dependencies->req_missing_for('deploy') ) {
+    $self->throw_exception("Unable to proceed without $missing");
   }
 
   my $db_tr = SQL::Translator->new({
@@ -710,7 +712,7 @@ sub _set_db_version {
   # formatted by this new function will sort _after_ any existing 200... strings.
   my @tm = gettimeofday();
   my @dt = gmtime ($tm[0]);
-  my $o = $vtable->create({
+  my $o = $vtable->new_result({
     version => $version,
     installed => sprintf("v%04d%02d%02d_%02d%02d%02d.%03.0f",
       $dt[5] + 1900,
@@ -721,7 +723,7 @@ sub _set_db_version {
       $dt[0],
       int($tm[1] / 1000), # convert to millisecs
     ),
-  });
+  })->insert;
 }
 
 sub _read_sql_file {
@@ -744,23 +746,28 @@ sub _read_sql_file {
 
 sub _source_exists
 {
-    my ($self, $rs) = @_;
-
-    return try {
-      $rs->search(\'1=0')->cursor->next;
-      1;
-    } catch {
-      0;
-    };
+  my ($self, $rs) = @_;
+
+  ( dbic_internal_try {
+    $rs->search(\'1=0')->cursor->next;
+    1;
+  } )
+    ? 1
+    : 0
+  ;
 }
 
-1;
+=head1 FURTHER QUESTIONS?
 
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 COPYRIGHT AND LICENSE
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
-=head1 LICENSE
+=cut
 
-You may distribute this code under the same terms as Perl itself.
+1;
index 2a295d3..d0299cd 100644 (file)
@@ -73,12 +73,13 @@ method.
 
 The deserializing hook called on the object during deserialization.
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
-
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 10b554a..dff403b 100644 (file)
@@ -3,6 +3,10 @@ package DBIx::Class::StartupCheck;
 use strict;
 use warnings;
 
+1;
+
+__END__
+
 =head1 NAME
 
 DBIx::Class::StartupCheck - Run environment checks on startup
@@ -30,22 +34,13 @@ warning message on startup sent to STDERR, explaining what to do about
 it and how to suppress the message. If you don't see any messages, you
 have nothing to worry about.
 
-=head1 CONTRIBUTORS
-
-Nigel Metheringham
-
-Brandon Black
+=head1 FURTHER QUESTIONS?
 
-Matt S. Trout
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 AUTHOR
+=head1 COPYRIGHT AND LICENSE
 
-Jon Schutz
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
-
-=cut
-
-1;
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 1addeaf..47aef36 100644 (file)
@@ -16,6 +16,7 @@ use DBIx::Class::Carp;
 use DBIx::Class::Storage::BlockRunner;
 use Scalar::Util qw/blessed weaken/;
 use DBIx::Class::Storage::TxnScopeGuard;
+use DBIx::Class::_Util 'dbic_internal_try';
 use Try::Tiny;
 use namespace::clean;
 
@@ -51,7 +52,6 @@ sub new {
   $self = ref $self if ref $self;
 
   my $new = bless( {
-    transaction_depth => 0,
     savepoints => [],
   }, $self);
 
@@ -227,6 +227,7 @@ sub txn_commit {
     $self->debugobj->txn_commit() if $self->debug;
     $self->_exec_txn_commit;
     $self->{transaction_depth}--;
+    $self->savepoints([]);
   }
   elsif($self->transaction_depth > 1) {
     $self->{transaction_depth}--;
@@ -250,8 +251,20 @@ sub txn_rollback {
 
   if ($self->transaction_depth == 1) {
     $self->debugobj->txn_rollback() if $self->debug;
-    $self->_exec_txn_rollback;
     $self->{transaction_depth}--;
+
+    # in case things get really hairy - just disconnect
+    dbic_internal_try { $self->_exec_txn_rollback; 1 } or do {
+      my $rollback_error = $@;
+
+      # whatever happens, too low down the stack to care
+      # FIXME - revisit if stackable exceptions become a thing
+      dbic_internal_try { $self->disconnect };
+
+      die $rollback_error;
+    };
+
+    $self->savepoints([]);
   }
   elsif ($self->transaction_depth > 1) {
     $self->{transaction_depth}--;
@@ -271,6 +284,98 @@ sub txn_rollback {
   }
 }
 
+# to be called by several internal stacked transaction handler codepaths
+# not for external consumption
+# *DOES NOT* throw exceptions, instead:
+#  - returns false on success
+#  - returns the exception on failed rollback
+sub __delicate_rollback {
+  my $self = shift;
+
+  if (
+    ( $self->transaction_depth || 0 ) > 1
+      and
+    # FIXME - the autosvp check here shouldn't be happening, it should be a role-ish thing
+    # The entire concept needs to be rethought with the storage layer... or something
+    ! $self->auto_savepoint
+      and
+    # the handle seems healthy, and there is nothing for us to do with it
+    # just go ahead and bow out, without triggering the txn_rollback() "nested exception"
+    # the unwind will eventually fail somewhere higher up if at all
+    # FIXME: a ::Storage::DBI-specific method, not a generic ::Storage one
+    $self->_seems_connected
+  ) {
+    # all above checks out - there is nothing to do on the $dbh itself
+    # just a plain soft-decrease of depth
+    $self->{transaction_depth}--;
+    return;
+  }
+
+  my @args = @_;
+  my $rbe;
+
+  dbic_internal_try {
+    $self->txn_rollback; 1
+  }
+  catch {
+
+    $rbe = $_;
+
+    # we were passed an existing exception to augment (think DESTROY stacks etc)
+    if (@args) {
+      my ($exception) = @args;
+
+      # append our text - THIS IS A TEMPORARY FIXUP!
+      #
+      # If the passed in exception is a reference, or an object we don't know
+      # how to augment - flattening it is just damn rude
+      if (
+        # FIXME - a better way, not liable to destroy an existing exception needs
+        # to be created. For the time being perpetuating the sin below in order
+        # to break the deadlock of which yak is being shaved first
+        0
+          and
+        length ref $$exception
+          and
+        (
+          ! defined blessed $$exception
+            or
+          ! $$exception->isa( 'DBIx::Class::Exception' )
+        )
+      ) {
+
+        ##################
+        ### FIXME - TODO
+        ##################
+
+      }
+      else {
+
+        # SUCH HIDEOUS, MUCH AUGH! (and double WOW on the s/// at the end below)
+        $rbe =~ s/ at .+? line \d+$//;
+
+        (
+          (
+            defined blessed $$exception
+              and
+            $$exception->isa( 'DBIx::Class::Exception' )
+          )
+            ? (
+              $$exception->{msg} =
+                "Transaction aborted: $$exception->{msg}. Rollback failed: $rbe"
+            )
+            : (
+              $$exception =
+                "Transaction aborted: $$exception. Rollback failed: $rbe"
+            )
+        ) =~ s/Transaction aborted: (?=Transaction aborted:)//;
+      }
+    }
+  };
+
+  return $rbe;
+}
+
 =head2 svp_begin
 
 Arguments: $savepoint_name?
@@ -434,10 +539,10 @@ shell environment.
 
 =head2 debugfh
 
-Set or retrieve the filehandle used for trace/debug output.  This should be
-an IO::Handle compatible object (only the C<print> method is used).  Initially
-set to be STDERR - although see information on the
-L<DBIC_TRACE> environment variable.
+An opportunistic proxy to L<< ->debugobj->debugfh(@_)
+|DBIx::Class::Storage::Statistics/debugfh >>
+If the currently set L</debugobj> does not have a L</debugfh> method, caling
+this is a no-op.
 
 =cut
 
@@ -471,9 +576,12 @@ sub debugobj {
       my @pp_args;
 
       if ($profile =~ /^\.?\//) {
-        require Config::Any;
 
-        my $cfg = try {
+        if ( my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('config_file_reader') ) {
+          $self->throw_exception("Unable to parse TRACE_PROFILE config file '$profile' without $missing");
+        }
+
+        my $cfg = dbic_internal_try {
           Config::Any->load_files({ files => [$profile], use_ext => 1 });
         } catch {
           # sanitize the error message a bit
@@ -499,7 +607,7 @@ sub debugobj {
       #
       # Yes I am aware this is fragile and TxnScopeGuard needs
       # a better fix. This is another yak to shave... :(
-      try {
+      dbic_internal_try {
         DBIx::Class::Storage::Debug::PrettyPrint->new(@pp_args);
       } catch {
         $self->throw_exception($_);
@@ -634,7 +742,6 @@ filename the file is read with L<Config::Any> and the results are
 used as the configuration for tracing.  See L<SQL::Abstract::Tree/new>
 for what that structure should look like.
 
-
 =head2 DBIX_CLASS_STORAGE_DBI_DEBUG
 
 Old name for DBIC_TRACE
@@ -644,13 +751,16 @@ Old name for DBIC_TRACE
 L<DBIx::Class::Storage::DBI> - reference storage implementation using
 SQL::Abstract and DBI.
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 8dae0c9..0f884da 100644 (file)
@@ -1,22 +1,16 @@
 package # hide from pause until we figure it all out
   DBIx::Class::Storage::BlockRunner;
 
+use warnings;
 use strict;
 
 use DBIx::Class::Exception;
 use DBIx::Class::Carp;
 use Context::Preserve 'preserve_context';
-use DBIx::Class::_Util 'is_exception';
+use DBIx::Class::_Util qw( is_exception qsub dbic_internal_try );
 use Scalar::Util qw(weaken blessed reftype);
 use Try::Tiny;
-
-# DO NOT edit away without talking to riba first, he will just put it back
-BEGIN {
-  local $ENV{PERL_STRICTURES_EXTRA} = 0;
-  require Moo; Moo->import;
-  require Sub::Quote; Sub::Quote->import('quote_sub');
-}
-use warnings NONFATAL => 'all';
+use Moo;
 use namespace::clean;
 
 =head1 NAME
@@ -43,16 +37,16 @@ has wrap_txn => (
 has retry_handler => (
   is => 'ro',
   required => 1,
-  isa => quote_sub( q{
+  isa => qsub q{
     (Scalar::Util::reftype($_[0])||'') eq 'CODE'
       or DBIx::Class::Exception->throw('retry_handler must be a CODE reference')
-  }),
+  },
 );
 
 has retry_debug => (
   is => 'rw',
   # use a sub - to be evaluated on the spot lazily
-  default => quote_sub( '$ENV{DBIC_STORAGE_RETRY_DEBUG}' ),
+  default => qsub '$ENV{DBIC_STORAGE_RETRY_DEBUG}',
   lazy => 1,
 );
 
@@ -67,19 +61,19 @@ has failed_attempt_count => (
   writer => '_set_failed_attempt_count',
   default => 0,
   lazy => 1,
-  trigger => quote_sub(q{
+  trigger => qsub q{
     $_[0]->throw_exception( sprintf (
       'Reached max_attempts amount of %d, latest exception: %s',
       $_[0]->max_attempts, $_[0]->last_exception
     )) if $_[0]->max_attempts <= ($_[1]||0);
-  }),
+  },
 );
 
 has exception_stack => (
   is => 'ro',
   init_arg => undef,
   clearer => '_reset_exception_stack',
-  default => quote_sub(q{ [] }),
+  default => qsub q{ [] },
   lazy => 1,
 );
 
@@ -128,7 +122,7 @@ sub _run {
   my $run_err = '';
 
   return preserve_context {
-    try {
+    dbic_internal_try {
       if (defined $txn_init_depth) {
         $self->storage->txn_begin;
         $txn_begin_ok = 1;
@@ -142,9 +136,14 @@ sub _run {
     my @res = @_;
 
     my $storage = $self->storage;
-    my $cur_depth = $storage->transaction_depth;
 
-    if (defined $txn_init_depth and $run_err eq '') {
+    if (
+      defined $txn_init_depth
+        and
+      ! is_exception $run_err
+        and
+      defined( my $cur_depth = $storage->transaction_depth )
+    ) {
       my $delta_txn = (1 + $txn_init_depth) - $cur_depth;
 
       if ($delta_txn) {
@@ -157,29 +156,23 @@ sub _run {
         ) unless $delta_txn == 1 and $cur_depth == 0;
       }
       else {
-        $run_err = eval { $storage->txn_commit; 1 } ? '' : $@;
+        dbic_internal_try {
+          $storage->txn_commit;
+          1;
+        }
+        catch {
+          $run_err = $_;
+        };
       }
     }
 
     # something above threw an error (could be the begin, the code or the commit)
     if ( is_exception $run_err ) {
 
-      # attempt a rollback if we did begin in the first place
-      if ($txn_begin_ok) {
-        # some DBDs go crazy if there is nothing to roll back on, perform a soft-check
-        my $rollback_exception = $storage->_seems_connected
-          ? (! eval { $storage->txn_rollback; 1 }) ? $@ : ''
-          : 'lost connection to storage'
-        ;
-
-        if ( $rollback_exception and (
-          ! defined blessed $rollback_exception
-            or
-          ! $rollback_exception->isa('DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION')
-        ) ) {
-          $run_err = "Transaction aborted: $run_err. Rollback failed: $rollback_exception";
-        }
-      }
+      # Attempt a rollback if we did begin in the first place
+      # Will append rollback error if possible
+      $storage->__delicate_rollback( \$run_err )
+        if $txn_begin_ok;
 
       push @{ $self->exception_stack }, $run_err;
 
@@ -219,13 +212,16 @@ sub _run {
   };
 }
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 9b5e3b0..54dff81 100644 (file)
@@ -10,11 +10,14 @@ use mro 'c3';
 use DBIx::Class::Carp;
 use Scalar::Util qw/refaddr weaken reftype blessed/;
 use List::Util qw/first/;
-use Sub::Name 'subname';
 use Context::Preserve 'preserve_context';
 use Try::Tiny;
-use overload ();
-use Data::Compare (); # no imports!!! guard against insane architecture
+use SQL::Abstract qw(is_plain_value is_literal_value);
+use DBIx::Class::_Util qw(
+  quote_sub perlstring serialize
+  dbic_internal_try
+  detected_reinvoked_destructor scope_guard
+);
 use namespace::clean;
 
 # default cursor class, overridable in connect_info attributes
@@ -101,12 +104,13 @@ for my $meth (keys %$storage_accessor_idx, qw(
   txn_begin
 
   insert
-  insert_bulk
   update
   delete
   select
   select_single
 
+  _insert_bulk
+
   with_deferred_fk_checks
 
   get_use_dbms_capability
@@ -119,10 +123,16 @@ for my $meth (keys %$storage_accessor_idx, qw(
   my $orig = __PACKAGE__->can ($meth)
     or die "$meth is not a ::Storage::DBI method!";
 
-  no strict 'refs';
-  no warnings 'redefine';
-  *{__PACKAGE__ ."::$meth"} = subname $meth => sub {
+  my $possibly_a_setter = $storage_accessor_idx->{$meth} ? 1 : 0;
+
+  quote_sub
+    __PACKAGE__ ."::$meth", sprintf( <<'EOC', $possibly_a_setter, perlstring $meth ), { '$orig' => \$orig };
+
     if (
+      # if this is an actual *setter* - just set it, no need to connect
+      # and determine the driver
+      !( %1$s and @_ > 1 )
+        and
       # only fire when invoked on an instance, a valid class-based invocation
       # would e.g. be setting a default for an inherited accessor
       ref $_[0]
@@ -131,10 +141,6 @@ for my $meth (keys %$storage_accessor_idx, qw(
         and
       ! $_[0]->{_in_determine_driver}
         and
-      # if this is a known *setter* - just set it, no need to connect
-      # and determine the driver
-      ! ( $storage_accessor_idx->{$meth} and @_ > 1 )
-        and
       # Only try to determine stuff if we have *something* that either is or can
       # provide a DSN. Allows for bare $schema's generated with a plain ->connect()
       # to still be marginally useful
@@ -142,16 +148,15 @@ for my $meth (keys %$storage_accessor_idx, qw(
     ) {
       $_[0]->_determine_driver;
 
-      # This for some reason crashes and burns on perl 5.8.1
-      # IFF the method ends up throwing an exception
-      #goto $_[0]->can ($meth);
+      # work around http://rt.perl.org/rt3/Public/Bug/Display.html?id=35878
+      goto $_[0]->can(%2$s) unless DBIx::Class::_ENV_::BROKEN_GOTO;
 
-      my $cref = $_[0]->can ($meth);
+      my $cref = $_[0]->can(%2$s);
       goto $cref;
     }
 
     goto $orig;
-  };
+EOC
 }
 
 =head1 NAME
@@ -220,16 +225,30 @@ sub new {
     weaken (
       $seek_and_destroy{ refaddr($_[0]) } = $_[0]
     );
+
+    # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+    # collected before leaving this scope. Depending on the code above, this
+    # may very well be just a preventive measure guarding future modifications
+    undef;
   }
 
   END {
-    local $?; # just in case the DBI destructor changes it somehow
 
-    # destroy just the object if not native to this process
-    $_->_verify_pid for (grep
-      { defined $_ }
-      values %seek_and_destroy
-    );
+    if(
+      ! DBIx::Class::_ENV_::BROKEN_FORK
+        and
+      my @instances = grep { defined $_ } values %seek_and_destroy
+    ) {
+      local $?; # just in case the DBI destructor changes it somehow
+
+      # disarm the handle if not native to this process (see comment on top)
+      $_->_verify_pid for @instances;
+    }
+
+    # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+    # collected before leaving this scope. Depending on the code above, this
+    # may very well be just a preventive measure guarding future modifications
+    undef;
   }
 
   sub CLONE {
@@ -241,44 +260,52 @@ sub new {
 
     for (@instances) {
       $_->_dbh(undef);
-
-      $_->transaction_depth(0);
-      $_->savepoints([]);
+      $_->disconnect;
 
       # properly renumber existing refs
       $_->_arm_global_destructor
     }
+
+    # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+    # collected before leaving this scope. Depending on the code above, this
+    # may very well be just a preventive measure guarding future modifications
+    undef;
   }
 }
 
 sub DESTROY {
-  my $self = shift;
+  return if &detected_reinvoked_destructor;
+
+  $_[0]->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
 
   # some databases spew warnings on implicit disconnect
-  $self->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
+  return unless defined $_[0]->_dbh;
+
   local $SIG{__WARN__} = sub {};
-  $self->_dbh(undef);
+  $_[0]->_dbh(undef);
+  # not calling ->disconnect here - we are being destroyed - nothing to reset
 
-  # this op is necessary, since the very last perl runtime statement
-  # triggers a global destruction shootout, and the $SIG localization
-  # may very well be destroyed before perl actually gets to do the
-  # $dbh undef
-  1;
+  # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+  # collected before leaving this scope. Depending on the code above, this
+  # may very well be just a preventive measure guarding future modifications
+  undef;
 }
 
 # handle pid changes correctly - do not destroy parent's connection
 sub _verify_pid {
-  my $self = shift;
 
-  my $pid = $self->_conn_pid;
-  if( defined $pid and $pid != $$ and my $dbh = $self->_dbh ) {
+  my $pid = $_[0]->_conn_pid;
+
+  if( defined $pid and $pid != $$ and my $dbh = $_[0]->_dbh ) {
     $dbh->{InactiveDestroy} = 1;
-    $self->_dbh(undef);
-    $self->transaction_depth(0);
-    $self->savepoints([]);
+    $_[0]->_dbh(undef);
+    $_[0]->disconnect;
   }
 
-  return;
+  # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+  # collected before leaving this scope. Depending on the code above, this
+  # may very well be just a preventive measure guarding future modifications
+  undef;
 }
 
 =head2 connect_info
@@ -869,23 +896,43 @@ database is not in C<AutoCommit> mode.
 =cut
 
 sub disconnect {
-  my ($self) = @_;
+  my $self = shift;
 
-  if( $self->_dbh ) {
-    my @actions;
+  # this physical disconnect below might very well throw
+  # in order to unambiguously reset the state - do the cleanup in guard
+
+  my $g = scope_guard {
+
+    {
+      local $@ if DBIx::Class::_ENV_::UNSTABLE_DOLLARAT;
+      eval { $self->_dbh->disconnect };
+    }
+
+    $self->_dbh(undef);
+    $self->_dbh_details({});
+    $self->transaction_depth(undef);
+    $self->_dbh_autocommit(undef);
+    $self->savepoints([]);
+
+    # FIXME - this needs reenabling with the proper "no reset on same DSN" check
+    #$self->_sql_maker(undef); # this may also end up being different
+  };
 
-    push @actions, ( $self->on_disconnect_call || () );
-    push @actions, $self->_parse_connect_do ('on_disconnect_do');
+  if( $self->_dbh ) {
 
-    $self->_do_connection_actions(disconnect_call_ => $_) for @actions;
+    $self->_do_connection_actions(disconnect_call_ => $_) for (
+      ( $self->on_disconnect_call || () ),
+      $self->_parse_connect_do ('on_disconnect_do')
+    );
 
     # stops the "implicit rollback on disconnect" warning
     $self->_exec_txn_rollback unless $self->_dbh_autocommit;
-
-    %{ $self->_dbh->{CachedKids} } = ();
-    $self->_dbh->disconnect;
-    $self->_dbh(undef);
   }
+
+  # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+  # collected before leaving this scope. Depending on the code above, this
+  # may very well be just a preventive measure guarding future modifications
+  undef;
 }
 
 =head2 with_deferred_fk_checks
@@ -905,8 +952,8 @@ in MySQL's case disabled entirely.
 
 # Storage subclasses should override this
 sub with_deferred_fk_checks {
-  my ($self, $sub) = @_;
-  $sub->();
+  #my ($self, $sub) = @_;
+  $_[1]->();
 }
 
 =head2 connected
@@ -926,40 +973,34 @@ answering, etc.) This method is used internally by L</dbh>.
 =cut
 
 sub connected {
-  my $self = shift;
-  return 0 unless $self->_seems_connected;
+  return 0 unless $_[0]->_seems_connected;
 
   #be on the safe side
-  local $self->_dbh->{RaiseError} = 1;
+  local $_[0]->_dbh->{RaiseError} = 1;
 
-  return $self->_ping;
+  return $_[0]->_ping;
 }
 
 sub _seems_connected {
-  my $self = shift;
-
-  $self->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
+  $_[0]->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
 
-  my $dbh = $self->_dbh
-    or return 0;
+  $_[0]->_dbh
+    and
+  $_[0]->_dbh->FETCH('Active')
+    and
+  return 1;
 
-  return $dbh->FETCH('Active');
+  # explicitly reset all state
+  $_[0]->disconnect;
+  return 0;
 }
 
 sub _ping {
-  my $self = shift;
-
-  my $dbh = $self->_dbh or return 0;
-
-  return $dbh->ping;
+  ($_[0]->_dbh || return 0)->ping;
 }
 
 sub ensure_connected {
-  my ($self) = @_;
-
-  unless ($self->connected) {
-    $self->_populate_dbh;
-  }
+  $_[0]->connected || ( $_[0]->_populate_dbh && 1 );
 }
 
 =head2 dbh
@@ -973,26 +1014,26 @@ instead.
 =cut
 
 sub dbh {
-  my ($self) = @_;
-
-  if (not $self->_dbh) {
-    $self->_populate_dbh;
-  } else {
-    $self->ensure_connected;
-  }
-  return $self->_dbh;
+  # maybe save a ping call
+  $_[0]->_dbh
+    ? ( $_[0]->ensure_connected and $_[0]->_dbh )
+    : $_[0]->_populate_dbh
+  ;
 }
 
 # this is the internal "get dbh or connect (don't check)" method
 sub _get_dbh {
-  my $self = shift;
-  $self->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
-  $self->_populate_dbh unless $self->_dbh;
-  return $self->_dbh;
+  $_[0]->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
+  $_[0]->_dbh || $_[0]->_populate_dbh;
 }
 
+# *DELIBERATELY* not a setter (for the time being)
+# Too intertwined with everything else for any kind of sanity
 sub sql_maker {
-  my ($self) = @_;
+  my $self = shift;
+
+  $self->throw_exception('sql_maker() is not a setter method') if @_;
+
   unless ($self->_sql_maker) {
     my $sql_maker_class = $self->sql_maker_class;
 
@@ -1052,32 +1093,32 @@ sub _rebless {}
 sub _init {}
 
 sub _populate_dbh {
-  my ($self) = @_;
 
-  $self->_dbh(undef); # in case ->connected failed we might get sent here
-  $self->_dbh_details({}); # reset everything we know
+  # reset internal states
+  # also in case ->connected failed we might get sent here
+  $_[0]->disconnect;
 
-  $self->_dbh($self->_connect);
+  $_[0]->_dbh($_[0]->_connect);
 
-  $self->_conn_pid($$) unless DBIx::Class::_ENV_::BROKEN_FORK; # on win32 these are in fact threads
+  $_[0]->_conn_pid($$) unless DBIx::Class::_ENV_::BROKEN_FORK; # on win32 these are in fact threads
 
-  $self->_determine_driver;
+  $_[0]->_determine_driver;
 
   # Always set the transaction depth on connect, since
   #  there is no transaction in progress by definition
-  $self->{transaction_depth} = $self->_dbh_autocommit ? 0 : 1;
+  $_[0]->transaction_depth( $_[0]->_dbh_autocommit ? 0 : 1 );
+
+  $_[0]->_run_connection_actions unless $_[0]->{_in_determine_driver};
 
-  $self->_run_connection_actions unless $self->{_in_determine_driver};
+  $_[0]->_dbh;
 }
 
 sub _run_connection_actions {
-  my $self = shift;
-  my @actions;
-
-  push @actions, ( $self->on_connect_call || () );
-  push @actions, $self->_parse_connect_do ('on_connect_do');
 
-  $self->_do_connection_actions(connect_call_ => $_) for @actions;
+  $_[0]->_do_connection_actions(connect_call_ => $_) for (
+    ( $_[0]->on_connect_call || () ),
+    $_[0]->_parse_connect_do ('on_connect_do'),
+  );
 }
 
 
@@ -1122,12 +1163,18 @@ sub get_dbms_capability {
 sub _server_info {
   my $self = shift;
 
-  my $info;
-  unless ($info = $self->_dbh_details->{info}) {
+  # FIXME - ideally this needs to be an ||= assignment, and the final
+  # assignment at the end of this do{} should be gone entirely. However
+  # this confuses CXSA: https://rt.cpan.org/Ticket/Display.html?id=103296
+  $self->_dbh_details->{info} || do {
 
-    $info = {};
+    # this guarantees that problematic conninfo won't be hidden
+    # by the try{} below
+    $self->ensure_connected;
+
+    my $info = {};
 
-    my $server_version = try {
+    my $server_version = dbic_internal_try {
       $self->_get_server_version
     } catch {
       # driver determination *may* use this codepath
@@ -1162,9 +1209,7 @@ sub _server_info {
     }
 
     $self->_dbh_details->{info} = $info;
-  }
-
-  return $info;
+  };
 }
 
 sub _get_server_version {
@@ -1190,7 +1235,7 @@ sub _describe_connection {
   my $self = shift;
 
   my $drv;
-  try {
+  dbic_internal_try {
     $drv = $self->_extract_driver_from_connect_info;
     $self->ensure_connected;
   };
@@ -1204,7 +1249,7 @@ sub _describe_connection {
     DBIC_DRIVER => ref $self,
     $drv ? (
       DBD => $drv,
-      DBD_VER => try { $drv->VERSION },
+      DBD_VER => dbic_internal_try { $drv->VERSION },
     ) : (),
   };
 
@@ -1245,7 +1290,7 @@ sub _describe_connection {
   ) {
     # some drivers barf on things they do not know about instead
     # of returning undef
-    my $v = try { $self->_dbh_get_info($inf) };
+    my $v = dbic_internal_try { $self->_dbh_get_info($inf) };
     next unless defined $v;
 
     #my $key = sprintf( '%s(%s)', $inf, $DBI::Const::GetInfoType::GetInfoType{$inf} );
@@ -1304,7 +1349,7 @@ sub _determine_driver {
         "Your storage subclass @{[ ref $self ]} provides (or inherits) the method "
       . 'source_bind_attributes() for which support has been removed as of Jan 2013. '
       . 'If you are not sure how to proceed please contact the development team via '
-      . 'http://search.cpan.org/dist/DBIx-Class/lib/DBIx/Class.pm#GETTING_HELP/SUPPORT'
+      . DBIx::Class::_ENV_::HELP_URL
       );
     }
 
@@ -1343,7 +1388,7 @@ sub _extract_driver_from_connect_info {
 sub _determine_connector_driver {
   my ($self, $conn) = @_;
 
-  my $dbtype = $self->_dbh_get_info('SQL_DBMS_NAME');
+  my $dbtype = $self->_get_rdbms_name;
 
   if (not $dbtype) {
     $self->_warn_undetermined_driver(
@@ -1370,6 +1415,8 @@ sub _determine_connector_driver {
   }
 }
 
+sub _get_rdbms_name { shift->_dbh_get_info('SQL_DBMS_NAME') }
+
 sub _warn_undetermined_driver {
   my ($self, $msg) = @_;
 
@@ -1383,24 +1430,41 @@ sub _warn_undetermined_driver {
 }
 
 sub _do_connection_actions {
-  my $self          = shift;
-  my $method_prefix = shift;
-  my $call          = shift;
-
-  if (not ref($call)) {
-    my $method = $method_prefix . $call;
-    $self->$method(@_);
-  } elsif (ref($call) eq 'CODE') {
-    $self->$call(@_);
-  } elsif (ref($call) eq 'ARRAY') {
-    if (ref($call->[0]) ne 'ARRAY') {
-      $self->_do_connection_actions($method_prefix, $_) for @$call;
-    } else {
-      $self->_do_connection_actions($method_prefix, @$_) for @$call;
+  my ($self, $method_prefix, $call, @args) = @_;
+
+  dbic_internal_try {
+    if (not ref($call)) {
+      my $method = $method_prefix . $call;
+      $self->$method(@args);
+    }
+    elsif (ref($call) eq 'CODE') {
+      $self->$call(@args);
+    }
+    elsif (ref($call) eq 'ARRAY') {
+      if (ref($call->[0]) ne 'ARRAY') {
+        $self->_do_connection_actions($method_prefix, $_) for @$call;
+      }
+      else {
+        $self->_do_connection_actions($method_prefix, @$_) for @$call;
+      }
+    }
+    else {
+      $self->throw_exception (sprintf ("Don't know how to process conection actions of type '%s'", ref($call)) );
     }
-  } else {
-    $self->throw_exception (sprintf ("Don't know how to process conection actions of type '%s'", ref($call)) );
   }
+  catch {
+    if ( $method_prefix =~ /^connect/ ) {
+      # this is an on_connect cycle - we can't just throw while leaving
+      # a handle in an undefined state in our storage object
+      # kill it with fire and rethrow
+      $self->_dbh(undef);
+      $self->disconnect;  # the $dbh is gone, but we still need to reset the rest
+      $self->throw_exception( $_[0] );
+    }
+    else {
+      carp "Disconnect action failed: $_[0]";
+    }
+  };
 
   return $self;
 }
@@ -1415,7 +1479,19 @@ sub disconnect_call_do_sql {
   $self->_do_query(@_);
 }
 
-# override in db-specific backend when necessary
+=head2 connect_call_datetime_setup
+
+A no-op stub method, provided so that one can always safely supply the
+L<connection option|/DBIx::Class specific connection attributes>
+
+ on_connect_call => 'datetime_setup'
+
+This way one does not need to know in advance whether the underlying
+storage requires any sort of hand-holding when dealing with calendar
+data.
+
+=cut
+
 sub connect_call_datetime_setup { 1 }
 
 sub _do_query {
@@ -1480,7 +1556,7 @@ sub _connect {
     }, '__DBIC__DBH__ERROR__HANDLER__';
   };
 
-  try {
+  dbic_internal_try {
     if(ref $info->[0] eq 'CODE') {
       $dbh = $info->[0]->();
     }
@@ -1535,19 +1611,17 @@ sub _connect {
 }
 
 sub txn_begin {
-  my $self = shift;
-
   # this means we have not yet connected and do not know the AC status
   # (e.g. coderef $dbh), need a full-fledged connection check
-  if (! defined $self->_dbh_autocommit) {
-    $self->ensure_connected;
+  if (! defined $_[0]->_dbh_autocommit) {
+    $_[0]->ensure_connected;
   }
   # Otherwise simply connect or re-connect on pid changes
   else {
-    $self->_get_dbh;
+    $_[0]->_get_dbh;
   }
 
-  $self->next::method(@_);
+  shift->next::method(@_);
 }
 
 sub _exec_txn_begin {
@@ -1568,9 +1642,8 @@ sub _exec_txn_begin {
 sub txn_commit {
   my $self = shift;
 
-  $self->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
   $self->throw_exception("Unable to txn_commit() on a disconnected storage")
-    unless $self->_dbh;
+    unless $self->_seems_connected;
 
   # esoteric case for folks using external $dbh handles
   if (! $self->transaction_depth and ! $self->_dbh->FETCH('AutoCommit') ) {
@@ -1599,9 +1672,10 @@ sub _exec_txn_commit {
 sub txn_rollback {
   my $self = shift;
 
-  $self->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
-  $self->throw_exception("Unable to txn_rollback() on a disconnected storage")
-    unless $self->_dbh;
+  # do a minimal connectivity check due to weird shit like
+  # https://rt.cpan.org/Public/Bug/Display.html?id=62370
+  $self->throw_exception("lost connection to storage")
+    unless $self->_seems_connected;
 
   # esoteric case for folks using external $dbh handles
   if (! $self->transaction_depth and ! $self->_dbh->FETCH('AutoCommit') ) {
@@ -1627,17 +1701,12 @@ sub _exec_txn_rollback {
   shift->_dbh->rollback;
 }
 
-# generate some identical methods
-for my $meth (qw/svp_begin svp_release svp_rollback/) {
-  no strict qw/refs/;
-  *{__PACKAGE__ ."::$meth"} = subname $meth => sub {
-    my $self = shift;
-    $self->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
-    $self->throw_exception("Unable to $meth() on a disconnected storage")
-      unless $self->_dbh;
-    $self->next::method(@_);
-  };
-}
+# generate the DBI-specific stubs, which then fallback to ::Storage proper
+quote_sub __PACKAGE__ . "::$_" => sprintf (<<'EOS', $_) for qw(svp_begin svp_release svp_rollback);
+  $_[0]->throw_exception('Unable to %s() on a disconnected storage')
+    unless $_[0]->_seems_connected;
+  shift->next::method(@_);
+EOS
 
 # This used to be the top-half of _execute.  It was split out to make it
 #  easier to override in NoBindVars without duping the rest.  It takes up
@@ -1678,8 +1747,8 @@ sub _gen_sql_bind {
   ) {
     carp_unique 'DateTime objects passed to search() are not supported '
       . 'properly (InflateColumn::DateTime formats and settings are not '
-      . 'respected.) See "Formatting DateTime objects in queries" in '
-      . 'DBIx::Class::Manual::Cookbook. To disable this warning for good '
+      . 'respected.) See ".. format a DateTime object for searching?" in '
+      . 'DBIx::Class::Manual::FAQ. To disable this warning for good '
       . 'set $ENV{DBIC_DT_SEARCH_OK} to true'
   }
 
@@ -1689,13 +1758,10 @@ sub _gen_sql_bind {
 sub _resolve_bindattrs {
   my ($self, $ident, $bind, $colinfos) = @_;
 
-  $colinfos ||= {};
-
   my $resolve_bindinfo = sub {
     #my $infohash = shift;
 
-    %$colinfos = %{ $self->_resolve_column_info($ident) }
-      unless keys %$colinfos;
+    $colinfos ||= { %{ $self->_resolve_column_info($ident) } };
 
     my $ret;
     if (my $col = $_[0]->{dbic_colname}) {
@@ -1712,42 +1778,22 @@ sub _resolve_bindattrs {
   };
 
   return [ map {
-    my $resolved =
       ( ref $_ ne 'ARRAY' or @$_ != 2 ) ? [ {}, $_ ]
     : ( ! defined $_->[0] )             ? [ {}, $_->[1] ]
-    : (ref $_->[0] eq 'HASH')           ? [ (exists $_->[0]{dbd_attrs} or $_->[0]{sqlt_datatype})
-                                              ? $_->[0]
-                                              : $resolve_bindinfo->($_->[0])
-                                            , $_->[1] ]
+    : (ref $_->[0] eq 'HASH')           ? [(
+                                            ! keys %{$_->[0]}
+                                              or
+                                            exists $_->[0]{dbd_attrs}
+                                              or
+                                            $_->[0]{sqlt_datatype}
+                                           ) ? $_->[0]
+                                             : $resolve_bindinfo->($_->[0])
+                                           , $_->[1]
+                                          ]
     : (ref $_->[0] eq 'SCALAR')         ? [ { sqlt_datatype => ${$_->[0]} }, $_->[1] ]
     :                                     [ $resolve_bindinfo->(
                                               { dbic_colname => $_->[0] }
                                             ), $_->[1] ]
-    ;
-
-    if (
-      ! exists $resolved->[0]{dbd_attrs}
-        and
-      ! $resolved->[0]{sqlt_datatype}
-        and
-      length ref $resolved->[1]
-        and
-      ! overload::Method($resolved->[1], '""')
-    ) {
-      require Data::Dumper;
-      local $Data::Dumper::Maxdepth = 1;
-      local $Data::Dumper::Terse = 1;
-      local $Data::Dumper::Useqq = 1;
-      local $Data::Dumper::Indent = 0;
-      local $Data::Dumper::Pad = ' ';
-      $self->throw_exception(
-        'You must supply a datatype/bindtype (see DBIx::Class::ResultSet/DBIC BIND VALUES) '
-      . 'for non-scalar value '. Data::Dumper::Dumper ($resolved->[1])
-      );
-    }
-
-    $resolved;
-
   } @$bind ];
 }
 
@@ -1781,31 +1827,28 @@ sub _query_end {
 }
 
 sub _dbi_attrs_for_bind {
-  my ($self, $ident, $bind) = @_;
+  #my ($self, $ident, $bind) = @_;
 
-  my @attrs;
+  return [ map {
 
-  for (map { $_->[0] } @$bind) {
-    push @attrs, do {
-      if (exists $_->{dbd_attrs}) {
-        $_->{dbd_attrs}
-      }
-      elsif($_->{sqlt_datatype}) {
-        # cache the result in the dbh_details hash, as it can not change unless
-        # we connect to something else
-        my $cache = $self->_dbh_details->{_datatype_map_cache} ||= {};
-        if (not exists $cache->{$_->{sqlt_datatype}}) {
-          $cache->{$_->{sqlt_datatype}} = $self->bind_attribute_by_data_type($_->{sqlt_datatype}) || undef;
-        }
-        $cache->{$_->{sqlt_datatype}};
-      }
-      else {
-        undef;  # always push something at this position
-      }
-    }
-  }
+    exists $_->{dbd_attrs}  ?  $_->{dbd_attrs}
+
+  : ! $_->{sqlt_datatype}   ? undef
+
+  :                           do {
 
-  return \@attrs;
+    # cache the result in the dbh_details hash, as it (usually) can not change
+    # unless we connect to something else
+    # FIXME: for the time being Oracle is an exception, pending a rewrite of
+    # the LOB storage
+    my $cache = $_[0]->_dbh_details->{_datatype_map_cache} ||= {};
+
+    $cache->{$_->{sqlt_datatype}} = $_[0]->bind_attribute_by_data_type($_->{sqlt_datatype})
+      if ! exists $cache->{$_->{sqlt_datatype}};
+
+    $cache->{$_->{sqlt_datatype}};
+
+  } } map { $_->[0] } @{$_[2]} ];
 }
 
 sub _execute {
@@ -1886,14 +1929,15 @@ sub _bind_sth_params {
       );
     }
     else {
-      # FIXME SUBOPTIMAL - most likely this is not necessary at all
-      # confirm with dbi-dev whether explicit stringification is needed
-      my $v = ( length ref $bind->[$i][1] and overload::Method($bind->[$i][1], '""') )
+      # FIXME SUBOPTIMAL - DBI needs fixing to always stringify regardless of DBD
+      my $v = ( length ref $bind->[$i][1] and is_plain_value $bind->[$i][1] )
         ? "$bind->[$i][1]"
         : $bind->[$i][1]
       ;
+
       $sth->bind_param(
         $i + 1,
+        # The temp-var is CRUCIAL - DO NOT REMOVE IT, breaks older DBD::SQLite RT#79576
         $v,
         $bind_attrs->[$i],
       );
@@ -1914,9 +1958,7 @@ sub _prefetch_autovalues {
       (
         ! exists $to_insert->{$col}
           or
-        ref $to_insert->{$col} eq 'SCALAR'
-          or
-        (ref $to_insert->{$col} eq 'REF' and ref ${$to_insert->{$col}} eq 'ARRAY')
+        is_literal_value($to_insert->{$col})
       )
     ) {
       $values{$col} = $self->_sequence_fetch(
@@ -1953,11 +1995,9 @@ sub insert {
     }
 
     # nothing to retrieve when explicit values are supplied
-    next if (defined $to_insert->{$col} and ! (
-      ref $to_insert->{$col} eq 'SCALAR'
-        or
-      (ref $to_insert->{$col} eq 'REF' and ref ${$to_insert->{$col}} eq 'ARRAY')
-    ));
+    next if (
+      defined $to_insert->{$col} and ! is_literal_value($to_insert->{$col})
+    );
 
     # the 'scalar keys' is a trick to preserve the ->columns declaration order
     $retrieve_cols{$col} = scalar keys %retrieve_cols if (
@@ -1984,12 +2024,30 @@ sub insert {
 
   my %returned_cols = %$to_insert;
   if (my $retlist = $sqla_opts->{returning}) {  # if IR is supported - we will get everything in one set
-    @ir_container = try {
-      local $SIG{__WARN__} = sub {};
-      my @r = $sth->fetchrow_array;
-      $sth->finish;
-      @r;
-    } unless @ir_container;
+
+    unless( @ir_container ) {
+      dbic_internal_try {
+
+        # FIXME - need to investigate why Caelum silenced this in 4d4dc518
+        local $SIG{__WARN__} = sub {};
+
+        @ir_container = $sth->fetchrow_array;
+        $sth->finish;
+
+      } catch {
+        # Evict the $sth from the cache in case we got here, since the finish()
+        # is crucial, at least on older Firebirds, possibly on other engines too
+        #
+        # It would be too complex to make this a proper subclass override,
+        # and besides we already take the try{} penalty, adding a catch that
+        # triggers infrequently is a no-brainer
+        #
+        if( my $kids = $self->_dbh->{CachedKids} ) {
+          $kids->{$_} == $sth and delete $kids->{$_}
+            for keys %$kids
+        }
+      };
+    }
 
     @returned_cols{@$retlist} = @ir_container if @ir_container;
   }
@@ -2033,26 +2091,28 @@ sub insert {
 }
 
 sub insert_bulk {
-  my ($self, $source, $cols, $data) = @_;
+  carp_unique(
+    'insert_bulk() should have never been exposed as a public method and '
+  . 'calling it is depecated as of Aug 2014. If you believe having a genuine '
+  . 'use for this method please contact the development team via '
+  . DBIx::Class::_ENV_::HELP_URL
+  );
 
-  my @col_range = (0..$#$cols);
+  return '0E0' unless @{$_[3]||[]};
 
-  # FIXME SUBOPTIMAL - most likely this is not necessary at all
-  # confirm with dbi-dev whether explicit stringification is needed
-  #
-  # forcibly stringify whatever is stringifiable
-  # ResultSet::populate() hands us a copy - safe to mangle
-  for my $r (0 .. $#$data) {
-    for my $c (0 .. $#{$data->[$r]}) {
-      $data->[$r][$c] = "$data->[$r][$c]"
-        if ( length ref $data->[$r][$c] and overload::Method($data->[$r][$c], '""') );
-    }
-  }
+  shift->_insert_bulk(@_);
+}
+
+sub _insert_bulk {
+  my ($self, $source, $cols, $data) = @_;
+
+  $self->throw_exception('Calling _insert_bulk without a dataset to process makes no sense')
+    unless @{$data||[]};
 
   my $colinfos = $source->columns_info($cols);
 
   local $self->{_autoinc_supplied_for_op} =
-    (first { $_->{is_auto_increment} } values %$colinfos)
+    (grep { $_->{is_auto_increment} } values %$colinfos)
       ? 1
       : 0
   ;
@@ -2078,17 +2138,17 @@ sub insert_bulk {
   # can't just hand SQLA a set of some known "values" (e.g. hashrefs that
   # can be later matched up by address), because we want to supply a real
   # value on which perhaps e.g. datatype checks will be performed
-  my ($proto_data, $value_type_by_col_idx);
-  for my $i (@col_range) {
-    my $colname = $cols->[$i];
-    if (ref $data->[0][$i] eq 'SCALAR') {
+  my ($proto_data, $serialized_bind_type_by_col_idx);
+  for my $col_idx (0..$#$cols) {
+    my $colname = $cols->[$col_idx];
+    if (ref $data->[0][$col_idx] eq 'SCALAR') {
       # no bind value at all - no type
 
-      $proto_data->{$colname} = $data->[0][$i];
+      $proto_data->{$colname} = $data->[0][$col_idx];
     }
-    elsif (ref $data->[0][$i] eq 'REF' and ref ${$data->[0][$i]} eq 'ARRAY' ) {
+    elsif (ref $data->[0][$col_idx] eq 'REF' and ref ${$data->[0][$col_idx]} eq 'ARRAY' ) {
       # repack, so we don't end up mangling the original \[]
-      my ($sql, @bind) = @${$data->[0][$i]};
+      my ($sql, @bind) = @${$data->[0][$col_idx]};
 
       # normalization of user supplied stuff
       my $resolved_bind = $self->_resolve_bindattrs(
@@ -2097,23 +2157,23 @@ sub insert_bulk {
 
       # store value-less (attrs only) bind info - we will be comparing all
       # supplied binds against this for sanity
-      $value_type_by_col_idx->{$i} = [ map { $_->[0] } @$resolved_bind ];
+      $serialized_bind_type_by_col_idx->{$col_idx} = serialize [ map { $_->[0] } @$resolved_bind ];
 
       $proto_data->{$colname} = \[ $sql, map { [
         # inject slice order to use for $proto_bind construction
-          { %{$resolved_bind->[$_][0]}, _bind_data_slice_idx => $i, _literal_bind_subindex => $_+1 }
+          { %{$resolved_bind->[$_][0]}, _bind_data_slice_idx => $col_idx, _literal_bind_subindex => $_+1 }
             =>
           $resolved_bind->[$_][1]
         ] } (0 .. $#bind)
       ];
     }
     else {
-      $value_type_by_col_idx->{$i} = undef;
+      $serialized_bind_type_by_col_idx->{$col_idx} = undef;
 
       $proto_data->{$colname} = \[ '?', [
-        { dbic_colname => $colname, _bind_data_slice_idx => $i }
+        { dbic_colname => $colname, _bind_data_slice_idx => $col_idx }
           =>
-        $data->[0][$i]
+        $data->[0][$col_idx]
       ] ];
     }
   }
@@ -2124,11 +2184,11 @@ sub insert_bulk {
     [ $proto_data ],
   );
 
-  if (! @$proto_bind and keys %$value_type_by_col_idx) {
+  if (! @$proto_bind and keys %$serialized_bind_type_by_col_idx) {
     # if the bindlist is empty and we had some dynamic binds, this means the
     # storage ate them away (e.g. the NoBindVars component) and interpolated
     # them directly into the SQL. This obviously can't be good for multi-inserts
-    $self->throw_exception('Cannot insert_bulk without support for placeholders');
+    $self->throw_exception('Unable to invoke fast-path insert without storage placeholder support');
   }
 
   # sanity checks
@@ -2146,19 +2206,19 @@ sub insert_bulk {
         Data::Dumper::Concise::Dumper ({
           map { $cols->[$_] =>
             $data->[$r_idx][$_]
-          } @col_range
+          } 0..$#$cols
         }),
       }
     );
   };
 
-  for my $col_idx (@col_range) {
+  for my $col_idx (0..$#$cols) {
     my $reference_val = $data->[0][$col_idx];
 
     for my $row_idx (1..$#$data) {  # we are comparing against what we got from [0] above, hence start from 1
       my $val = $data->[$row_idx][$col_idx];
 
-      if (! exists $value_type_by_col_idx->{$col_idx}) { # literal no binds
+      if (! exists $serialized_bind_type_by_col_idx->{$col_idx}) { # literal no binds
         if (ref $val ne 'SCALAR') {
           $bad_slice_report_cref->(
             "Incorrect value (expecting SCALAR-ref \\'$$reference_val')",
@@ -2174,8 +2234,8 @@ sub insert_bulk {
           );
         }
       }
-      elsif (! defined $value_type_by_col_idx->{$col_idx} ) {  # regular non-literal value
-        if (ref $val eq 'SCALAR' or (ref $val eq 'REF' and ref $$val eq 'ARRAY') ) {
+      elsif (! defined $serialized_bind_type_by_col_idx->{$col_idx} ) {  # regular non-literal value
+        if (is_literal_value($val)) {
           $bad_slice_report_cref->("Literal SQL found where a plain bind value is expected", $row_idx, $col_idx);
         }
       }
@@ -2202,16 +2262,17 @@ sub insert_bulk {
           }
           # need to check the bind attrs - a bind will happen only once for
           # the entire dataset, so any changes further down will be ignored.
-          elsif (! Data::Compare::Compare(
-            $value_type_by_col_idx->{$col_idx},
-            [
+          elsif (
+            $serialized_bind_type_by_col_idx->{$col_idx}
+              ne
+            serialize [
               map
               { $_->[0] }
               @{$self->_resolve_bindattrs(
                 $source, [ @{$$val}[1 .. $#$$val] ], $colinfos,
               )}
-            ],
-          )) {
+            ]
+          ) {
             $bad_slice_report_cref->(
               'Differing bind attributes on literal/bind values not supported',
               $row_idx,
@@ -2228,7 +2289,7 @@ sub insert_bulk {
   # scope guard
   my $guard = $self->txn_scope_guard;
 
-  $self->_query_start( $sql, @$proto_bind ? [[undef => '__BULK_INSERT__' ]] : () );
+  $self->_query_start( $sql, @$proto_bind ? [[ {} => '__BULK_INSERT__' ]] : () );
   my $sth = $self->_prepare_sth($self->_dbh, $sql);
   my $rv = do {
     if (@$proto_bind) {
@@ -2242,7 +2303,7 @@ sub insert_bulk {
     }
   };
 
-  $self->_query_end( $sql, @$proto_bind ? [[ undef => '__BULK_INSERT__' ]] : () );
+  $self->_query_end( $sql, @$proto_bind ? [[ {} => '__BULK_INSERT__' ]] : () );
 
   $guard->commit;
 
@@ -2256,16 +2317,13 @@ sub insert_bulk {
 sub _dbh_execute_for_fetch {
   my ($self, $source, $sth, $proto_bind, $cols, $data) = @_;
 
-  my @idx_range = ( 0 .. $#$proto_bind );
-
   # If we have any bind attributes to take care of, we will bind the
   # proto-bind data (which will never be used by execute_for_fetch)
   # However since column bindtypes are "sticky", this is sufficient
   # to get the DBD to apply the bindtype to all values later on
-
   my $bind_attrs = $self->_dbi_attrs_for_bind($source, $proto_bind);
 
-  for my $i (@idx_range) {
+  for my $i (0 .. $#$proto_bind) {
     $sth->bind_param (
       $i+1, # DBI bind indexes are 1-based
       $proto_bind->[$i][1],
@@ -2285,17 +2343,35 @@ sub _dbh_execute_for_fetch {
   my $fetch_tuple = sub {
     return undef if ++$fetch_row_idx > $#$data;
 
-    return [ map { defined $_->{_literal_bind_subindex}
-      ? ${ $data->[ $fetch_row_idx ]->[ $_->{_bind_data_slice_idx} ]}
-         ->[ $_->{_literal_bind_subindex} ]
-          ->[1]
-      : $data->[ $fetch_row_idx ]->[ $_->{_bind_data_slice_idx} ]
-    } map { $_->[0] } @$proto_bind];
+    return [ map {
+      my $v = ! defined $_->{_literal_bind_subindex}
+
+        ? $data->[ $fetch_row_idx ]->[ $_->{_bind_data_slice_idx} ]
+
+        # There are no attributes to resolve here - we already did everything
+        # when we constructed proto_bind. However we still want to sanity-check
+        # what the user supplied, so pass stuff through to the resolver *anyway*
+        : $self->_resolve_bindattrs (
+            undef,  # a fake rsrc
+            [ ${ $data->[ $fetch_row_idx ]->[ $_->{_bind_data_slice_idx} ]}->[ $_->{_literal_bind_subindex} ] ],
+            {},     # a fake column_info bag
+          )->[0][1]
+      ;
+
+      # FIXME SUBOPTIMAL - DBI needs fixing to always stringify regardless of DBD
+      # For the time being forcibly stringify whatever is stringifiable
+      my $vref;
+
+      ( !length ref $v or ! ($vref = is_plain_value $v) )   ? $v
+    : defined blessed( $$vref )                             ? "$$vref"
+                                                            : $$vref
+    ;
+    } map { $_->[0] } @$proto_bind ];
   };
 
   my $tuple_status = [];
   my ($rv, $err);
-  try {
+  dbic_internal_try {
     $rv = $sth->execute_for_fetch(
       $fetch_tuple,
       $tuple_status,
@@ -2314,7 +2390,7 @@ sub _dbh_execute_for_fetch {
   );
 
   # Statement must finish even if there was an exception.
-  try {
+  dbic_internal_try {
     $sth->finish
   }
   catch {
@@ -2342,7 +2418,7 @@ sub _dbh_execute_inserts_with_no_binds {
   my ($self, $sth, $count) = @_;
 
   my $err;
-  try {
+  dbic_internal_try {
     my $dbh = $self->_get_dbh;
     local $dbh->{RaiseError} = 1;
     local $dbh->{PrintError} = 0;
@@ -2354,7 +2430,7 @@ sub _dbh_execute_inserts_with_no_binds {
   };
 
   # Make sure statement is finished even if there was an exception.
-  try {
+  dbic_internal_try {
     $sth->finish
   }
   catch {
@@ -2417,37 +2493,17 @@ sub _select_args {
   #) if $orig_attrs->{!args_as_stored_at_the_end_of_this_method!};
 
   my $sql_maker = $self->sql_maker;
-  my $alias2source = $self->_resolve_ident_sources ($ident);
 
   my $attrs = {
     %$orig_attrs,
     select => $select,
     from => $ident,
     where => $where,
-
-    # limit dialects use this stuff
-    # yes, some CDBICompat crap does not supply an {alias} >.<
-    ( $orig_attrs->{alias} and $alias2source->{$orig_attrs->{alias}} )
-      ? ( _rsroot_rsrc => $alias2source->{$orig_attrs->{alias}} )
-      : ()
-    ,
   };
 
-  # Sanity check the attributes (SQLMaker does it too, but
-  # in case of a software_limit we'll never reach there)
-  if (defined $attrs->{offset}) {
-    $self->throw_exception('A supplied offset attribute must be a non-negative integer')
-      if ( $attrs->{offset} =~ /\D/ or $attrs->{offset} < 0 );
-  }
-
-  if (defined $attrs->{rows}) {
-    $self->throw_exception("The rows attribute must be a positive integer if present")
-      if ( $attrs->{rows} =~ /\D/ or $attrs->{rows} <= 0 );
-  }
-  elsif ($attrs->{offset}) {
-    # MySQL actually recommends this approach.  I cringe.
-    $attrs->{rows} = $sql_maker->__max_int;
-  }
+  # MySQL actually recommends this approach.  I cringe.
+  $attrs->{rows} ||= $sql_maker->__max_int
+    if $attrs->{offset};
 
   # see if we will need to tear the prefetch apart to satisfy group_by == select
   # this is *extremely tricky* to get right, I am still not sure I did
@@ -2467,13 +2523,13 @@ sub _select_args {
     # are happy (this includes MySQL in strict_mode)
     # If any of the other joined tables are referenced in the group_by
     # however - the user is on their own
-    ( $prefetch_needs_subquery or $attrs->{_related_results_construction} )
+    ( $prefetch_needs_subquery or ! $attrs->{_simple_passthrough_construction} )
       and
     $attrs->{group_by}
       and
     @{$attrs->{group_by}}
       and
-    my $grp_aliases = try { # try{} because $attrs->{from} may be unreadable
+    my $grp_aliases = dbic_internal_try { # internal_try{} because $attrs->{from} may be unreadable
       $self->_resolve_aliastypes_from_select_args({ from => $attrs->{from}, group_by => $attrs->{group_by} })
     }
   ) {
@@ -2521,6 +2577,8 @@ sub _select_args {
   $orig_attrs->{_last_sqlmaker_alias_map} = $attrs->{_aliastypes};
 
 ###
+  #   my $alias2source = $self->_resolve_ident_sources ($ident);
+  #
   # This would be the point to deflate anything found in $attrs->{where}
   # (and leave $attrs->{bind} intact). Problem is - inflators historically
   # expect a result object. And all we have is a resultsource (it is trivial
@@ -2585,10 +2643,10 @@ see L<DBIx::Class::SQLMaker::LimitDialects>.
 sub _dbh_columns_info_for {
   my ($self, $dbh, $table) = @_;
 
-  if ($dbh->can('column_info')) {
-    my %result;
-    my $caught;
-    try {
+  my %result;
+
+  if (! DBIx::Class::_ENV_::STRESSTEST_COLUMN_INFO_UNAWARE_STORAGE and $dbh->can('column_info')) {
+    dbic_internal_try {
       my ($schema,$tab) = $table =~ /^(.+?)\.(.+)$/ ? ($1,$2) : (undef,$table);
       my $sth = $dbh->column_info( undef,$schema, $tab, '%' );
       $sth->execute();
@@ -2604,39 +2662,75 @@ sub _dbh_columns_info_for {
         $result{$col_name} = \%column_info;
       }
     } catch {
-      $caught = 1;
+      %result = ();
     };
-    return \%result if !$caught && scalar keys %result;
+
+    return \%result if keys %result;
   }
 
-  my %result;
   my $sth = $dbh->prepare($self->sql_maker->select($table, undef, \'1 = 0'));
   $sth->execute;
-  my @columns = @{$sth->{NAME_lc}};
-  for my $i ( 0 .. $#columns ){
-    my %column_info;
-    $column_info{data_type} = $sth->{TYPE}->[$i];
-    $column_info{size} = $sth->{PRECISION}->[$i];
-    $column_info{is_nullable} = $sth->{NULLABLE}->[$i] ? 1 : 0;
-
-    if ($column_info{data_type} =~ m/^(.*?)\((.*?)\)$/) {
-      $column_info{data_type} = $1;
-      $column_info{size}    = $2;
+
+### The acrobatics with lc names is necessary to support both the legacy
+### API that used NAME_lc exclusively, *AND* at the same time work properly
+### with column names differing in cas eonly (thanks pg!)
+
+  my ($columns, $seen_lcs);
+
+  ++$seen_lcs->{lc($_)} and $columns->{$_} = {
+    idx => scalar keys %$columns,
+    name => $_,
+    lc_name => lc($_),
+  } for @{$sth->{NAME}};
+
+  $seen_lcs->{$_->{lc_name}} == 1
+    and
+  $_->{name} = $_->{lc_name}
+    for values %$columns;
+
+  for ( values %$columns ) {
+    my $inf = {
+      data_type => $sth->{TYPE}->[$_->{idx}],
+      size => $sth->{PRECISION}->[$_->{idx}],
+      is_nullable => $sth->{NULLABLE}->[$_->{idx}] ? 1 : 0,
+    };
+
+    if ($inf->{data_type} =~ m/^(.*?)\((.*?)\)$/) {
+      @{$inf}{qw( data_type  size)} = ($1, $2);
     }
 
-    $result{$columns[$i]} = \%column_info;
+    $result{$_->{name}} = $inf;
   }
+
   $sth->finish;
 
-  foreach my $col (keys %result) {
-    my $colinfo = $result{$col};
-    my $type_num = $colinfo->{data_type};
-    my $type_name;
-    if(defined $type_num && $dbh->can('type_info')) {
-      my $type_info = $dbh->type_info($type_num);
-      $type_name = $type_info->{TYPE_NAME} if $type_info;
-      $colinfo->{data_type} = $type_name if $type_name;
+  if ($dbh->can('type_info')) {
+    for my $inf (values %result) {
+      next if ! defined $inf->{data_type};
+
+      $inf->{data_type} = (
+        (
+          (
+            $dbh->type_info( $inf->{data_type} )
+              ||
+            next
+          )
+            ||
+          next
+        )->{TYPE_NAME}
+          ||
+        next
+      );
+
+      # FIXME - this may be an artifact of the DBD::Pg implmentation alone
+      # needs more testing in the future...
+      $inf->{size} -= 4 if (
+        ( $inf->{size}||0 > 4 )
+          and
+        $inf->{data_type} =~ qr/^text$/i
+      );
     }
+
   }
 
   return \%result;
@@ -2656,7 +2750,7 @@ Return the row id of the last insert.
 sub _dbh_last_insert_id {
     my ($self, $dbh, $source, $col) = @_;
 
-    my $id = try { $dbh->last_insert_id (undef, undef, $source->name, $col) };
+    my $id = dbic_internal_try { $dbh->last_insert_id (undef, undef, $source->name, $col) };
 
     return $id if defined $id;
 
@@ -2707,15 +2801,15 @@ sub _determine_supports_placeholders {
 
   # some drivers provide a $dbh attribute (e.g. Sybase and $dbh->{syb_dynamic_supported})
   # but it is inaccurate more often than not
-  return try {
+  ( dbic_internal_try {
     local $dbh->{PrintError} = 0;
     local $dbh->{RaiseError} = 1;
     $dbh->do('select ?', {}, 1);
     1;
-  }
-  catch {
-    0;
-  };
+  } )
+    ? 1
+    : 0
+  ;
 }
 
 # Check if placeholders bound to non-string types throw exceptions
@@ -2724,16 +2818,16 @@ sub _determine_supports_typeless_placeholders {
   my $self = shift;
   my $dbh  = $self->_get_dbh;
 
-  return try {
+  ( dbic_internal_try {
     local $dbh->{PrintError} = 0;
     local $dbh->{RaiseError} = 1;
     # this specifically tests a bind that is NOT a string
     $dbh->do('select 1 where 1 = ?', {}, 1);
     1;
-  }
-  catch {
-    0;
-  };
+  } )
+    ? 1
+    : 0
+  ;
 }
 
 =head2 sqlt_type
@@ -2868,11 +2962,12 @@ sub create_ddl_dir {
     add_drop_table => 1,
     ignore_constraint_names => 1,
     ignore_index_names => 1,
+    quote_identifiers => $self->sql_maker->_quoting_enabled,
     %{$sqltargs || {}}
   };
 
-  unless (DBIx::Class::Optional::Dependencies->req_ok_for ('deploy')) {
-    $self->throw_exception("Can't create a ddl file without " . DBIx::Class::Optional::Dependencies->req_missing_for ('deploy') );
+  if (my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('deploy')) {
+    $self->throw_exception("Can't create a ddl file without $missing");
   }
 
   my $sqlt = SQL::Translator->new( $sqltargs );
@@ -2962,10 +3057,21 @@ sub create_ddl_dir {
         unless $dest_schema->name;
     }
 
-    my $diff = SQL::Translator::Diff::schema_diff($source_schema, $db,
-                                                  $dest_schema,   $db,
-                                                  $sqltargs
-                                                 );
+    my $diff = do {
+      # FIXME - this is a terrible workaround for
+      # https://github.com/dbsrgits/sql-translator/commit/2d23c1e
+      # Fixing it in this sloppy manner so that we don't hve to
+      # lockstep an SQLT release as well. Needs to be removed at
+      # some point, and SQLT dep bumped
+      local $SQL::Translator::Producer::SQLite::NO_QUOTES
+        if $SQL::Translator::Producer::SQLite::NO_QUOTES;
+
+      SQL::Translator::Diff::schema_diff($source_schema, $db,
+                                         $dest_schema,   $db,
+                                         $sqltargs
+                                       );
+    };
+
     if(!open $file, ">$difffile") {
       $self->throw_exception("Can't write to $difffile ($!)");
       next;
@@ -2983,7 +3089,8 @@ sub create_ddl_dir {
 
 =back
 
-Returns the statements used by L</deploy> and L<DBIx::Class::Schema/deploy>.
+Returns the statements used by L<DBIx::Class::Storage/deploy>
+and L<DBIx::Class::Schema/deploy>.
 
 The L<SQL::Translator> (not L<DBI>) database driver name can be explicitly
 provided in C<$type>, otherwise the result of L</sqlt_type> is used as default.
@@ -3016,8 +3123,8 @@ sub deployment_statements {
       return join('', @rows);
   }
 
-  unless (DBIx::Class::Optional::Dependencies->req_ok_for ('deploy') ) {
-    $self->throw_exception("Can't deploy without a ddl_dir or " . DBIx::Class::Optional::Dependencies->req_missing_for ('deploy') );
+  if (my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('deploy') ) {
+    $self->throw_exception("Can't deploy without a pregenerated 'ddl_dir' directory or $missing");
   }
 
   # sources needs to be a parser arg, but for simplicity allow at top level
@@ -3025,6 +3132,9 @@ sub deployment_statements {
   $sqltargs->{parser_args}{sources} = delete $sqltargs->{sources}
       if exists $sqltargs->{sources};
 
+  $sqltargs->{quote_identifiers} = $self->sql_maker->_quoting_enabled
+    unless exists $sqltargs->{quote_identifiers};
+
   my $tr = SQL::Translator->new(
     producer => "SQL::Translator::Producer::${type}",
     %$sqltargs,
@@ -3053,7 +3163,7 @@ sub deploy {
     return if($line =~ /^COMMIT/m);
     return if $line =~ /^\s+$/; # skip whitespace only
     $self->_query_start($line);
-    try {
+    dbic_internal_try {
       # do a dbh_do cycle here, as we need some error checking in
       # place (even though we will ignore errors)
       $self->dbh_do (sub { $_[1]->do($line) });
@@ -3257,13 +3367,13 @@ transactions.  You're on your own for handling all sorts of exceptional
 cases if you choose the C<< AutoCommit => 0 >> path, just as you would
 be with raw DBI.
 
+=head1 FURTHER QUESTIONS?
 
-=head1 AUTHOR AND CONTRIBUTORS
-
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
-
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 9384117..7490d89 100644 (file)
@@ -122,15 +122,19 @@ sub _exec_svp_rollback {
   $self->_exec_txn_rollback;
 }
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sts=2 sw=2:
index db8517d..cfabc73 100644 (file)
@@ -7,8 +7,7 @@ use base 'DBIx::Class::Storage::DBI';
 use mro 'c3';
 
 use Sub::Name;
-use Try::Tiny;
-use DBIx::Class::_Util 'sigwarn_silencer';
+use DBIx::Class::_Util qw( sigwarn_silencer modver_gt_or_eq );
 use namespace::clean;
 
 =head1 NAME
@@ -45,7 +44,7 @@ sub _init {
   unless ($DBD::ADO::__DBIC_MONKEYPATCH_CHECKED__) {
     require DBD::ADO;
 
-    unless (try { DBD::ADO->VERSION('2.99'); 1 }) {
+    unless ( modver_gt_or_eq( 'DBD::ADO', '2.99' ) ) {
       no warnings 'redefine';
       my $disconnect = *DBD::ADO::db::disconnect{CODE};
 
@@ -75,15 +74,19 @@ sub _init {
 #  $sth;
 #}
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sts=2 sw=2:
index 8eb1719..c7cb5c3 100644 (file)
@@ -141,15 +141,19 @@ sub format_datetime {
   return $datetime_parser->format_datetime(shift);
 }
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sts=2 sw=2:
index 5c50ca3..8b1a782 100644 (file)
@@ -64,16 +64,19 @@ sub all {
   return @rows;
 }
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
+1;
+
 # vim:sts=2 sw=2:
index 09cbee6..ac42a1e 100644 (file)
@@ -60,7 +60,7 @@ size of the bind sizes in the first prepare call:
 
 L<https://rt.cpan.org/Ticket/Display.html?id=52048>
 
-The C<ado_size> workaround is used (see L<DBD::ADO/"ADO Providers">) with the
+The C<ado_size> workaround is used (see L<DBD::ADO/ADO providers>) with the
 approximate maximum size of the data_type of the bound column, or 8000 (maximum
 VARCHAR size) if the data_type is not available.
 
@@ -182,16 +182,18 @@ sub _dbi_attrs_for_bind {
 
   my $attrs = $self->next::method(@_);
 
-  foreach my $attr (@$attrs) {
-    $attr->{ado_size} ||= 8000 if $attr;
-  }
+  # The next::method above caches the returned hashrefs in a _dbh related
+  # structure. It is safe for us to modify it in this manner, as the default
+  # does not really change (albeit the entire logic is insane and is pending
+  # a datatype-objects rewrite)
+  $_ and $_->{ado_size} ||= 8000 for @$attrs;
 
   return $attrs;
 }
 
-# Can't edit all the binds in _dbi_attrs_for_bind for insert_bulk, so we take
+# Can't edit all the binds in _dbi_attrs_for_bind for _insert_bulk, so we take
 # care of those GUIDs here.
-sub insert_bulk {
+sub _insert_bulk {
   my $self = shift;
   my ($source, $cols, $data) = @_;
 
@@ -436,15 +438,19 @@ sub format_datetime {
   return $datetime_parser->format_datetime(shift);
 }
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sts=2 sw=2:
index 1ada243..6253ee6 100644 (file)
@@ -87,16 +87,19 @@ sub all {
   return @rows;
 }
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
+1;
+
 # vim:sts=2 sw=2:
index db9fb8b..37ed620 100644 (file)
@@ -23,7 +23,8 @@ statements with values bound to columns or conditions that are not strings will
 throw implicit type conversion errors.
 
 As long as a column L<data_type|DBIx::Class::ResultSource/add_columns> is
-defined and resolves to a base RDBMS native type via L</_native_data_type> as
+defined and resolves to a base RDBMS native type via
+L<_native_data_type|DBIx::Class::Storage::DBI/_native_data_type> as
 defined in your Storage driver, the placeholder for this column will be
 converted to:
 
@@ -77,13 +78,16 @@ sub connect_call_set_auto_cast {
   $self->auto_cast(1);
 }
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 6681d23..cac1529 100644 (file)
@@ -3,10 +3,11 @@ package DBIx::Class::Storage::DBI::Cursor;
 use strict;
 use warnings;
 
-use base qw/DBIx::Class::Cursor/;
+use base 'DBIx::Class::Cursor';
 
-use Try::Tiny;
-use Scalar::Util qw/refaddr weaken/;
+use Scalar::Util qw(refaddr weaken);
+use List::Util 'shuffle';
+use DBIx::Class::_Util qw( detected_reinvoked_destructor dbic_internal_try );
 use namespace::clean;
 
 __PACKAGE__->mk_group_accessors('simple' =>
@@ -82,6 +83,11 @@ Returns a new L<DBIx::Class::Storage::DBI::Cursor> object.
 
       $self->{_intra_thread} = 1;
     }
+
+    # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+    # collected before leaving this scope. Depending on the code above, this
+    # may very well be just a preventive measure guarding future modifications
+    undef;
   }
 }
 
@@ -177,7 +183,14 @@ sub all {
 
   (undef, $sth) = $self->storage->_select( @{$self->{args}} );
 
-  return @{$sth->fetchall_arrayref};
+  return (
+    DBIx::Class::_ENV_::SHUFFLE_UNORDERED_RESULTSETS
+      and
+    ! $self->{attrs}{order_by}
+  )
+    ? shuffle @{$sth->fetchall_arrayref}
+    : @{$sth->fetchall_arrayref}
+  ;
 }
 
 sub sth {
@@ -221,11 +234,23 @@ Resets the cursor to the beginning of the L<DBIx::Class::ResultSet>.
 sub reset {
   $_[0]->__finish_sth if $_[0]->{sth};
   $_[0]->sth(undef);
+
+  # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+  # collected before leaving this scope. Depending on the code above, this
+  # may very well be just a preventive measure guarding future modifications
+  undef;
 }
 
 
 sub DESTROY {
+  return if &detected_reinvoked_destructor;
+
   $_[0]->__finish_sth if $_[0]->{sth};
+
+  # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+  # collected before leaving this scope. Depending on the code above, this
+  # may very well be just a preventive measure guarding future modifications
+  undef;
 }
 
 sub __finish_sth {
@@ -240,9 +265,36 @@ sub __finish_sth {
   my $self = shift;
 
   # No need to care about failures here
-  try { local $SIG{__WARN__} = sub {}; $self->{sth}->finish } if (
-    $self->{sth} and ! try { ! $self->{sth}->FETCH('Active') }
+  dbic_internal_try {
+    local $SIG{__WARN__} = sub {};
+    $self->{sth}->finish
+  } if (
+    $self->{sth}
+      and
+    # weird double-negative to catch the case of ->FETCH throwing
+    # and attempt a finish *anyway*
+    ! dbic_internal_try {
+      ! $self->{sth}->FETCH('Active')
+    }
   );
+
+  # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+  # collected before leaving this scope. Depending on the code above, this
+  # may very well be just a preventive measure guarding future modifications
+  undef;
 }
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
+=cut
+
 1;
index 7634eb6..c34e641 100644 (file)
@@ -60,8 +60,6 @@ sub _dbh_last_insert_id {
   return @res ? $res[0] : undef;
 }
 
-1;
-
 =head1 NAME
 
 DBIx::Class::Storage::DBI::DB2 - IBM DB2 support for DBIx::Class
@@ -73,13 +71,19 @@ RowNumberOver over FetchFirst depending on the availability of support for
 RowNumberOver, queries the server name_sep from L<DBI> and sets the L<DateTime>
 parser to L<DateTime::Format::DB2>.
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sts=2 sw=2:
index f0178bd..588dce6 100644 (file)
@@ -8,9 +8,10 @@ use warnings;
 # in ::Storage::DBI::InterBase as opposed to inheriting
 # directly from ::Storage::DBI::Firebird::Common
 use base qw/DBIx::Class::Storage::DBI::InterBase/;
-
 use mro 'c3';
 
+1;
+
 =head1 NAME
 
 DBIx::Class::Storage::DBI::Firebird - Driver for the Firebird RDBMS via
@@ -21,17 +22,13 @@ L<DBD::Firebird>
 This is an empty subclass of L<DBIx::Class::Storage::DBI::InterBase> for use
 with L<DBD::Firebird>, see that driver for details.
 
-=cut
-
-1;
-
-=head1 AUTHOR
-
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
-# vim:sts=2 sw=2:
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 7e6b518..6e61ca5 100644 (file)
@@ -164,8 +164,6 @@ sub format_date {
   return $date_parser->format_datetime(shift);
 }
 
-1;
-
 =head1 CAVEATS
 
 =over 4
@@ -178,13 +176,19 @@ work with earlier versions.
 
 =back
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sts=2 sw=2:
index 5483af4..8485e86 100644 (file)
@@ -41,6 +41,11 @@ sub _prep_for_execute {
   my $table = $self->sql_maker->_quote($ident->name);
   $op = uc $op;
 
+  DBIx::Class::Exception->throw(
+    "Unexpected _autoinc_supplied_for_op flag in callstack - please file a bug including the stacktrace ( @{[ DBIx::Class::_ENV_::HELP_URL() ]} ):\n\n STACKTRACE STARTS",
+    'stacktrace'
+  ) if $op ne 'INSERT' and $op ne 'UPDATE';
+
   my ($sql, $bind) = $self->next::method(@_);
 
   return (<<EOS, $bind);
@@ -51,13 +56,16 @@ EOS
 
 }
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index ca6bf55..e8c123d 100644 (file)
@@ -169,14 +169,18 @@ sub format_date {
   return $date_parser->format_datetime(shift);
 }
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
index cb6d8f9..7b6ef6c 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 use base qw/DBIx::Class::Storage::DBI::Firebird::Common/;
 use mro 'c3';
-use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 use namespace::clean;
 
 =head1 NAME
@@ -38,12 +38,13 @@ sub _ping {
   local $dbh->{RaiseError} = 1;
   local $dbh->{PrintError} = 0;
 
-  return try {
+  (dbic_internal_try {
     $dbh->do('select 1 from rdb$database');
     1;
-  } catch {
-    0;
-  };
+  })
+    ? 1
+    : 0
+  ;
 }
 
 # We want dialect 3 for new features and quoting to work, DBD::InterBase uses
@@ -131,8 +132,6 @@ sub connect_call_datetime_setup {
   $self->_get_dbh->{ib_time_all} = 'ISO';
 }
 
-1;
-
 =head1 CAVEATS
 
 =over 4
@@ -149,13 +148,19 @@ Alternately, use the L<ODBC|DBIx::Class::Storage::DBI::ODBC::Firebird> driver.
 
 =back
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sts=2 sw=2:
index 34d3745..4eb090a 100644 (file)
@@ -9,7 +9,7 @@ use base qw/
 /;
 use mro 'c3';
 
-use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 use List::Util 'first';
 use namespace::clean;
 
@@ -81,7 +81,7 @@ sub _execute {
 
     # we didn't even try on ftds
     unless ($self->_no_scope_identity_query) {
-      ($identity) = try { $sth->fetchrow_array };
+      ($identity) = dbic_internal_try { $sth->fetchrow_array };
       $sth->finish;
     }
 
@@ -161,7 +161,7 @@ sub sql_limit_dialect {
     # stored procedures like xp_msver, or version detection failed for some
     # other reason.
     # So, we use a query to check if RNO is implemented.
-    try {
+    dbic_internal_try {
       $self->_get_dbh->selectrow_array('SELECT row_number() OVER (ORDER BY rand())');
       $supports_rno = 1;
     };
@@ -178,12 +178,13 @@ sub _ping {
   local $dbh->{RaiseError} = 1;
   local $dbh->{PrintError} = 0;
 
-  return try {
+  (dbic_internal_try {
     $dbh->do('select 1');
     1;
-  } catch {
-    0;
-  };
+  })
+    ? 1
+    : 0
+  ;
 }
 
 package # hide from PAUSE
@@ -327,12 +328,13 @@ for this flag - you are urged to do so. If DBIC internals insist that an
 ordered subselect is necessary for an operation, and you believe there is a
 different/better way to get the same result - please file a bugreport.
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
-
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 85810cc..2ca9939 100644 (file)
@@ -99,7 +99,7 @@ sub interpolate_unquoted {
       and
     $_[1]
       and
-    $_[2] !~ /\D/
+    $_[2] !~ /[^0-9]/
       and
     $_[1] =~ /int(?:eger)? | (?:tiny|small|medium|big)int/ix
   );
@@ -119,13 +119,16 @@ sub _prep_interpolated_value {
   return $_[2];
 }
 
-=head1 AUTHORS
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 1e8851b..5a6d078 100644 (file)
@@ -50,8 +50,6 @@ sub _disable_odbc_array_ops {
   }
 }
 
-1;
-
 =head1 NAME
 
 DBIx::Class::Storage::DBI::ODBC - Base class for ODBC drivers
@@ -61,13 +59,19 @@ DBIx::Class::Storage::DBI::ODBC - Base class for ODBC drivers
 This class simply provides a mechanism for discovering and loading a sub-class
 for a specific ODBC backend.  It should be transparent to the user.
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sts=2 sw=2:
index e8cca9a..69b3b9b 100644 (file)
@@ -143,15 +143,19 @@ sub format_datetime {
   return $datetime_parser->format_datetime(shift);
 }
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sts=2 sw=2:
index e17715c..8ff7d1f 100644 (file)
@@ -20,13 +20,14 @@ over ODBC
 
 This is an empty subclass of L<DBIx::Class::Storage::DBI::DB2>.
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
-=cut
-# vim:sts=2 sw=2:
index 095f416..91f7292 100644 (file)
@@ -8,6 +8,7 @@ use base qw/
 /;
 use mro 'c3';
 use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 use namespace::clean;
 
 =head1 NAME
@@ -48,7 +49,7 @@ sub _exec_svp_release { 1 }
 sub _exec_svp_rollback {
   my ($self, $name) = @_;
 
-  try {
+  dbic_internal_try {
     $self->_dbh->do("ROLLBACK TO SAVEPOINT $name")
   }
   catch {
@@ -59,13 +60,20 @@ sub _exec_svp_rollback {
   };
 }
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
-You may distribute this code under the same terms as Perl itself.
 
 =cut
+
 # vim:sts=2 sw=2:
+
+1;
index 911ca48..4ee00eb 100644 (file)
@@ -9,6 +9,7 @@ use base qw/
 use mro 'c3';
 use Scalar::Util 'reftype';
 use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 use DBIx::Class::Carp;
 use namespace::clean;
 
@@ -227,7 +228,7 @@ sub _run_connection_actions {
     !!$self->_using_dynamic_cursors
   ) {
     if ($use_dyncursors) {
-      try {
+      dbic_internal_try {
         my $dbh = $self->_dbh;
         local $dbh->{RaiseError} = 1;
         local $dbh->{PrintError} = 0;
@@ -308,15 +309,19 @@ sub connect_call_use_server_cursors {
   $self->_get_dbh->{odbc_SQL_ROWSET_SIZE} = $sql_rowset_size;
 }
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sw=2 sts=2 et
index 0a6bd1a..32b8984 100644 (file)
@@ -32,12 +32,14 @@ fail with:
 B<WORKAROUND:> use the C<uniqueidentifier> type instead, it is more efficient
 anyway.
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
-=cut
index b0184e8..6dd8b72 100644 (file)
@@ -40,12 +40,13 @@ no matter the database version, add
 
 to your Schema class.
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
-
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index d763953..30a9f54 100644 (file)
@@ -7,8 +7,8 @@ use mro 'c3';
 use DBIx::Class::Carp;
 use Scope::Guard ();
 use Context::Preserve 'preserve_context';
-use Try::Tiny;
 use List::Util 'first';
+use DBIx::Class::_Util qw( modver_gt_or_eq_and_lt dbic_internal_try );
 use namespace::clean;
 
 __PACKAGE__->sql_limit_dialect ('RowNum');
@@ -103,9 +103,6 @@ sub deployment_statements {
   my ($schema, $type, $version, $dir, $sqltargs, @rest) = @_;
 
   $sqltargs ||= {};
-  my $quote_char = $self->schema->storage->sql_maker->quote_char;
-  $sqltargs->{quote_table_names} = $quote_char ? 1 : 0;
-  $sqltargs->{quote_field_names} = $quote_char ? 1 : 0;
 
   if (
     ! exists $sqltargs->{producer_args}{oracle_version}
@@ -275,12 +272,13 @@ sub _ping {
   local $dbh->{RaiseError} = 1;
   local $dbh->{PrintError} = 0;
 
-  return try {
+  ( dbic_internal_try {
     $dbh->do('select 1 from dual');
     1;
-  } catch {
-    0;
-  };
+  })
+    ? 1
+    : 0
+  ;
 }
 
 sub _dbh_execute {
@@ -422,11 +420,18 @@ sub _dbi_attrs_for_bind {
 
   my $attrs = $self->next::method($ident, $bind);
 
-  for my $i (0 .. $#$attrs) {
-    if (keys %{$attrs->[$i]||{}} and my $col = $bind->[$i][0]{dbic_colname}) {
-      $attrs->[$i]{ora_field} = $col;
-    }
-  }
+  # Push the column name into all bind attrs, make sure to *NOT* write into
+  # the existing $attrs->[$idx]{..} hashref, as it is cached by the call to
+  # next::method above.
+  # FIXME - this code will go away when the LobWriter refactor lands
+  $attrs->[$_]
+    and
+  keys %{ $attrs->[$_] }
+    and
+  $bind->[$_][0]{dbic_colname}
+    and
+  $attrs->[$_] = { %{$attrs->[$_]}, ora_field => $bind->[$_][0]{dbic_colname} }
+    for 0 .. $#$attrs;
 
   $attrs;
 }
@@ -436,20 +441,11 @@ sub bind_attribute_by_data_type {
 
   if ($self->_is_lob_type($dt)) {
 
-    # this is a hot-ish codepath, store an escape-flag in the DBD namespace, so that
-    # things like Class::Unload work (unlikely but possible)
-    unless ($DBD::Oracle::__DBIC_DBD_VERSION_CHECK_OK__) {
-
-      # no earlier - no later
-      if ($DBD::Oracle::VERSION eq '1.23') {
-        $self->throw_exception(
-          "BLOB/CLOB support in DBD::Oracle == 1.23 is broken, use an earlier or later ".
-          "version (https://rt.cpan.org/Public/Bug/Display.html?id=46016)"
-        );
-      }
-
-      $DBD::Oracle::__DBIC_DBD_VERSION_CHECK_OK__ = 1;
-    }
+    # no earlier - no later
+    $self->throw_exception(
+      "BLOB/CLOB support in DBD::Oracle == 1.23 is broken, use an earlier or later "
+    . "version (https://rt.cpan.org/Public/Bug/Display.html?id=46016)"
+    ) if modver_gt_or_eq_and_lt( 'DBD::Oracle', '1.23', '1.24' );
 
     return {
       ora_type => $self->_is_text_lob_type($dt)
@@ -638,7 +634,7 @@ Unfortunately, Oracle doesn't support identifiers over 30 chars in length, so
 the L<DBIx::Class::Relationship> name is shortened and appended with half of an
 MD5 hash.
 
-See L<DBIx::Class::Storage/"relname_to_table_alias">.
+See L<DBIx::Class::Storage::DBI/relname_to_table_alias>.
 
 =cut
 
@@ -766,13 +762,16 @@ It uses the same syntax as L<DBIx::Class::ResultSet/order_by>
   # ORDER SIBLINGS BY
   #     firstname ASC
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index c0b46e8..5dd6e2f 100644 (file)
@@ -68,16 +68,13 @@ Probably lots more.
 
 =back
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-Justin Wheeler C<< <jwheeler@datademons.com> >>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 CONTRIBUTORS
+=head1 COPYRIGHT AND LICENSE
 
-David Jack Olrik C<< <djo@cpan.org> >>
-
-=head1 LICENSE
-
-This module is licensed under the same terms as Perl itself.
-
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index fcdab67..ded6d06 100644 (file)
@@ -8,7 +8,7 @@ use base qw/DBIx::Class::Storage::DBI/;
 use Scope::Guard ();
 use Context::Preserve 'preserve_context';
 use DBIx::Class::Carp;
-use Try::Tiny;
+use DBIx::Class::_Util 'modver_gt_or_eq';
 use namespace::clean;
 
 __PACKAGE__->sql_limit_dialect ('LimitOffset');
@@ -168,19 +168,27 @@ sub bind_attribute_by_data_type {
   if ($self->_is_binary_lob_type($data_type)) {
     # this is a hot-ish codepath, use an escape flag to minimize
     # amount of function/method calls
-    # additionally version.pm is cock, and memleaks on multiple
-    # ->VERSION calls
     # the flag is stored in the DBD namespace, so that Class::Unload
     # will work (unlikely, but still)
-    unless ($DBD::Pg::__DBIC_DBD_VERSION_CHECK_DONE__) {
-      if ($self->_server_info->{normalized_dbms_version} >= 9.0) {
-        try { DBD::Pg->VERSION('2.17.2'); 1 } or carp (
-          __PACKAGE__.': BYTEA columns are known to not work on Pg >= 9.0 with DBD::Pg < 2.17.2'
+    unless (
+      modver_gt_or_eq( 'DBD::Pg', '2.17.2' )
+        or
+      $DBD::Pg::__DBIC_DBD_VERSION_CHECK_DONE__
+    ) {
+      if ( $self->_server_info->{normalized_dbms_version} >= 9.0 ) {
+        $self->throw_exception(
+          'BYTEA columns are known to not work on Pg >= 9.0 with DBD::Pg < 2.17.2'
         );
       }
-      elsif (not try { DBD::Pg->VERSION('2.9.2'); 1 } ) { carp (
-        __PACKAGE__.': DBD::Pg 2.9.2 or greater is strongly recommended for BYTEA column support'
-      )}
+      elsif (
+        my $missing = DBIx::Class::Optional::Dependencies->req_missing_for([qw( rdbms_pg binary_data )])
+      ) {
+        # FIXME - perhaps this needs to be an exception too...?
+        # too old to test sensibly...
+        carp (
+          __PACKAGE__ . ": BYTEA column support strongly recommends $missing"
+        )
+      }
 
       $DBD::Pg::__DBIC_DBD_VERSION_CHECK_DONE__ = 1;
     }
@@ -266,12 +274,13 @@ option to connect(), for example:
                     },
                   );
 
-=head1 AUTHORS
-
-See L<DBIx::Class/CONTRIBUTORS>
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 3c58716..7d61118 100644 (file)
@@ -1,9 +1,13 @@
 package DBIx::Class::Storage::DBI::Replicated;
 
+use warnings;
+use strict;
+
 BEGIN {
-  use DBIx::Class;
-  die('The following modules are required for Replication ' . DBIx::Class::Optional::Dependencies->req_missing_for ('replicated') . "\n" )
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('replicated');
+  require DBIx::Class::Optional::Dependencies;
+  if ( my $missing = DBIx::Class::Optional::Dependencies->req_missing_for('replicated') ) {
+    die "The following modules are required for Replicated storage support: $missing\n";
+  }
 }
 
 use Moose;
@@ -17,11 +21,10 @@ use Hash::Merge;
 use List::Util qw/min max reduce/;
 use Context::Preserve 'preserve_context';
 use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 
 use namespace::clean -except => 'meta';
 
-=encoding utf8
-
 =head1 NAME
 
 DBIx::Class::Storage::DBI::Replicated - BETA Replicated database support
@@ -265,7 +268,6 @@ my $method_dispatch = {
     build_datetime_parser
     last_insert_id
     insert
-    insert_bulk
     update
     delete
     dbh
@@ -310,11 +312,14 @@ my $method_dispatch = {
     _parse_connect_do
     savepoints
     _sql_maker_opts
+    _use_multicolumn_in
     _conn_pid
     _dbh_autocommit
     _native_data_type
     _get_dbh
     sql_maker_class
+    insert_bulk
+    _insert_bulk
     _execute
     _do_query
     _dbh_execute
@@ -329,6 +334,7 @@ my $method_dispatch = {
   unimplemented => [qw/
     _arm_global_destructor
     _verify_pid
+    __delicate_rollback
 
     get_use_dbms_capability
     set_use_dbms_capability
@@ -336,6 +342,7 @@ my $method_dispatch = {
     set_dbms_capability
     _dbh_details
     _dbh_get_info
+    _get_rdbms_name
 
     _determine_connector_driver
     _extract_driver_from_connect_info
@@ -364,7 +371,7 @@ my $method_dispatch = {
     # the capability framework
     # not sure if CMOP->initialize does evil things to DBIC::S::DBI, fix if a problem
     grep
-      { $_ =~ /^ _ (?: use | supports | determine_supports ) _ /x }
+      { $_ =~ /^ _ (?: use | supports | determine_supports ) _ /x and $_ ne '_use_multicolumn_in' }
       ( Class::MOP::Class->initialize('DBIx::Class::Storage::DBI')->get_all_method_names )
   )],
 };
@@ -404,7 +411,7 @@ for my $method (@{$method_dispatch->{unimplemented}}) {
 
 =head2 read_handler
 
-Defines an object that implements the read side of L<BIx::Class::Storage::DBI>.
+Defines an object that implements the read side of L<DBIx::Class::Storage::DBI>.
 
 =cut
 
@@ -417,7 +424,7 @@ has 'read_handler' => (
 
 =head2 write_handler
 
-Defines an object that implements the write side of L<BIx::Class::Storage::DBI>,
+Defines an object that implements the write side of L<DBIx::Class::Storage::DBI>,
 as well as methods that don't write or read that can be called on only one
 storage, methods that return a C<$dbh>, and any methods that don't make sense to
 run on a replicant.
@@ -589,7 +596,8 @@ sub _build_read_handler {
 =head2 around: connect_replicants
 
 All calls to connect_replicants needs to have an existing $schema tacked onto
-top of the args, since L<DBIx::Storage::DBI> needs it, and any C<connect_info>
+top of the args, since L<DBIx::Class::Storage::DBI> needs it, and any
+L<connect_info|DBIx::Class::Storage::DBI/connect_info>
 options merged with the master, with replicant opts having higher priority.
 
 =cut
@@ -692,7 +700,7 @@ sub execute_reliably {
   local $self->{read_handler} = $self->master;
 
   my $args = \@_;
-  return try {
+  return dbic_internal_try {
     $coderef->(@$args);
   } catch {
     $self->throw_exception("coderef returned an error: $_");
@@ -1080,7 +1088,8 @@ sub _get_server_version {
 Due to the fact that replicants can lag behind a master, you must take care to
 make sure you use one of the methods to force read queries to a master should
 you need realtime data integrity.  For example, if you insert a row, and then
-immediately re-read it from the database (say, by doing $result->discard_changes)
+immediately re-read it from the database (say, by doing
+L<< $result->discard_changes|DBIx::Class::Row/discard_changes >>)
 or you insert a row and then immediately build a query that expects that row
 to be an item, you should force the master to handle reads.  Otherwise, due to
 the lag, there is no certainty your data will be in the expected state.
@@ -1112,18 +1121,16 @@ using the Schema clone method.
   ## $new_schema will use only the Master storage for all reads/writes while
   ## the $schema object will use replicated storage.
 
-=head1 AUTHOR
-
-  John Napiorkowski <john.napiorkowski@takkle.com>
-
-Based on code originated by:
+=head1 FURTHER QUESTIONS?
 
-  Norbert Csongrádi <bert@cpan.org>
-  Peter Siklósi <einon@einon.hu>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index de9c2e9..26e3580 100644 (file)
@@ -97,7 +97,7 @@ This class defines the following methods.
 
 =head2 _build_current_replicant
 
-Lazy builder for the L</current_replicant_storage> attribute.
+Lazy builder for the L</current_replicant> attribute.
 
 =cut
 
@@ -243,13 +243,16 @@ sub _get_forced_pool {
   }
 }
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-John Napiorkowski <jjnapiork@cpan.org>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 806a05f..c7a160e 100644 (file)
@@ -39,13 +39,16 @@ sub next_storage {
   return  (shift->pool->active_replicants)[0];
 }
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-John Napiorkowski <john.napiorkowski@takkle.com>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 1fc7b94..6b430f4 100644 (file)
@@ -78,13 +78,16 @@ sub _random_number {
   rand($_[1])
 }
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-John Napiorkowski <john.napiorkowski@takkle.com>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 0b49b98..a95f3cd 100644 (file)
@@ -1,24 +1,22 @@
-package DBIx::Class::Storage::DBI::Replicated::Introduction;
-
 =head1 NAME
 
 DBIx::Class::Storage::DBI::Replicated::Introduction - Minimum Need to Know
 
 =head1 SYNOPSIS
 
-This is an introductory document for L<DBIx::Class::Storage::Replication>.
+This is an introductory document for L<DBIx::Class::Storage::DBI::Replicated>.
 
 This document is not an overview of what replication is or why you should be
-using it.  It is not a document explaining how to setup MySQL native replication
-either.  Copious external resources are available for both.  This document
+using it. It is not a document explaining how to setup MySQL native replication
+either. Copious external resources are available for both. This document
 presumes you have the basics down.
 
 =head1 DESCRIPTION
 
-L<DBIx::Class> supports a framework for using database replication.  This system
+L<DBIx::Class> supports a framework for using database replication. This system
 is integrated completely, which means once it's setup you should be able to
 automatically just start using a replication cluster without additional work or
-changes to your code.  Some caveats apply, primarily related to the proper use
+changes to your code. Some caveats apply, primarily related to the proper use
 of transactions (you are wrapping all your database modifying statements inside
 a transaction, right ;) ) however in our experience properly written DBIC will
 work transparently with Replicated storage.
@@ -137,7 +135,7 @@ will result in increased database loads, so choose a number with care.  Our
 experience is that setting the number around 5 seconds results in a good
 performance / integrity balance.
 
-'master_read_weight' is an option associated with the ::Random balancer.  It
+'master_read_weight' is an option associated with the ::Random balancer. It
 allows you to let the master be read from.  I usually leave this off (default
 is off).
 
@@ -171,14 +169,14 @@ will find L<MySQL::Sandbox> an easy way to set up a replication cluster.
 
 And now your $schema object is properly configured!  Enjoy!
 
-=head1 AUTHOR
-
-John Napiorkowski <jjnapiork@cpan.org>
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
-1;
index 8b15016..7c82d28 100644 (file)
@@ -7,6 +7,7 @@ use Scalar::Util 'reftype';
 use DBI ();
 use MooseX::Types::Moose qw/Num Int ClassName HashRef/;
 use DBIx::Class::Storage::DBI::Replicated::Types 'DBICStorageDBI';
+use DBIx::Class::_Util 'dbic_internal_try';
 use Try::Tiny;
 
 use namespace::clean -except => 'meta';
@@ -293,7 +294,7 @@ Returns 1 on success and undef on failure.
 sub _safely {
   my ($self, $replicant, $name, $code) = @_;
 
-  return try {
+  return dbic_internal_try {
     $code->();
     1;
   } catch {
@@ -410,13 +411,16 @@ sub validate_replicants {
   $self->_last_validated(time);
 }
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-John Napiorkowski <john.napiorkowski@takkle.com>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index a541e7d..7c6084b 100644 (file)
@@ -38,7 +38,9 @@ when it gets too far behind the master, if it stops replicating, etc.
 
 This attribute DOES NOT reflect a replicant's internal status, i.e. if it is
 properly replicating from a master and has not fallen too many seconds behind a
-reliability threshold.  For that, use L</is_replicating>  and L</lag_behind_master>.
+reliability threshold. For that, use
+L<DBIx::Class::Storage::DBI::Replicated/is_replicating> and
+L<DBIx::Class::Storage::DBI::Replicated/lag_behind_master>.
 Since the implementation of those functions database specific (and not all DBIC
 supported DBs support replication) you should refer your database-specific
 storage driver for more information.
@@ -85,13 +87,16 @@ sub debugobj {
 L<http://en.wikipedia.org/wiki/Replicant>,
 L<DBIx::Class::Storage::DBI::Replicated>
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-John Napiorkowski <john.napiorkowski@takkle.com>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 0fcb9b2..0782a6e 100644 (file)
@@ -35,12 +35,4 @@ subtype Weight,
   where { $_ >= 0 },
   message { 'weight must be a decimal greater than 0' };
 
-# AUTHOR
-#
-#  John Napiorkowski <john.napiorkowski@takkle.com>
-#
-# LICENSE
-#
-#  You may distribute this code under the same terms as Perl itself.
-
 1;
index f26eb3c..cb4ad2a 100644 (file)
@@ -4,7 +4,8 @@ use Moose::Role;
 use Scalar::Util 'reftype';
 requires qw/_query_start/;
 
-use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
+
 use namespace::clean -except => 'meta';
 
 =head1 NAME
@@ -33,7 +34,7 @@ Add C<DSN: > to debugging output.
 around '_query_start' => sub {
   my ($method, $self, $sql, @bind) = @_;
 
-  my $dsn = (try { $self->dsn }) || $self->_dbi_connect_info->[0];
+  my $dsn = (dbic_internal_try { $self->dsn }) || $self->_dbi_connect_info->[0];
 
   my($op, $rest) = (($sql=~m/^(\w+)(.+)$/),'NOP', 'NO SQL');
   my $storage_type = $self->can('active') ? 'REPLICANT' : 'MASTER';
@@ -42,7 +43,7 @@ around '_query_start' => sub {
     if ((reftype($dsn)||'') ne 'CODE') {
       "$op [DSN_$storage_type=$dsn]$rest";
     }
-    elsif (my $id = try { $self->id }) {
+    elsif (my $id = dbic_internal_try { $self->id }) {
       "$op [$storage_type=$id]$rest";
     }
     else {
@@ -57,13 +58,16 @@ around '_query_start' => sub {
 
 L<DBIx::Class::Storage::DBI>
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-John Napiorkowski <john.napiorkowski@takkle.com>
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index b830921..3d054bb 100644 (file)
@@ -5,6 +5,7 @@ use warnings;
 use base qw/DBIx::Class::Storage::DBI::UniqueIdentifier/;
 use mro 'c3';
 use List::Util 'first';
+use DBIx::Class::_Util 'dbic_internal_try';
 use Try::Tiny;
 use namespace::clean;
 
@@ -76,7 +77,7 @@ sub _prefetch_autovalues {
     my $table_name = $source->from;
     $table_name    = $$table_name if ref $table_name;
 
-    my ($identity) = try {
+    my ($identity) = dbic_internal_try {
       $dbh->selectrow_array("SELECT GET_IDENTITY('$table_name')")
     };
 
@@ -139,15 +140,14 @@ sub select_single {
 
 sub build_datetime_parser {
   my $self = shift;
-  my $type = "DateTime::Format::Strptime";
-  try {
-    eval "require ${type}"
+  dbic_internal_try {
+    require DateTime::Format::Strptime;
   }
   catch {
-    $self->throw_exception("Couldn't load ${type}: $_");
+    $self->throw_exception("Couldn't load DateTime::Format::Strptime: $_");
   };
 
-  return $type->new( pattern => '%Y-%m-%d %H:%M:%S.%6N' );
+  return DateTime::Format::Strptime->new( pattern => '%Y-%m-%d %H:%M:%S.%6N' );
 }
 
 =head2 connect_call_datetime_setup
@@ -212,12 +212,13 @@ be turned off (or increased) by the DBA by executing:
 
 Highly recommended.
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
-
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 189562e..a341b20 100644 (file)
@@ -85,15 +85,19 @@ sub all {
   return @rows;
 }
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
 # vim:sts=2 sw=2:
index 2e4e312..28e9a08 100644 (file)
@@ -6,7 +6,8 @@ use warnings;
 use base qw/DBIx::Class::Storage::DBI/;
 use mro 'c3';
 
-use DBIx::Class::_Util qw(modver_gt_or_eq sigwarn_silencer);
+use SQL::Abstract 'is_plain_value';
+use DBIx::Class::_Util qw(modver_gt_or_eq sigwarn_silencer dbic_internal_try);
 use DBIx::Class::Carp;
 use Try::Tiny;
 use namespace::clean;
@@ -60,14 +61,9 @@ stringifiable object.
 
 Even if you upgrade DBIx::Class (which works around the bug starting from
 version 0.08210) you may still have corrupted/incorrect data in your database.
-DBIx::Class will currently detect when this condition (more than one
-stringifiable object in one CRUD call) is encountered and will issue a warning
-pointing to this section. This warning will be removed 2 years from now,
-around April 2015, You can disable it after you've audited your data by
-setting the C<DBIC_RT79576_NOWARN> environment variable. Note - the warning
-is emitted only once per callsite per process and only when the condition in
-question is encountered. Thus it is very unlikely that your logsystem will be
-flooded as a result of this.
+DBIx::Class warned about this condition for several years, hoping to give
+anyone affected sufficient notice of the potential issues. The warning was
+removed in version 0.082900.
 
 =back
 
@@ -126,11 +122,23 @@ sub _exec_svp_release {
 sub _exec_svp_rollback {
   my ($self, $name) = @_;
 
-  # For some reason this statement changes the value of $dbh->{AutoCommit}, so
-  # we localize it here to preserve the original value.
-  local $self->_dbh->{AutoCommit} = $self->_dbh->{AutoCommit};
+  $self->_dbh->do("ROLLBACK TO SAVEPOINT $name");
+}
+
+# older SQLite has issues here too - both of these are in fact
+# completely benign warnings (or at least so say the tests)
+sub _exec_txn_rollback {
+  local $SIG{__WARN__} = sigwarn_silencer( qr/rollback ineffective/ )
+    unless $DBD::SQLite::__DBIC_TXN_SYNC_SANE__;
 
-  $self->_dbh->do("ROLLBACK TRANSACTION TO SAVEPOINT $name");
+  shift->next::method(@_);
+}
+
+sub _exec_txn_commit {
+  local $SIG{__WARN__} = sigwarn_silencer( qr/commit ineffective/ )
+    unless $DBD::SQLite::__DBIC_TXN_SYNC_SANE__;
+
+  shift->next::method(@_);
 }
 
 sub _ping {
@@ -160,26 +168,29 @@ sub _ping {
   unless ($DBD::SQLite::__DBIC_TXN_SYNC_SANE__) {
     # since we do not have access to sqlite3_get_autocommit(), do a trick
     # to attempt to *safely* determine what state are we *actually* in.
-    # FIXME
-    # also using T::T here leads to bizarre leaks - will figure it out later
-    my $really_not_in_txn = do {
-      local $@;
+
+    my $really_not_in_txn;
+
+    # not assigning RV directly to env above, because this causes a bizarre
+    # leak of the catch{} cref on older perls... wtf
+    dbic_internal_try {
 
       # older versions of DBD::SQLite do not properly detect multiline BEGIN/COMMIT
       # statements to adjust their {AutoCommit} state. Hence use such a statement
       # pair here as well, in order to escape from poking {AutoCommit} needlessly
       # https://rt.cpan.org/Public/Bug/Display.html?id=80087
-      eval {
-        # will fail instantly if already in a txn
-        $dbh->do("-- multiline\nBEGIN");
-        $dbh->do("-- multiline\nCOMMIT");
-        1;
-      } or do {
-        ($@ =~ /transaction within a transaction/)
-          ? 0
-          : undef
-        ;
-      };
+      #
+      # will fail instantly if already in a txn
+      $dbh->do("-- multiline\nBEGIN");
+      $dbh->do("-- multiline\nCOMMIT");
+
+      $really_not_in_txn = 1;
+    }
+    catch {
+      $really_not_in_txn = ( $_[0] =~ qr/transaction within a transaction/
+        ? 0
+        : undef
+      );
     };
 
     # if we were unable to determine this - we may very well be dead
@@ -204,7 +215,7 @@ sub _ping {
   }
 
   # do the actual test and return on no failure
-  ( $ping_fail ||= ! try { $dbh->do('SELECT * FROM sqlite_master LIMIT 1'); 1 } )
+  ( $ping_fail ||= ! dbic_internal_try { $dbh->do('SELECT * FROM sqlite_master LIMIT 1'); 1 } )
     or return 1; # the actual RV of _ping()
 
   # ping failed (or so it seems) - need to do some cleanup
@@ -213,8 +224,7 @@ sub _ping {
   # keeps the actual file handle open. We don't really want this to happen,
   # so force-close the handle via DBI itself
   #
-  local $@; # so that we do not clobber the real error as set above
-  eval { $dbh->disconnect }; # if it fails - it fails
+  dbic_internal_try { $dbh->disconnect }; # if it fails - it fails
   undef; # the actual RV of _ping()
 }
 
@@ -232,10 +242,6 @@ sub deployment_statements {
     $sqltargs->{producer_args}{sqlite_version} = $dver;
   }
 
-  $sqltargs->{quote_identifiers}
-    = !!$self->sql_maker->_quote_chars
-  if ! exists $sqltargs->{quote_identifiers};
-
   $self->next::method($schema, $type, $version, $dir, $sqltargs, @rest);
 }
 
@@ -308,14 +314,7 @@ sub _dbi_attrs_for_bind {
       = modver_gt_or_eq('DBD::SQLite', '1.37') ? 1 : 0;
   }
 
-  # an attempt to detect former effects of RT#79576, bug itself present between
-  # 0.08191 and 0.08209 inclusive (fixed in 0.08210 and higher)
-  my $stringifiable = 0;
-
   for my $i (0.. $#$bindattrs) {
-
-    $stringifiable++ if ( length ref $bind->[$i][1] and overload::Method($bind->[$i][1], '""') );
-
     if (
       defined $bindattrs->[$i]
         and
@@ -358,14 +357,6 @@ sub _dbi_attrs_for_bind {
     }
   }
 
-  carp_unique(
-    'POSSIBLE *PAST* DATA CORRUPTION detected - see '
-  . 'DBIx::Class::Storage::DBI::SQLite/RT79576 or '
-  . 'http://v.gd/DBIC_SQLite_RT79576 for further details or set '
-  . '$ENV{DBIC_RT79576_NOWARN} to disable this warning. Trigger '
-  . 'condition encountered'
-  ) if (!$ENV{DBIC_RT79576_NOWARN} and $stringifiable > 1);
-
   return $bindattrs;
 }
 
@@ -394,14 +385,17 @@ sub connect_call_use_foreign_keys {
   );
 }
 
-1;
-
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
index 79a449e..9072b38 100644 (file)
@@ -2,6 +2,7 @@ package DBIx::Class::Storage::DBI::Sybase;
 
 use strict;
 use warnings;
+use DBIx::Class::_Util 'dbic_internal_try';
 use Try::Tiny;
 use namespace::clean;
 
@@ -21,28 +22,25 @@ L<DBD::Sybase>
 
 =cut
 
-sub _rebless {
-  my $self = shift;
+sub _rebless { shift->_determine_connector_driver('Sybase') }
 
-  my $dbtype;
-  try {
-    $dbtype = @{$self->_get_dbh->selectrow_arrayref(qq{sp_server_info \@attribute_id=1})}[2]
-  } catch {
-    $self->throw_exception("Unable to establish connection to determine database type: $_")
-  };
+sub _get_rdbms_name {
+  my $self = shift;
 
-  if ($dbtype) {
-    $dbtype =~ s/\W/_/gi;
+  dbic_internal_try {
+    my $name = $self->_get_dbh->selectrow_arrayref('sp_server_info @attribute_id=1')->[2];
 
-    # saner class name
-    $dbtype = 'ASE' if $dbtype eq 'SQL_Server';
+    if ($name) {
+      $name =~ s/\W/_/gi;
 
-    my $subclass = __PACKAGE__ . "::$dbtype";
-    if ($self->load_optional_class($subclass)) {
-      bless $self, $subclass;
-      $self->_rebless;
+      # saner class name
+      $name = 'ASE' if $name eq 'SQL_Server';
     }
-  }
+
+    $name;  # RV
+  } catch {
+    $self->throw_exception("Unable to establish connection to determine database type: $_")
+  };
 }
 
 sub _init {
@@ -80,9 +78,9 @@ sub _ping {
 
 # FIXME if the main connection goes stale, does opening another for this statement
 # really determine anything?
-
+# FIXME (2) THIS MAKES 0 SENSE!!! Need to test later
   if ($dbh->{syb_no_child_con}) {
-    return try {
+    return dbic_internal_try {
       $self->_connect->do('select 1');
       1;
     }
@@ -91,13 +89,14 @@ sub _ping {
     };
   }
 
-  return try {
-    $dbh->do('select 1');
-    1;
-  }
-  catch {
-    0;
-  };
+  return (
+    (dbic_internal_try {
+      $dbh->do('select 1');
+      1;
+    })
+      ? 1
+      : 0
+  );
 }
 
 sub _set_max_connect {
@@ -131,14 +130,18 @@ sub _using_freetds_version {
   return $inf =~ /v([0-9\.]+)/ ? $1 : 0;
 }
 
-1;
+=head1 FURTHER QUESTIONS?
 
-=head1 AUTHORS
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-See L<DBIx::Class/CONTRIBUTORS>.
+=head1 COPYRIGHT AND LICENSE
 
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
index 50a8f6b..3479ff3 100644 (file)
@@ -16,7 +16,7 @@ use Sub::Name();
 use Data::Dumper::Concise 'Dumper';
 use Try::Tiny;
 use Context::Preserve 'preserve_context';
-use DBIx::Class::_Util 'sigwarn_silencer';
+use DBIx::Class::_Util qw( sigwarn_silencer dbic_internal_try );
 use namespace::clean;
 
 __PACKAGE__->sql_limit_dialect ('GenericSubQ');
@@ -179,7 +179,7 @@ sub disconnect {
 
 # Even though we call $sth->finish for uses off the bulk API, there's still an
 # "active statement" warning on disconnect, which we throw away here.
-# This is due to the bug described in insert_bulk.
+# This is due to the bug described in _insert_bulk.
 # Currently a noop because 'prepare' is used instead of 'prepare_cached'.
   local $SIG{__WARN__} = sigwarn_silencer(qr/active statement/i)
     if $self->_is_bulk_storage;
@@ -233,7 +233,7 @@ Also sets the C<log_on_update> value for blob write operations. The default is
 C<1>, but C<0> is better if your database is configured for it.
 
 See
-L<DBD::Sybase/Handling_IMAGE/TEXT_data_with_syb_ct_get_data()/syb_ct_send_data()>.
+L<DBD::Sybase/Handling IMAGE/TEXT data with syb_ct_get_data()/syb_ct_send_data()>.
 
 =cut
 
@@ -256,23 +256,6 @@ sub _is_lob_column {
 sub _prep_for_execute {
   my ($self, $op, $ident, $args) = @_;
 
-  #
-### This is commented out because all tests pass. However I am leaving it
-### here as it may prove necessary (can't think through all combinations)
-### BTW it doesn't currently work exactly - need better sensitivity to
-  # currently set value
-  #
-  #my ($op, $ident) = @_;
-  #
-  # inherit these from the parent for the duration of _prep_for_execute
-  # Don't know how to make a localizing loop with if's, otherwise I would
-  #local $self->{_autoinc_supplied_for_op}
-  #  = $self->_parent_storage->_autoinc_supplied_for_op
-  #if ($op eq 'insert' or $op eq 'update') and $self->_parent_storage;
-  #local $self->{_perform_autoinc_retrieval}
-  #  = $self->_parent_storage->_perform_autoinc_retrieval
-  #if ($op eq 'insert' or $op eq 'update') and $self->_parent_storage;
-
   my $limit;  # extract and use shortcut on limit without offset
   if ($op eq 'select' and ! $args->[4] and $limit = $args->[3]) {
     $args = [ @$args ];
@@ -353,10 +336,12 @@ sub insert {
 
   my $columns_info = $source->columns_info;
 
-  my $identity_col =
-    (first { $columns_info->{$_}{is_auto_increment} }
-      keys %$columns_info )
-    || '';
+  my ($identity_col) = grep
+    { $columns_info->{$_}{is_auto_increment} }
+    keys %$columns_info
+  ;
+
+  $identity_col = '' if ! defined $identity_col;
 
   # FIXME - this is duplication from DBI.pm. When refactored towards
   # the LobWriter this can be folded back where it belongs.
@@ -364,10 +349,10 @@ sub insert {
     ? 1
     : 0
   ;
-  local $self->{_perform_autoinc_retrieval} =
-    ($identity_col and ! exists $to_insert->{$identity_col})
-      ? $identity_col
-      : undef
+
+  local $self->{_perform_autoinc_retrieval} = $self->{_autoinc_supplied_for_op}
+    ? undef
+    : $identity_col
   ;
 
   # check for empty insert
@@ -391,53 +376,42 @@ sub insert {
 
   my $blob_cols = $self->_remove_blob_cols($source, $to_insert);
 
-  # do we need the horrific SELECT MAX(COL) hack?
-  my $need_dumb_last_insert_id = (
-    $self->_perform_autoinc_retrieval
-      &&
-    ($self->_identity_method||'') ne '@@IDENTITY'
-  );
-
-  my $next = $self->next::can;
-
-  # we are already in a transaction, or there are no blobs
-  # and we don't need the PK - just (try to) do it
-  if ($self->{transaction_depth}
-        || (!$blob_cols && !$need_dumb_last_insert_id)
+  # if a new txn is needed - it must happen on the _writer/new connection (for now)
+  my $guard;
+  if (
+    ! $self->transaction_depth
+      and
+    (
+      $blob_cols
+        or
+      # do we need the horrific SELECT MAX(COL) hack?
+      (
+        $self->_perform_autoinc_retrieval
+          and
+        ( ($self->_identity_method||'') ne '@@IDENTITY' )
+      )
+    )
   ) {
-    return $self->_insert (
-      $next, $source, $to_insert, $blob_cols, $identity_col
-    );
+    $self = $self->_writer_storage;
+    $guard = $self->txn_scope_guard;
   }
 
-  # otherwise use the _writer_storage to do the insert+transaction on another
-  # connection
-  my $guard = $self->_writer_storage->txn_scope_guard;
-
-  my $updated_cols = $self->_writer_storage->_insert (
-    $next, $source, $to_insert, $blob_cols, $identity_col
-  );
-
-  $self->_identity($self->_writer_storage->_identity);
+  my $updated_cols = $self->next::method ($source, $to_insert);
 
-  $guard->commit;
-
-  return $updated_cols;
-}
-
-sub _insert {
-  my ($self, $next, $source, $to_insert, $blob_cols, $identity_col) = @_;
-
-  my $updated_cols = $self->$next ($source, $to_insert);
-
-  my $final_row = {
-    ($identity_col ?
-      ($identity_col => $self->last_insert_id($source, $identity_col)) : ()),
-    %$to_insert,
-    %$updated_cols,
-  };
+  $self->_insert_blobs (
+    $source,
+    $blob_cols,
+    {
+      ( $identity_col
+        ? ( $identity_col => $self->last_insert_id($source, $identity_col) )
+        : ()
+      ),
+      %$to_insert,
+      %$updated_cols,
+    },
+  ) if $blob_cols;
 
-  $self->_insert_blobs ($source, $blob_cols, $final_row) if $blob_cols;
+  $guard->commit if $guard;
 
   return $updated_cols;
 }
@@ -501,7 +475,7 @@ sub update {
   }
 }
 
-sub insert_bulk {
+sub _insert_bulk {
   my $self = shift;
   my ($source, $cols, $data) = @_;
 
@@ -535,10 +509,10 @@ sub insert_bulk {
 
 # next::method uses a txn anyway, but it ends too early in case we need to
 # select max(col) to get the identity for inserting blobs.
-    ($self, my $guard) = $self->{transaction_depth} == 0 ?
-      ($self->_writer_storage, $self->_writer_storage->txn_scope_guard)
-      :
-      ($self, undef);
+    ($self, my $guard) = $self->transaction_depth
+      ? ($self, undef)
+      : ($self->_writer_storage, $self->_writer_storage->txn_scope_guard)
+    ;
 
     $self->next::method(@_);
 
@@ -607,7 +581,7 @@ sub insert_bulk {
 # This ignores any data conversion errors detected by the client side libs, as
 # they are usually harmless.
   my $orig_cslib_cb = DBD::Sybase::set_cslib_cb(
-    Sub::Name::subname insert_bulk => sub {
+    Sub::Name::subname _insert_bulk_cslib_errhandler => sub {
       my ($layer, $origin, $severity, $errno, $errmsg, $osmsg, $blkmsg) = @_;
 
       return 1 if $errno == 36;
@@ -622,7 +596,7 @@ sub insert_bulk {
   });
 
   my $exception = '';
-  try {
+  dbic_internal_try {
     my $bulk = $self->_bulk_storage;
 
     my $guard = $bulk->txn_scope_guard;
@@ -678,14 +652,14 @@ sub insert_bulk {
 
   if ($exception =~ /-Y option/) {
     my $w = 'Sybase bulk API operation failed due to character set incompatibility, '
-          . 'reverting to regular array inserts. Try unsetting the LANG environment variable'
+          . 'reverting to regular array inserts. Try unsetting the LC_ALL environment variable'
     ;
     $w .= "\n$exception" if $self->debug;
     carp $w;
 
     $self->_bulk_storage(undef);
     unshift @_, $self;
-    goto \&insert_bulk;
+    goto \&_insert_bulk;
   }
   elsif ($exception) {
 # rollback makes the bulkLogin connection unusable
@@ -709,7 +683,8 @@ sub _remove_blob_cols {
       }
       else {
         $fields->{$col} = \"''";
-        $blob_cols{$col} = $blob_val unless $blob_val eq '';
+        $blob_cols{$col} = $blob_val
+          if length $blob_val;
       }
     }
   }
@@ -717,7 +692,7 @@ sub _remove_blob_cols {
   return %blob_cols ? \%blob_cols : undef;
 }
 
-# same for insert_bulk
+# same for _insert_bulk
 sub _remove_blob_cols_array {
   my ($self, $source, $cols, $data) = @_;
 
@@ -735,7 +710,7 @@ sub _remove_blob_cols_array {
         else {
           $data->[$j][$i] = \"''";
           $blob_cols[$j][$i] = $blob_val
-            unless $blob_val eq '';
+            if length $blob_val;
         }
       }
     }
@@ -747,7 +722,7 @@ sub _remove_blob_cols_array {
 sub _update_blobs {
   my ($self, $source, $blob_cols, $where) = @_;
 
-  my @primary_cols = try
+  my @primary_cols = dbic_internal_try
     { $source->_pri_cols_or_die }
     catch {
       $self->throw_exception("Cannot update TEXT/IMAGE column(s): $_")
@@ -757,7 +732,7 @@ sub _update_blobs {
   if (
     ref $where eq 'HASH'
       and
-    @primary_cols == grep { defined $where->{$_} } @primary_cols
+    ! grep { ! defined $where->{$_} } @primary_cols
   ) {
     my %row_to_update;
     @row_to_update{@primary_cols} = @{$where}{@primary_cols};
@@ -776,26 +751,29 @@ sub _update_blobs {
 }
 
 sub _insert_blobs {
-  my ($self, $source, $blob_cols, $row) = @_;
-  my $dbh = $self->_get_dbh;
+  my ($self, $source, $blob_cols, $row_data) = @_;
 
   my $table = $source->name;
 
-  my %row = %$row;
-  my @primary_cols = try
+  my @primary_cols = dbic_internal_try
     { $source->_pri_cols_or_die }
     catch {
       $self->throw_exception("Cannot update TEXT/IMAGE column(s): $_")
     };
 
   $self->throw_exception('Cannot update TEXT/IMAGE column(s) without primary key values')
-    if ((grep { defined $row{$_} } @primary_cols) != @primary_cols);
+    if grep { ! defined $row_data->{$_} } @primary_cols;
+
+  # if we are 2-phase inserting a blob - there is nothing to retrieve anymore,
+  # regardless of the previous state of the flag
+  local $self->{_perform_autoinc_retrieval}
+    if $self->_perform_autoinc_retrieval;
+
+  my %where = map {( $_ => $row_data->{$_} )} @primary_cols;
 
   for my $col (keys %$blob_cols) {
     my $blob = $blob_cols->{$col};
 
-    my %where = map { ($_, $row{$_}) } @primary_cols;
-
     my $cursor = $self->select ($source, [$col], \%where, {});
     $cursor->next;
     my $sth = $cursor->sth;
@@ -807,7 +785,7 @@ sub _insert_blobs {
       );
     }
 
-    try {
+    dbic_internal_try {
       do {
         $sth->func('CS_GET', 1, 'ct_data_info') or die $sth->errstr;
       } while $sth->fetch;
@@ -1082,15 +1060,15 @@ for L<DBIx::Class::InflateColumn::DateTime>.
 
 =head1 LIMITED QUERIES
 
-Because ASE does not have a good way to limit results in SQL that works for all
-types of queries, the limit dialect is set to
-L<GenericSubQ|SQL::Abstract::Limit/GenericSubQ>.
+Because ASE does not have a good way to limit results in SQL that works for
+all types of queries, the limit dialect is set to
+L<GenericSubQ|DBIx::Class::SQLMaker::LimitDialects/GenericSubQ>.
 
 Fortunately, ASE and L<DBD::Sybase> support cursors properly, so when
-L<GenericSubQ|SQL::Abstract::Limit/GenericSubQ> is too slow you can use
-the L<software_limit|DBIx::Class::ResultSet/software_limit>
-L<DBIx::Class::ResultSet> attribute to simulate limited queries by skipping over
-records.
+L<GenericSubQ|DBIx::Class::SQLMaker::LimitDialects/GenericSubQ> is too slow
+you can use the L<software_limit|DBIx::Class::ResultSet/software_limit>
+L<DBIx::Class::ResultSet> attribute to simulate limited queries by skipping
+over records.
 
 =head1 TEXT/IMAGE COLUMNS
 
@@ -1130,7 +1108,7 @@ L<populate|DBIx::Class::ResultSet/populate> call, eg.:
 
 B<NOTE:> the L<add_columns|DBIx::Class::ResultSource/add_columns>
 calls in your C<Result> classes B<must> list columns in database order for this
-to work. Also, you may have to unset the C<LANG> environment variable before
+to work. Also, you may have to unset the C<LC_ALL> environment variable before
 loading your app, as C<BCP -Y> is not yet supported in DBD::Sybase .
 
 When inserting IMAGE columns using this method, you'll need to use
@@ -1196,13 +1174,13 @@ bulk_insert using prepare_cached (see comments.)
 
 =back
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
-
-=cut
-# vim:sts=2 sw=2:
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index b5ade31..ffd72c4 100644 (file)
@@ -81,8 +81,10 @@ You can also enable this driver explicitly using:
   $schema->storage_type('::DBI::Sybase::ASE::NoBindVars');
   $schema->connect($dsn, $user, $pass, \%opts);
 
-See the discussion in L<< DBD::Sybase/Using ? Placeholders & bind parameters to
-$sth->execute >> for details on the pros and cons of using placeholders.
+See the discussion in
+L<< DBD::Sybase/Using ? Placeholders & bind parameters to $sth->execute >>
+for details on the pros and cons of using placeholders with this particular
+driver.
 
 One advantage of not using placeholders is that C<select @@identity> will work
 for obtaining the last insert id of an C<IDENTITY> column, instead of having to
@@ -94,13 +96,13 @@ course) into the SQL query itself, without using placeholders.
 The caching of prepared statements is also explicitly disabled, as the
 interpolation renders it useless.
 
-=head1 AUTHORS
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
-
-=cut
-# vim:sts=2 sw=2:
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 341c1e3..9db543c 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 use warnings;
 use base qw/DBIx::Class::Storage::DBI::Sybase/;
 use mro 'c3';
-use Try::Tiny;
+use DBIx::Class::_Util 'dbic_internal_try';
 use namespace::clean;
 
 =head1 NAME
@@ -67,7 +67,7 @@ sub set_textsize {
   my $text_size =
     shift
       ||
-    try { $self->_dbic_cinnect_attributes->{LongReadLen} }
+    dbic_internal_try { $self->_dbic_connect_attributes->{LongReadLen} }
       ||
     32768; # the DBD::Sybase default
 
@@ -103,14 +103,17 @@ sub _exec_txn_rollback {
   $dbh->do('ROLLBACK');
 }
 
-1;
-
-=head1 AUTHORS
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
index bb3da24..2cdad77 100644 (file)
@@ -38,12 +38,13 @@ This subclass supports MSSQL connected via L<DBD::Sybase>.
   $schema->storage_type('::DBI::Sybase::MSSQL');
   $schema->connect_info('dbi:Sybase:....', ...);
 
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
-
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 12dfa5b..ea03b05 100644 (file)
@@ -171,14 +171,18 @@ sub format_datetime {
   return $datetime_formatter->format_datetime(shift);
 }
 
-1;
-
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
+
index 5d266bc..e616b4c 100644 (file)
@@ -43,12 +43,13 @@ disabled, as the interpolation renders it useless.
 In all other respects, it is a subclass of
 L<DBIx::Class::Storage::DBI::Sybase::Microsoft_SQL_Server>.
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
-
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 6d48a4a..e6f3466 100644 (file)
@@ -106,13 +106,16 @@ sub _prefetch_autovalues  {
   return $self->next::method(@_);
 }
 
-=head1 AUTHOR
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 83ee8b2..1e76d6b 100644 (file)
@@ -44,13 +44,19 @@ sub _prep_for_execute {
   return $self->next::method(@_) if ( $_[0] eq 'select' or $_[0] eq 'insert' );
 
 
-  # FIXME FIXME FIXME - this is a terrible, gross, incomplete hack
-  # it should be trivial for mst to port this to DQ (and a good
-  # exercise as well, since we do not yet have such wide tree walking
-  # in place). For the time being this will work in limited cases,
-  # mainly complex update/delete, which is really all we want it for
-  # currently (allows us to fix some bugs without breaking MySQL in
-  # the process, and is also crucial for Shadow to be usable)
+  # FIXME FIXME FIXME - this is a terrible, gross, incomplete, MySQL-specific
+  # hack but it works rather well for the limited amount of actual use cases
+  # which can not be done in any other way on MySQL. This allows us to fix
+  # some bugs without breaking MySQL support in the process and is also
+  # crucial for more complex things like Shadow to be usable
+  #
+  # This code is just a pre-analyzer, working in tandem with ::SQLMaker::MySQL,
+  # where the possibly-set value of {_modification_target_referenced_re} is
+  # used to demarcate which part of the final SQL to double-wrap in a subquery.
+  #
+  # This is covered extensively by "offline" tests, so when the DQ work
+  # resumes, this will get flagged. Afaik there are no AST-visitor code of that
+  # magnitude yet (Oct 2015) within DQ, so a good exercise overall.
 
   # extract the source name, construct modification indicator re
   my $sm = $self->sql_maker;
@@ -106,15 +112,17 @@ sub _run_connection_actions {
 sub sql_maker {
   my $self = shift;
 
-  unless ($self->_sql_maker) {
-    my $maker = $self->next::method (@_);
+  # it is critical to get the version *before* calling next::method
+  # otherwise the potential connect will obliterate the sql_maker
+  # next::method will populate in the _sql_maker accessor
+  my $mysql_ver = $self->_server_info->{normalized_dbms_version};
 
-    # mysql 3 does not understand a bare JOIN
-    my $mysql_ver = $self->_dbh_get_info('SQL_DBMS_VER');
-    $maker->{_default_jointype} = 'INNER' if $mysql_ver =~ /^3/;
-  }
+  my $sm = $self->next::method(@_);
+
+  # mysql 3 does not understand a bare JOIN
+  $sm->{_default_jointype} = 'INNER' if $mysql_ver < 4;
 
-  return $self->_sql_maker;
+  $sm;
 }
 
 sub sqlt_type {
@@ -204,12 +212,13 @@ Enables session-wide strict options upon connecting. Equivalent to:
     ]
   });
 
-=head1 AUTHORS
-
-See L<DBIx::Class/CONTRIBUTORS>
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 80283dc..14410b7 100644 (file)
@@ -2,9 +2,24 @@ package   #hide from PAUSE
   DBIx::Class::Storage::DBIHacks;
 
 #
-# This module contains code that should never have seen the light of day,
-# does not belong in the Storage, or is otherwise unfit for public
-# display. The arrival of SQLA2 should immediately obsolete 90% of this
+# This module contains code supporting a battery of special cases and tests for
+# many corner cases pushing the envelope of what DBIC can do. When work on
+# these utilities began in mid 2009 (51a296b402c) it wasn't immediately obvious
+# that these pieces, despite their misleading on-first-sighe-flakiness, will
+# become part of the generic query rewriting machinery of DBIC, allowing it to
+# both generate and process queries representing incredibly complex sets with
+# reasonable efficiency.
+#
+# Now (end of 2015), more than 6 years later the routines in this class have
+# stabilized enough, and are meticulously covered with tests, to a point where
+# an effort to formalize them into user-facing APIs might be worthwhile.
+#
+# An implementor working on publicizing and/or replacing the routines with a
+# more modern SQL generation framework should keep in mind that pretty much all
+# existing tests are constructed on the basis of real-world code used in
+# production somewhere.
+#
+# Please hack on this responsibly ;)
 #
 
 use strict;
@@ -15,7 +30,9 @@ use mro 'c3';
 
 use List::Util 'first';
 use Scalar::Util 'blessed';
-use Sub::Name 'subname';
+use DBIx::Class::_Util qw(UNRESOLVABLE_CONDITION serialize);
+use SQL::Abstract qw(is_plain_value is_literal_value);
+use DBIx::Class::Carp;
 use namespace::clean;
 
 #
@@ -38,7 +55,11 @@ sub _prune_unused_joins {
     $self->_use_join_optimizer
   );
 
-  my $orig_aliastypes = $self->_resolve_aliastypes_from_select_args($attrs);
+  my $orig_aliastypes =
+    $attrs->{_precalculated_aliastypes}
+      ||
+    $self->_resolve_aliastypes_from_select_args($attrs)
+  ;
 
   my $new_aliastypes = { %$orig_aliastypes };
 
@@ -110,8 +131,8 @@ sub _adjust_select_args_for_complex_prefetch {
   my $outer_attrs = { %$attrs };
   delete @{$outer_attrs}{qw(from bind rows offset group_by _grouped_by_distinct having)};
 
-  my $inner_attrs = { %$attrs };
-  delete @{$inner_attrs}{qw(for collapse select as _related_results_construction)};
+  my $inner_attrs = { %$attrs, _simple_passthrough_construction => 1 };
+  delete @{$inner_attrs}{qw(for collapse select as)};
 
   # there is no point of ordering the insides if there is no limit
   delete $inner_attrs->{order_by} if (
@@ -168,18 +189,27 @@ sub _adjust_select_args_for_complex_prefetch {
     push @{$inner_attrs->{as}}, $attrs->{as}[$i];
   }
 
-  # We will need to fetch all native columns in the inner subquery, which may
+  my $inner_aliastypes = $self->_resolve_aliastypes_from_select_args($inner_attrs);
+
+  # In the inner subq we will need to fetch *only* native columns which may
   # be a part of an *outer* join condition, or an order_by (which needs to be
   # preserved outside), or wheres. In other words everything but the inner
   # selector
   # We can not just fetch everything because a potential has_many restricting
   # join collapse *will not work* on heavy data types.
-  my $connecting_aliastypes = $self->_resolve_aliastypes_from_select_args({
-    %$inner_attrs,
-    select => [],
-  });
 
-  for (sort map { keys %{$_->{-seen_columns}||{}} } map { values %$_ } values %$connecting_aliastypes) {
+  # essentially a map of all non-selecting seen columns
+  # the sort is there for a nicer select list
+  for (
+    sort
+      map
+        { keys %{$_->{-seen_columns}||{}} }
+        map
+          { values %{$inner_aliastypes->{$_}} }
+          grep
+            { $_ ne 'selecting' }
+            keys %$inner_aliastypes
+  ) {
     my $ci = $colinfo->{$_} or next;
     if (
       $ci->{-source_alias} eq $root_alias
@@ -202,8 +232,11 @@ sub _adjust_select_args_for_complex_prefetch {
     local $self->{_use_join_optimizer} = 1;
 
     # throw away multijoins since we def. do not care about those inside the subquery
-    ($inner_attrs->{from}, my $inner_aliastypes) = $self->_prune_unused_joins ({
-      %$inner_attrs, _force_prune_multiplying_joins => 1
+    # $inner_aliastypes *will* be redefined at this point
+    ($inner_attrs->{from}, $inner_aliastypes ) = $self->_prune_unused_joins ({
+      %$inner_attrs,
+      _force_prune_multiplying_joins => 1,
+      _precalculated_aliastypes => $inner_aliastypes,
     });
 
     # uh-oh a multiplier (which is not us) left in, this is a problem for limits
@@ -328,27 +361,53 @@ sub _adjust_select_args_for_complex_prefetch {
     });
   }
 
-  # This is totally horrific - the {where} ends up in both the inner and outer query
-  # Unfortunately not much can be done until SQLA2 introspection arrives, and even
-  # then if where conditions apply to the *right* side of the prefetch, you may have
-  # to both filter the inner select (e.g. to apply a limit) and then have to re-filter
-  # the outer select to exclude joins you didn't want in the first place
+  # FIXME: The {where} ends up in both the inner and outer query, i.e. *twice*
+  #
+  # This is rather horrific, and while we currently *do* have enough
+  # introspection tooling available to attempt a stab at properly deciding
+  # whether or not to include the where condition on the outside, the
+  # machinery is still too slow to apply it here.
+  # Thus for the time being we do not attempt any sanitation of the where
+  # clause and just pass it through on both sides of the subquery. This *will*
+  # be addressed at a later stage, most likely after folding the SQL generator
+  # into SQLMaker proper
   #
   # OTOH it can be seen as a plus: <ash> (notes that this query would make a DBA cry ;)
+  #
   return $outer_attrs;
 }
 
+# This is probably the ickiest, yet most relied upon part of the codebase:
+# this is the place where we take arbitrary SQL input and break it into its
+# constituent parts, making sure we know which *sources* are used in what
+# *capacity* ( selecting / restricting / grouping / ordering / joining, etc )
+# Although the method is pretty horrific, the worst thing that can happen is
+# for a classification failure, which in turn will result in a vocal exception,
+# and will lead to a relatively prompt fix.
+# The code has been slowly improving and is covered with a formiddable battery
+# of tests, so can be considered "reliably stable" at this point (Oct 2015).
+#
+# A note to implementors attempting to "replace" this - keep in mind that while
+# there are multiple optimization avenues, the actual "scan literal elements"
+# part *MAY NEVER BE REMOVED*, even if it is limited only ot the (future) AST
+# nodes that are deemed opaque (i.e. contain literal expressions). The use of
+# blackbox literals is at this point firmly a user-facing API, and is one of
+# *the* reasons DBIC remains as flexible as it is. In other words, when working
+# on this keep in mind that the following is widespread and *encouraged* way
+# of using DBIC in the wild when push comes to shove:
 #
-# I KNOW THIS SUCKS! GET SQLA2 OUT THE DOOR SO THIS CAN DIE!
+# $rs->search( {}, {
+#   select => \[ $random, @stuff],
+#   from => \[ $random, @stuff ],
+#   where => \[ $random, @stuff ],
+#   group_by => \[ $random, @stuff ],
+#   order_by => \[ $random, @stuff ],
+# } )
+#
+# Various incarnations of the above are reflected in many of the tests. If one
+# gets to fail, you get to fix it. A "this is crazy, nobody does that" is not
+# acceptable going forward.
 #
-# Due to a lack of SQLA2 we fall back to crude scans of all the
-# select/where/order/group attributes, in order to determine what
-# aliases are needed to fulfill the query. This information is used
-# throughout the code to prune unnecessary JOINs from the queries
-# in an attempt to reduce the execution time.
-# Although the method is pretty horrific, the worst thing that can
-# happen is for it to fail due to some scalar SQL, which in turn will
-# result in a vocal exception.
 sub _resolve_aliastypes_from_select_args {
   my ( $self, $attrs ) = @_;
 
@@ -389,7 +448,6 @@ sub _resolve_aliastypes_from_select_args {
   my $sql_maker = $self->sql_maker;
 
   # these are throw away results, do not pollute the bind stack
-  local $sql_maker->{select_bind};
   local $sql_maker->{where_bind};
   local $sql_maker->{group_bind};
   local $sql_maker->{having_bind};
@@ -416,7 +474,7 @@ sub _resolve_aliastypes_from_select_args {
   # generate sql chunks
   my $to_scan = {
     restricting => [
-      $sql_maker->_recurse_where ($attrs->{where}),
+      ($sql_maker->_recurse_where ($attrs->{where}))[0],
       $sql_maker->_parse_rs_attrs ({ having => $attrs->{having} }),
     ],
     grouping => [
@@ -429,111 +487,143 @@ sub _resolve_aliastypes_from_select_args {
       ),
     ],
     selecting => [
-      map { $sql_maker->_recurse_fields($_) } @{$attrs->{select}},
+      # kill all selectors which look like a proper subquery
+      # this is a sucky heuristic *BUT* - if we get it wrong the query will simply
+      # fail to run, so we are relatively safe
+      grep
+        { $_ !~ / \A \s* \( \s* SELECT \s+ .+? \s+ FROM \s+ .+? \) \s* \z /xsi }
+        map
+          { ($sql_maker->_recurse_fields($_))[0] }
+          @{$attrs->{select}}
     ],
-    ordering => [
-      map { $_->[0] } $self->_extract_order_criteria ($attrs->{order_by}, $sql_maker),
+    ordering => [ map
+      {
+        ( my $sql = (ref $_ ? $_->[0] : $_) ) =~ s/ \s+ (?: ASC | DESC ) \s* \z //xi;
+        $sql;
+      }
+      $sql_maker->_order_by_chunks( $attrs->{order_by} ),
     ],
   };
 
-  # throw away empty chunks and all 2-value arrayrefs: the thinking is that these are
-  # bind value specs left in by the sloppy renderer above. It is ok to do this
-  # at this point, since we are going to end up rewriting this crap anyway
-  for my $v (values %$to_scan) {
-    my @nv;
-    for (@$v) {
-      next if (
-        ! defined $_
-          or
-        (
-          ref $_ eq 'ARRAY'
-            and
-          ( @$_ == 0 or @$_ == 2 )
-        )
-      );
+  # we will be bulk-scanning anyway - pieces will not matter in that case,
+  # thus join everything up
+  # throw away empty-string chunks, and make sure no binds snuck in
+  # note that we operate over @{$to_scan->{$type}}, hence the
+  # semi-mindbending ... map ... for values ...
+  ( $_ = join ' ', map {
+
+    ( ! defined $_ )  ? ()
+  : ( length ref $_ ) ? (require Data::Dumper::Concise && $self->throw_exception(
+                          "Unexpected ref in scan-plan: " . Data::Dumper::Concise::Dumper($_)
+                        ))
+  : ( $_ =~ /^\s*$/ ) ? ()
+                      : $_
+
+  } @$_ ) for values %$to_scan;
+
+  # throw away empty to-scan's
+  (
+    length $to_scan->{$_}
+      or
+    delete $to_scan->{$_}
+  ) for keys %$to_scan;
 
-      if (ref $_) {
-        require Data::Dumper::Concise;
-        $self->throw_exception("Unexpected ref in scan-plan: " . Data::Dumper::Concise::Dumper($v) );
-      }
 
-      push @nv, $_;
-    }
 
-    $v = \@nv;
-  }
+  # these will be used for matching in the loop below
+  my $all_aliases = join ' | ', map { quotemeta $_ } keys %$alias_list;
+  my $fq_col_re = qr/
+    $lquote ( $all_aliases ) $rquote $sep (?: $lquote ([^$rquote]+) $rquote )?
+         |
+    \b ( $all_aliases ) \. ( [^\s\)\($rquote]+ )?
+  /x;
 
-  # kill all selectors which look like a proper subquery
-  # this is a sucky heuristic *BUT* - if we get it wrong the query will simply
-  # fail to run, so we are relatively safe
-  $to_scan->{selecting} = [ grep {
-    $_ !~ / \A \s* \( \s* SELECT \s+ .+? \s+ FROM \s+ .+? \) \s* \z /xsi
-  } @{ $to_scan->{selecting} || [] } ];
 
-  # first see if we have any exact matches (qualified or unqualified)
+  my $all_unq_columns = join ' | ',
+    map
+      { quotemeta $_ }
+      grep
+        # using a regex here shows up on profiles, boggle
+        { index( $_, '.') < 0 }
+        keys %$colinfo
+  ;
+  my $unq_col_re = $all_unq_columns
+    ? qr/
+      $lquote ( $all_unq_columns ) $rquote
+        |
+      (?: \A | \s ) ( $all_unq_columns ) (?: \s | \z )
+    /x
+    : undef
+  ;
+
+
+  # the actual scan, per type
   for my $type (keys %$to_scan) {
-    for my $piece (@{$to_scan->{$type}}) {
-      if ($colinfo->{$piece} and my $alias = $colinfo->{$piece}{-source_alias}) {
-        $aliases_by_type->{$type}{$alias} ||= { -parents => $alias_list->{$alias}{-join_path}||[] };
-        $aliases_by_type->{$type}{$alias}{-seen_columns}{$colinfo->{$piece}{-fq_colname}} = $piece;
-      }
-    }
-  }
 
-  # now loop through all fully qualified columns and get the corresponding
-  # alias (should work even if they are in scalarrefs)
-  for my $alias (keys %$alias_list) {
-    my $al_re = qr/
-      $lquote $alias $rquote $sep (?: $lquote ([^$rquote]+) $rquote )?
-        |
-      \b $alias \. ([^\s\)\($rquote]+)?
-    /x;
-
-    for my $type (keys %$to_scan) {
-      for my $piece (@{$to_scan->{$type}}) {
-        if (my @matches = $piece =~ /$al_re/g) {
-          $aliases_by_type->{$type}{$alias} ||= { -parents => $alias_list->{$alias}{-join_path}||[] };
-          $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = "$alias.$_"
-            for grep { defined $_ } @matches;
-        }
+
+    # now loop through all fully qualified columns and get the corresponding
+    # alias (should work even if they are in scalarrefs)
+    #
+    # The regex captures in multiples of 4, with one of the two pairs being
+    # undef. There may be a *lot* of matches, hence the convoluted loop
+    my @matches = $to_scan->{$type} =~ /$fq_col_re/g;
+    my $i = 0;
+    while( $i < $#matches ) {
+
+      if (
+        defined $matches[$i]
+      ) {
+        $aliases_by_type->{$type}{$matches[$i]} ||= { -parents => $alias_list->{$matches[$i]}{-join_path}||[] };
+
+        $aliases_by_type->{$type}{$matches[$i]}{-seen_columns}{"$matches[$i].$matches[$i+1]"} = "$matches[$i].$matches[$i+1]"
+          if defined $matches[$i+1];
+
+        $i += 2;
       }
+
+      $i += 2;
     }
-  }
 
-  # now loop through unqualified column names, and try to locate them within
-  # the chunks
-  for my $col (keys %$colinfo) {
-    next if $col =~ / \. /x;   # if column is qualified it was caught by the above
 
-    my $col_re = qr/ $lquote ($col) $rquote /x;
+    # now loop through unqualified column names, and try to locate them within
+    # the chunks, if there are any unqualified columns in the 1st place
+    next unless $unq_col_re;
 
-    for my $type (keys %$to_scan) {
-      for my $piece (@{$to_scan->{$type}}) {
-        if ( my @matches = $piece =~ /$col_re/g) {
-          my $alias = $colinfo->{$col}{-source_alias};
-          $aliases_by_type->{$type}{$alias} ||= { -parents => $alias_list->{$alias}{-join_path}||[] };
-          $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = $_
-            for grep { defined $_ } @matches;
-        }
-      }
+    # The regex captures in multiples of 2, one of the two being undef
+    for ( $to_scan->{$type} =~ /$unq_col_re/g ) {
+      defined $_ or next;
+      my $alias = $colinfo->{$_}{-source_alias} or next;
+      $aliases_by_type->{$type}{$alias} ||= { -parents => $alias_list->{$alias}{-join_path}||[] };
+      $aliases_by_type->{$type}{$alias}{-seen_columns}{"$alias.$_"} = $_
     }
   }
 
+
   # Add any non-left joins to the restriction list (such joins are indeed restrictions)
-  for my $j (values %$alias_list) {
-    my $alias = $j->{-alias} or next;
-    $aliases_by_type->{restricting}{$alias} ||= { -parents => $j->{-join_path}||[] } if (
-      (not $j->{-join_type})
+  (
+    $_->{-alias}
+      and
+    ! $aliases_by_type->{restricting}{ $_->{-alias} }
+      and
+    (
+      not $_->{-join_type}
         or
-      ($j->{-join_type} !~ /^left (?: \s+ outer)? $/xi)
-    );
-  }
+      $_->{-join_type} !~ /^left (?: \s+ outer)? $/xi
+    )
+      and
+    $aliases_by_type->{restricting}{ $_->{-alias} } = { -parents => $_->{-join_path}||[] }
+  ) for values %$alias_list;
 
-  for (keys %$aliases_by_type) {
-    delete $aliases_by_type->{$_} unless keys %{$aliases_by_type->{$_}};
-  }
 
-  return $aliases_by_type;
+  # final cleanup
+  (
+    keys %{$aliases_by_type->{$_}}
+      or
+    delete $aliases_by_type->{$_}
+  ) for keys %$aliases_by_type;
+
+
+  $aliases_by_type;
 }
 
 # This is the engine behind { distinct => 1 } and the general
@@ -629,12 +719,7 @@ sub _group_over_selection {
       # of the external order and convert them to MIN(X) for ASC or MAX(X)
       # for DESC, and group_by the root columns. The end result should be
       # exactly what we expect
-
-      # FIXME - this code is a joke, will need to be completely rewritten in
-      # the DQ branch. But I need to push a POC here, otherwise the
-      # pesky tests won't pass
-      # wrap any part of the order_by that "responds" to an ordering alias
-      # into a MIN/MAX
+      #
       $sql_maker ||= $self->sql_maker;
       $order_chunks ||= [
         map { ref $_ eq 'ARRAY' ? $_ : [ $_ ] } $sql_maker->_order_by_chunks($attrs->{order_by})
@@ -642,6 +727,8 @@ sub _group_over_selection {
 
       my ($chunk, $is_desc) = $sql_maker->_split_order_chunk($order_chunks->[$o_idx][0]);
 
+      # we reached that far - wrap any part of the order_by that "responded"
+      # to an ordering alias into a MIN/MAX
       $new_order_by[$o_idx] = \[
         sprintf( '%s( %s )%s',
           ($is_desc ? 'MAX' : 'MIN'),
@@ -654,9 +741,10 @@ sub _group_over_selection {
   }
 
   $self->throw_exception ( sprintf
-    'A required group_by clause could not be constructed automatically due to a complex '
-  . 'order_by criteria (%s). Either order_by columns only (no functions) or construct a suitable '
-  . 'group_by by hand',
+    'Unable to programatically derive a required group_by from the supplied '
+  . 'order_by criteria. To proceed either add an explicit group_by, or '
+  . 'simplify your order_by to only include plain columns '
+  . '(supplied order_by: %s)',
     join ', ', map { "'$_'" } @$leftovers,
   ) if $leftovers;
 
@@ -710,53 +798,66 @@ sub _resolve_ident_sources {
 # for all sources
 sub _resolve_column_info {
   my ($self, $ident, $colnames) = @_;
-  my $alias2src = $self->_resolve_ident_sources($ident);
+
+  return {} if $colnames and ! @$colnames;
+
+  my $sources = $self->_resolve_ident_sources($ident);
+
+  $_ = { rsrc => $_, colinfos => $_->columns_info }
+    for values %$sources;
 
   my (%seen_cols, @auto_colnames);
 
   # compile a global list of column names, to be able to properly
   # disambiguate unqualified column names (if at all possible)
-  for my $alias (keys %$alias2src) {
-    my $rsrc = $alias2src->{$alias};
-    for my $colname ($rsrc->columns) {
-      push @{$seen_cols{$colname}}, $alias;
-      push @auto_colnames, "$alias.$colname" unless $colnames;
-    }
+  for my $alias (keys %$sources) {
+    (
+      ++$seen_cols{$_}{$alias}
+        and
+      ! $colnames
+        and
+      push @auto_colnames, "$alias.$_"
+    ) for keys %{ $sources->{$alias}{colinfos} };
   }
 
   $colnames ||= [
     @auto_colnames,
-    grep { @{$seen_cols{$_}} == 1 } (keys %seen_cols),
+    ( grep { keys %{$seen_cols{$_}} == 1 } keys %seen_cols ),
   ];
 
-  my (%return, $colinfos);
-  foreach my $col (@$colnames) {
-    my ($source_alias, $colname) = $col =~ m/^ (?: ([^\.]+) \. )? (.+) $/x;
-
-    # if the column was seen exactly once - we know which rsrc it came from
-    $source_alias ||= $seen_cols{$colname}[0]
-      if ($seen_cols{$colname} and @{$seen_cols{$colname}} == 1);
+  my %return;
+  for (@$colnames) {
+    my ($colname, $source_alias) = reverse split /\./, $_;
 
-    next unless $source_alias;
+    my $assumed_alias =
+      $source_alias
+        ||
+      # if the column was seen exactly once - we know which rsrc it came from
+      (
+        $seen_cols{$colname}
+          and
+        keys %{$seen_cols{$colname}} == 1
+          and
+        ( %{$seen_cols{$colname}} )[0]
+      )
+        ||
+      next
+    ;
 
-    my $rsrc = $alias2src->{$source_alias}
-      or next;
+    $self->throw_exception(
+      "No such column '$colname' on source " . $sources->{$assumed_alias}{rsrc}->source_name
+    ) unless $seen_cols{$colname}{$assumed_alias};
 
-    $return{$col} = {
-      %{
-          ( $colinfos->{$source_alias} ||= $rsrc->columns_info )->{$colname}
-            ||
-          $self->throw_exception(
-            "No such column '$colname' on source " . $rsrc->source_name
-          );
-      },
-      -result_source => $rsrc,
-      -source_alias => $source_alias,
-      -fq_colname => $col eq $colname ? "$source_alias.$col" : $col,
+    $return{$_} = {
+      %{ $sources->{$assumed_alias}{colinfos}{$colname} },
+      -result_source => $sources->{$assumed_alias}{rsrc},
+      -source_alias => $assumed_alias,
+      -fq_colname => "$assumed_alias.$colname",
       -colname => $colname,
     };
 
-    $return{"$source_alias.$colname"} = $return{$col} if $col eq $colname;
+    $return{"$assumed_alias.$colname"} = $return{$_}
+      unless $source_alias;
   }
 
   return \%return;
@@ -780,31 +881,9 @@ sub _resolve_column_info {
 sub _inner_join_to_node {
   my ($self, $from, $alias) = @_;
 
-  # subqueries and other oddness are naturally not supported
-  return $from if (
-    ref $from ne 'ARRAY'
-      ||
-    @$from <= 1
-      ||
-    ref $from->[0] ne 'HASH'
-      ||
-    ! $from->[0]{-alias}
-      ||
-    $from->[0]{-alias} eq $alias  # this last bit means $alias is the head of $from - nothing to do
-  );
+  my $switch_branch = $self->_find_join_path_to_node($from, $alias);
 
-  # find the current $alias in the $from structure
-  my $switch_branch;
-  JOINSCAN:
-  for my $j (@{$from}[1 .. $#$from]) {
-    if ($j->[0]{-alias} eq $alias) {
-      $switch_branch = $j->[0]{-join_path};
-      last JOINSCAN;
-    }
-  }
-
-  # something else went quite wrong
-  return $from unless $switch_branch;
+  return $from unless @{$switch_branch||[]};
 
   # So it looks like we will have to switch some stuff around.
   # local() is useless here as we will be leaving the scope
@@ -832,6 +911,29 @@ sub _inner_join_to_node {
   return \@new_from;
 }
 
+sub _find_join_path_to_node {
+  my ($self, $from, $target_alias) = @_;
+
+  # subqueries and other oddness are naturally not supported
+  return undef if (
+    ref $from ne 'ARRAY'
+      ||
+    ref $from->[0] ne 'HASH'
+      ||
+    ! defined $from->[0]{-alias}
+  );
+
+  # no path - the head is the alias
+  return [] if $from->[0]{-alias} eq $target_alias;
+
+  for my $i (1 .. $#$from) {
+    return $from->[$i][0]{-join_path} if ( ($from->[$i][0]{-alias}||'') eq $target_alias );
+  }
+
+  # something else went quite wrong
+  return undef;
+}
+
 sub _extract_order_criteria {
   my ($self, $order_by, $sql_maker) = @_;
 
@@ -881,15 +983,15 @@ sub _order_by_is_stable {
   my ($self, $ident, $order_by, $where) = @_;
 
   my @cols = (
-    (map { $_->[0] } $self->_extract_order_criteria($order_by)),
-    $where ? @{$self->_extract_fixed_condition_columns($where)} :(),
-  ) or return undef;
+    ( map { $_->[0] } $self->_extract_order_criteria($order_by) ),
+    ( $where ? keys %{ $self->_extract_fixed_condition_columns($where) } : () ),
+  ) or return 0;
 
   my $colinfo = $self->_resolve_column_info($ident, \@cols);
 
   return keys %$colinfo
     ? $self->_columns_comprise_identifying_set( $colinfo,  \@cols )
-    : undef
+    : 0
   ;
 }
 
@@ -905,115 +1007,501 @@ sub _columns_comprise_identifying_set {
     return 1 if $src->_identifying_column_set($_);
   }
 
-  return undef;
+  return 0;
 }
 
-# this is almost identical to the above, except it accepts only
+# this is almost similar to _order_by_is_stable, except it takes
 # a single rsrc, and will succeed only if the first portion of the order
 # by is stable.
 # returns that portion as a colinfo hashref on success
-sub _main_source_order_by_portion_is_stable {
-  my ($self, $main_rsrc, $order_by, $where) = @_;
+sub _extract_colinfo_of_stable_main_source_order_by_portion {
+  my ($self, $attrs) = @_;
+
+  my $nodes = $self->_find_join_path_to_node($attrs->{from}, $attrs->{alias});
 
-  die "Huh... I expect a blessed result_source..."
-    if ref($main_rsrc) eq 'ARRAY';
+  return unless defined $nodes;
 
   my @ord_cols = map
     { $_->[0] }
-    ( $self->_extract_order_criteria($order_by) )
+    ( $self->_extract_order_criteria($attrs->{order_by}) )
   ;
   return unless @ord_cols;
 
-  my $colinfos = $self->_resolve_column_info($main_rsrc);
+  my $valid_aliases = { map { $_ => 1 } (
+    $attrs->{from}[0]{-alias},
+    map { values %$_ } @$nodes,
+  ) };
+
+  my $colinfos = $self->_resolve_column_info($attrs->{from});
+
+  my ($colinfos_to_return, $seen_main_src_cols);
+
+  for my $col (@ord_cols) {
+    # if order criteria is unresolvable - there is nothing we can do
+    my $colinfo = $colinfos->{$col} or last;
+
+    # if we reached the end of the allowed aliases - also nothing we can do
+    last unless $valid_aliases->{$colinfo->{-source_alias}};
+
+    $colinfos_to_return->{$col} = $colinfo;
 
-  for (0 .. $#ord_cols) {
+    $seen_main_src_cols->{$colinfo->{-colname}} = 1
+      if $colinfo->{-source_alias} eq $attrs->{alias};
+  }
+
+  # FIXME the condition may be singling out things on its own, so we
+  # conceivable could come back wi "stable-ordered by nothing"
+  # not confient enough in the parser yet, so punt for the time being
+  return unless $seen_main_src_cols;
+
+  my $main_src_fixed_cols_from_cond = [ $attrs->{where}
+    ? (
+      map
+      {
+        ( $colinfos->{$_} and $colinfos->{$_}{-source_alias} eq $attrs->{alias} )
+          ? $colinfos->{$_}{-colname}
+          : ()
+      }
+      keys %{ $self->_extract_fixed_condition_columns($attrs->{where}) }
+    )
+    : ()
+  ];
+
+  return $attrs->{result_source}->_identifying_column_set([
+    keys %$seen_main_src_cols,
+    @$main_src_fixed_cols_from_cond,
+  ]) ? $colinfos_to_return : ();
+}
+
+# Attempts to flatten a passed in SQLA condition as much as possible towards
+# a plain hashref, *without* altering its semantics. Required by
+# create/populate being able to extract definitive conditions from preexisting
+# resultset {where} stacks
+#
+# FIXME - while relatively robust, this is still imperfect, one of the first
+# things to tackle when we get access to a formalized AST. Note that this code
+# is covered by a *ridiculous* amount of tests, so starting with porting this
+# code would be a rather good exercise
+sub _collapse_cond {
+  my ($self, $where, $where_is_anded_array) = @_;
+
+  my $fin;
+
+  if (! $where) {
+    return;
+  }
+  elsif ($where_is_anded_array or ref $where eq 'HASH') {
+
+    my @pairs;
+
+    my @pieces = $where_is_anded_array ? @$where : $where;
+    while (@pieces) {
+      my $chunk = shift @pieces;
+
+      if (ref $chunk eq 'HASH') {
+        for (sort keys %$chunk) {
+
+          # Match SQLA 1.79 behavior
+          unless( length $_ ) {
+            is_literal_value($chunk->{$_})
+              ? carp 'Hash-pairs consisting of an empty string with a literal are deprecated, use -and => [ $literal ] instead'
+              : $self->throw_exception("Supplying an empty left hand side argument is not supported in hash-pairs")
+            ;
+          }
+
+          push @pairs, $_ => $chunk->{$_};
+        }
+      }
+      elsif (ref $chunk eq 'ARRAY') {
+        push @pairs, -or => $chunk
+          if @$chunk;
+      }
+      elsif ( ! length ref $chunk) {
+
+        # Match SQLA 1.79 behavior
+        $self->throw_exception("Supplying an empty left hand side argument is not supported in array-pairs")
+          if $where_is_anded_array and (! defined $chunk or ! length $chunk);
+
+        push @pairs, $chunk, shift @pieces;
+      }
+      else {
+        push @pairs, '', $chunk;
+      }
+    }
+
+    return unless @pairs;
+
+    my @conds = $self->_collapse_cond_unroll_pairs(\@pairs)
+      or return;
+
+    # Consolidate various @conds back into something more compact
+    for my $c (@conds) {
+      if (ref $c ne 'HASH') {
+        push @{$fin->{-and}}, $c;
+      }
+      else {
+        for my $col (sort keys %$c) {
+
+          # consolidate all -and nodes
+          if ($col =~ /^\-and$/i) {
+            push @{$fin->{-and}},
+              ref $c->{$col} eq 'ARRAY' ? @{$c->{$col}}
+            : ref $c->{$col} eq 'HASH' ? %{$c->{$col}}
+            : { $col => $c->{$col} }
+            ;
+          }
+          elsif ($col =~ /^\-/) {
+            push @{$fin->{-and}}, { $col => $c->{$col} };
+          }
+          elsif (exists $fin->{$col}) {
+            $fin->{$col} = [ -and => map {
+              (ref $_ eq 'ARRAY' and ($_->[0]||'') =~ /^\-and$/i )
+                ? @{$_}[1..$#$_]
+                : $_
+              ;
+            } ($fin->{$col}, $c->{$col}) ];
+          }
+          else {
+            $fin->{$col} = $c->{$col};
+          }
+        }
+      }
+    }
+  }
+  elsif (ref $where eq 'ARRAY') {
+    # we are always at top-level here, it is safe to dump empty *standalone* pieces
+    my $fin_idx;
+
+    for (my $i = 0; $i <= $#$where; $i++ ) {
+
+      # Match SQLA 1.79 behavior
+      $self->throw_exception(
+        "Supplying an empty left hand side argument is not supported in array-pairs"
+      ) if (! defined $where->[$i] or ! length $where->[$i]);
+
+      my $logic_mod = lc ( ($where->[$i] =~ /^(\-(?:and|or))$/i)[0] || '' );
+
+      if ($logic_mod) {
+        $i++;
+        $self->throw_exception("Unsupported top-level op/arg pair: [ $logic_mod => $where->[$i] ]")
+          unless ref $where->[$i] eq 'HASH' or ref $where->[$i] eq 'ARRAY';
+
+        my $sub_elt = $self->_collapse_cond({ $logic_mod => $where->[$i] })
+          or next;
+
+        my @keys = keys %$sub_elt;
+        if ( @keys == 1 and $keys[0] !~ /^\-/ ) {
+          $fin_idx->{ "COL_$keys[0]_" . serialize $sub_elt } = $sub_elt;
+        }
+        else {
+          $fin_idx->{ "SER_" . serialize $sub_elt } = $sub_elt;
+        }
+      }
+      elsif (! length ref $where->[$i] ) {
+        my $sub_elt = $self->_collapse_cond({ @{$where}[$i, $i+1] })
+          or next;
+
+        $fin_idx->{ "COL_$where->[$i]_" . serialize $sub_elt } = $sub_elt;
+        $i++;
+      }
+      else {
+        $fin_idx->{ "SER_" . serialize $where->[$i] } = $self->_collapse_cond( $where->[$i] ) || next;
+      }
+    }
+
+    if (! $fin_idx) {
+      return;
+    }
+    elsif ( keys %$fin_idx == 1 ) {
+      $fin = (values %$fin_idx)[0];
+    }
+    else {
+      my @or;
+
+      # at this point everything is at most one level deep - unroll if needed
+      for (sort keys %$fin_idx) {
+        if ( ref $fin_idx->{$_} eq 'HASH' and keys %{$fin_idx->{$_}} == 1 ) {
+          my ($l, $r) = %{$fin_idx->{$_}};
+
+          if (
+            ref $r eq 'ARRAY'
+              and
+            (
+              ( @$r == 1 and $l =~ /^\-and$/i )
+                or
+              $l =~ /^\-or$/i
+            )
+          ) {
+            push @or, @$r
+          }
+
+          elsif (
+            ref $r eq 'HASH'
+              and
+            keys %$r == 1
+              and
+            $l =~ /^\-(?:and|or)$/i
+          ) {
+            push @or, %$r;
+          }
+
+          else {
+            push @or, $l, $r;
+          }
+        }
+        else {
+          push @or, $fin_idx->{$_};
+        }
+      }
+
+      $fin->{-or} = \@or;
+    }
+  }
+  else {
+    # not a hash not an array
+    $fin = { -and => [ $where ] };
+  }
+
+  # unroll single-element -and's
+  while (
+    $fin->{-and}
+      and
+    @{$fin->{-and}} < 2
+  ) {
+    my $and = delete $fin->{-and};
+    last if @$and == 0;
+
+    # at this point we have @$and == 1
     if (
-      ! $colinfos->{$ord_cols[$_]}
-        or
-      $colinfos->{$ord_cols[$_]}{-result_source} != $main_rsrc
+      ref $and->[0] eq 'HASH'
+        and
+      ! grep { exists $fin->{$_} } keys %{$and->[0]}
     ) {
-      $#ord_cols =  $_ - 1;
+      $fin = {
+        %$fin, %{$and->[0]}
+      };
+    }
+    else {
+      $fin->{-and} = $and;
       last;
     }
   }
 
-  # we just truncated it above
-  return unless @ord_cols;
+  # compress same-column conds found in $fin
+  for my $col ( grep { $_ !~ /^\-/ } keys %$fin ) {
+    next unless ref $fin->{$col} eq 'ARRAY' and ($fin->{$col}[0]||'') =~ /^\-and$/i;
+    my $val_bag = { map {
+      (! defined $_ )                          ? ( UNDEF => undef )
+    : ( ! length ref $_ or is_plain_value $_ ) ? ( "VAL_$_" => $_ )
+    : ( ( 'SER_' . serialize $_ ) => $_ )
+    } @{$fin->{$col}}[1 .. $#{$fin->{$col}}] };
+
+    if (keys %$val_bag == 1 ) {
+      ($fin->{$col}) = values %$val_bag;
+    }
+    else {
+      $fin->{$col} = [ -and => map { $val_bag->{$_} } sort keys %$val_bag ];
+    }
+  }
 
-  my $order_portion_ci = { map {
-    $colinfos->{$_}{-colname} => $colinfos->{$_},
-    $colinfos->{$_}{-fq_colname} => $colinfos->{$_},
-  } @ord_cols };
+  return keys %$fin ? $fin : ();
+}
 
-  # since all we check here are the start of the order_by belonging to the
-  # top level $rsrc, a present identifying set will mean that the resultset
-  # is ordered by its leftmost table in a stable manner
-  #
-  # RV of _identifying_column_set contains unqualified names only
-  my $unqualified_idset = $main_rsrc->_identifying_column_set({
-    ( $where ? %{
-      $self->_resolve_column_info(
-        $main_rsrc, $self->_extract_fixed_condition_columns($where)
-      )
-    } : () ),
-    %$order_portion_ci
-  }) or return;
+sub _collapse_cond_unroll_pairs {
+  my ($self, $pairs) = @_;
 
-  my $ret_info;
-  my %unqualified_idcols_from_order = map {
-    $order_portion_ci->{$_} ? ( $_ => $order_portion_ci->{$_} ) : ()
-  } @$unqualified_idset;
+  my @conds;
 
-  # extra optimization - cut the order_by at the end of the identifying set
-  # (just in case the user was stupid and overlooked the obvious)
-  for my $i (0 .. $#ord_cols) {
-    my $col = $ord_cols[$i];
-    my $unqualified_colname = $order_portion_ci->{$col}{-colname};
-    $ret_info->{$col} = { %{$order_portion_ci->{$col}}, -idx_in_order_subset => $i };
-    delete $unqualified_idcols_from_order{$ret_info->{$col}{-colname}};
+  while (@$pairs) {
+    my ($lhs, $rhs) = splice @$pairs, 0, 2;
 
-    # we didn't reach the end of the identifying portion yet
-    return $ret_info unless keys %unqualified_idcols_from_order;
+    if (! length $lhs) {
+      push @conds, $self->_collapse_cond($rhs);
+    }
+    elsif ( $lhs =~ /^\-and$/i ) {
+      push @conds, $self->_collapse_cond($rhs, (ref $rhs eq 'ARRAY'));
+    }
+    elsif ( $lhs =~ /^\-or$/i ) {
+      push @conds, $self->_collapse_cond(
+        (ref $rhs eq 'HASH') ? [ map { $_ => $rhs->{$_} } sort keys %$rhs ] : $rhs
+      );
+    }
+    else {
+      if (ref $rhs eq 'HASH' and ! keys %$rhs) {
+        # FIXME - SQLA seems to be doing... nothing...?
+      }
+      # normalize top level -ident, for saner extract_fixed_condition_columns code
+      elsif (ref $rhs eq 'HASH' and keys %$rhs == 1 and exists $rhs->{-ident}) {
+        push @conds, { $lhs => { '=', $rhs } };
+      }
+      elsif (ref $rhs eq 'HASH' and keys %$rhs == 1 and exists $rhs->{-value} and is_plain_value $rhs->{-value}) {
+        push @conds, { $lhs => $rhs->{-value} };
+      }
+      elsif (ref $rhs eq 'HASH' and keys %$rhs == 1 and exists $rhs->{'='}) {
+        if ( length ref $rhs->{'='} and is_literal_value $rhs->{'='} ) {
+          push @conds, { $lhs => $rhs };
+        }
+        else {
+          for my $p ($self->_collapse_cond_unroll_pairs([ $lhs => $rhs->{'='} ])) {
+
+            # extra sanity check
+            if (keys %$p > 1) {
+              require Data::Dumper::Concise;
+              local $Data::Dumper::Deepcopy = 1;
+              $self->throw_exception(
+                "Internal error: unexpected collapse unroll:"
+              . Data::Dumper::Concise::Dumper { in => { $lhs => $rhs }, out => $p }
+              );
+            }
+
+            my ($l, $r) = %$p;
+
+            push @conds, (
+              ! length ref $r
+                or
+              # the unroller recursion may return a '=' prepended value already
+              ref $r eq 'HASH' and keys %$rhs == 1 and exists $rhs->{'='}
+                or
+              is_plain_value($r)
+            )
+              ? { $l => $r }
+              : { $l => { '=' => $r } }
+            ;
+          }
+        }
+      }
+      elsif (ref $rhs eq 'ARRAY') {
+        # some of these conditionals encounter multi-values - roll them out using
+        # an unshift, which will cause extra looping in the while{} above
+        if (! @$rhs ) {
+          push @conds, { $lhs => [] };
+        }
+        elsif ( ($rhs->[0]||'') =~ /^\-(?:and|or)$/i ) {
+          $self->throw_exception("Value modifier not followed by any values: $lhs => [ $rhs->[0] ] ")
+            if  @$rhs == 1;
+
+          if( $rhs->[0] =~ /^\-and$/i ) {
+            unshift @$pairs, map { $lhs => $_ } @{$rhs}[1..$#$rhs];
+          }
+          # if not an AND then it's an OR
+          elsif(@$rhs == 2) {
+            unshift @$pairs, $lhs => $rhs->[1];
+          }
+          else {
+            push @conds, { $lhs => [ @{$rhs}[1..$#$rhs] ] };
+          }
+        }
+        elsif (@$rhs == 1) {
+          unshift @$pairs, $lhs => $rhs->[0];
+        }
+        else {
+          push @conds, { $lhs => $rhs };
+        }
+      }
+      # unroll func + { -value => ... }
+      elsif (
+        ref $rhs eq 'HASH'
+          and
+        ( my ($subop) = keys %$rhs ) == 1
+          and
+        length ref ((values %$rhs)[0])
+          and
+        my $vref = is_plain_value( (values %$rhs)[0] )
+      ) {
+        push @conds, { $lhs => { $subop => $$vref } }
+      }
+      else {
+        push @conds, { $lhs => $rhs };
+      }
+    }
   }
 
-  die 'How did we get here...';
+  return @conds;
 }
 
-# returns an arrayref of column names which *definitely* have some
-# sort of non-nullable equality requested in the given condition
-# specification. This is used to figure out if a resultset is
-# constrained to a column which is part of a unique constraint,
-# which in turn allows us to better predict how ordering will behave
-# etc.
+# Analyzes a given condition and attempts to extract all columns
+# with a definitive fixed-condition criteria. Returns a hashref
+# of k/v pairs suitable to be passed to set_columns(), with a
+# MAJOR CAVEAT - multi-value (contradictory) equalities are still
+# represented as a reference to the UNRESOVABLE_CONDITION constant
+# The reason we do this is that some codepaths only care about the
+# codition being stable, as opposed to actually making sense
+#
+# The normal mode is used to figure out if a resultset is constrained
+# to a column which is part of a unique constraint, which in turn
+# allows us to better predict how ordering will behave etc.
+#
+# With the optional "consider_nulls" boolean argument, the function
+# is instead used to infer inambiguous values from conditions
+# (e.g. the inheritance of resultset conditions on new_result)
 #
-# this is a rudimentary, incomplete, and error-prone extractor
-# however this is OK - it is conservative, and if we can not find
-# something that is in fact there - the stack will recover gracefully
-# Also - DQ and the mst it rode in on will save us all RSN!!!
 sub _extract_fixed_condition_columns {
-  my ($self, $where) = @_;
+  my ($self, $where, $consider_nulls) = @_;
+  my $where_hash = $self->_collapse_cond($_[1]);
 
-  return unless ref $where eq 'HASH';
+  my $res = {};
+  my ($c, $v);
+  for $c (keys %$where_hash) {
+    my $vals;
 
-  my @cols;
-  for my $lhs (keys %$where) {
-    if ($lhs =~ /^\-and$/i) {
-      push @cols, ref $where->{$lhs} eq 'ARRAY'
-        ? ( map { @{ $self->_extract_fixed_condition_columns($_) } } @{$where->{$lhs}} )
-        : @{ $self->_extract_fixed_condition_columns($where->{$lhs}) }
-      ;
+    if (!defined ($v = $where_hash->{$c}) ) {
+      $vals->{UNDEF} = $v if $consider_nulls
+    }
+    elsif (
+      ref $v eq 'HASH'
+        and
+      keys %$v == 1
+    ) {
+      if (exists $v->{-value}) {
+        if (defined $v->{-value}) {
+          $vals->{"VAL_$v->{-value}"} = $v->{-value}
+        }
+        elsif( $consider_nulls ) {
+          $vals->{UNDEF} = $v->{-value};
+        }
+      }
+      # do not need to check for plain values - _collapse_cond did it for us
+      elsif(
+        length ref $v->{'='}
+          and
+        (
+          ( ref $v->{'='} eq 'HASH' and keys %{$v->{'='}} == 1 and exists $v->{'='}{-ident} )
+            or
+          is_literal_value($v->{'='})
+        )
+       ) {
+        $vals->{ 'SER_' . serialize $v->{'='} } = $v->{'='};
+      }
+    }
+    elsif (
+      ! length ref $v
+        or
+      is_plain_value ($v)
+    ) {
+      $vals->{"VAL_$v"} = $v;
+    }
+    elsif (ref $v eq 'ARRAY' and ($v->[0]||'') eq '-and') {
+      for ( @{$v}[1..$#$v] ) {
+        my $subval = $self->_extract_fixed_condition_columns({ $c => $_ }, 'consider nulls');  # always fish nulls out on recursion
+        next unless exists $subval->{$c};  # didn't find anything
+        $vals->{
+          ! defined $subval->{$c}                                        ? 'UNDEF'
+        : ( ! length ref $subval->{$c} or is_plain_value $subval->{$c} ) ? "VAL_$subval->{$c}"
+        : ( 'SER_' . serialize $subval->{$c} )
+        } = $subval->{$c};
+      }
     }
-    elsif ($lhs !~ /^\-/) {
-      my $val = $where->{$lhs};
 
-      push @cols, $lhs if (defined $val and (
-        ! ref $val
-          or
-        (ref $val eq 'HASH' and keys %$val == 1 and defined $val->{'='})
-      ));
+    if (keys %$vals == 1) {
+      ($res->{$c}) = (values %$vals)
+        unless !$consider_nulls and exists $vals->{UNDEF};
+    }
+    elsif (keys %$vals > 1) {
+      $res->{$c} = UNRESOLVABLE_CONDITION;
     }
   }
-  return \@cols;
+
+  $res;
 }
 
 1;
index 7e491cd..f521658 100644 (file)
@@ -1,13 +1,14 @@
 package DBIx::Class::Storage::Statistics;
+
 use strict;
 use warnings;
 
-use base qw/DBIx::Class/;
-use IO::File;
+use DBIx::Class::_Util qw(sigwarn_silencer qsub);
+use IO::Handle ();
+use Moo;
+extends 'DBIx::Class';
 use namespace::clean;
 
-__PACKAGE__->mk_group_accessors(simple => qw/callback _debugfh silence/);
-
 =head1 NAME
 
 DBIx::Class::Storage::Statistics - SQL Statistics
@@ -26,55 +27,56 @@ for collecting the statistics as discussed in L<DBIx::Class::Manual::Cookbook>.
 
 =head1 METHODS
 
-=cut
-
 =head2 new
 
 Returns a new L<DBIx::Class::Storage::Statistics> object.
 
-=cut
-sub new {
-  my $self = {};
-  bless $self, (ref($_[0]) || $_[0]);
-
-  return $self;
-}
-
 =head2 debugfh
 
 Sets or retrieves the filehandle used for trace/debug output.  This should
-be an IO::Handle compatible object (only the C<print> method is used). Initially
-should be set to STDERR - although see information on the
-L<DBIC_TRACE> environment variable.
+be an L<IO::Handle> compatible object (only the
+L<< print|IO::Handle/METHODS >> method is used). By
+default it is initially set to STDERR - although see discussion of the
+L<DBIC_TRACE|DBIx::Class::Storage/DBIC_TRACE> environment variable.
 
-As getter it will lazily open a filehandle for you if one is not already set.
+Invoked as a getter it will lazily open a filehandle and set it to
+L<< autoflush|perlvar/HANDLE->autoflush( EXPR ) >> (if one is not
+already set).
 
 =cut
 
-sub debugfh {
-  my $self = shift;
+has debugfh => (
+  is => 'rw',
+  lazy => 1,
+  trigger => qsub '$_[0]->_defaulted_to_stderr(undef); $_[0]->_clear_debugfh unless $_[1];',
+  clearer => '_clear_debugfh',
+  builder => '_build_debugfh',
+);
+
+sub _build_debugfh {
+  my $fh;
+
+  my $debug_env = $ENV{DBIX_CLASS_STORAGE_DBI_DEBUG} || $ENV{DBIC_TRACE};
 
-  if (@_) {
-    $self->_debugfh($_[0]);
-  } elsif (!defined($self->_debugfh())) {
-    my $fh;
-    my $debug_env = $ENV{DBIX_CLASS_STORAGE_DBI_DEBUG}
-                  || $ENV{DBIC_TRACE};
-    if (defined($debug_env) && ($debug_env =~ /=(.+)$/)) {
-      $fh = IO::File->new($1, 'a')
-        or die("Cannot open trace file $1");
-    } else {
-      $fh = IO::File->new('>&STDERR')
-        or die('Duplication of STDERR for debug output failed (perhaps your STDERR is closed?)');
-    }
-
-    $fh->autoflush();
-    $self->_debugfh($fh);
+  if (defined($debug_env) and ($debug_env =~ /=(.+)$/)) {
+    open ($fh, '>>', $1)
+      or die("Cannot open trace file $1: $!\n");
   }
+  else {
+    open ($fh, '>&STDERR')
+      or die("Duplication of STDERR for debug output failed (perhaps your STDERR is closed?): $!\n");
+    $_[0]->_defaulted_to_stderr(1);
+  }
+
+  $fh->autoflush(1);
 
-  $self->_debugfh;
+  $fh;
 }
 
+has [qw(_defaulted_to_stderr silence callback)] => (
+  is => 'rw',
+);
+
 =head2 print
 
 Prints the specified string to our debugging filehandle.  Provided to save our
@@ -86,7 +88,13 @@ sub print {
 
   return if $self->silence;
 
-  $self->debugfh->print($msg);
+  my $fh = $self->debugfh;
+
+  # not using 'no warnings' here because all of this can change at runtime
+  local $SIG{__WARN__} = sigwarn_silencer(qr/^Wide character in print/)
+    if $self->_defaulted_to_stderr;
+
+  $fh->print($msg);
 }
 
 =head2 silence
@@ -196,18 +204,22 @@ sub query_start {
 Called when a query finishes executing.  Has the same arguments as query_start.
 
 =cut
+
 sub query_end {
   my ($self, $string) = @_;
 }
 
-1;
-
-=head1 AUTHOR AND CONTRIBUTORS
+=head1 FURTHER QUESTIONS?
 
-See L<AUTHOR|DBIx::Class/AUTHOR> and L<CONTRIBUTORS|DBIx::Class/CONTRIBUTORS> in DBIx::Class
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
+
+1;
index 18c99fa..31a2d5b 100644 (file)
@@ -2,10 +2,9 @@ package DBIx::Class::Storage::TxnScopeGuard;
 
 use strict;
 use warnings;
-use Try::Tiny;
-use Scalar::Util qw/weaken blessed refaddr/;
+use Scalar::Util qw(weaken blessed refaddr);
 use DBIx::Class;
-use DBIx::Class::_Util 'is_exception';
+use DBIx::Class::_Util qw(is_exception detected_reinvoked_destructor);
 use DBIx::Class::Carp;
 use namespace::clean;
 
@@ -24,9 +23,12 @@ sub new {
   # FIXME FRAGILE - any eval that fails but *does not* rethrow between here
   # and the unwind will trample over $@ and invalidate the entire mechanism
   # There got to be a saner way of doing this...
-  if (is_exception $@) {
+  #
+  # Deliberately *NOT* using is_exception - if someone left a misbehaving
+  # antipattern value in $@, it's not our business to whine about it
+  if( defined $@ and length $@ ) {
     weaken(
-      $guard->{existing_exception_ref} = (ref($@) eq '') ? \$@ : $@
+      $guard->{existing_exception_ref} = (length ref $@) ? $@ : \$@
     );
   }
 
@@ -45,68 +47,69 @@ sub commit {
   $self->{storage}->throw_exception("Refusing to execute multiple commits on scope guard $self")
     if $self->{inactivated};
 
-  $self->{storage}->txn_commit;
+  # FIXME - this assumption may be premature: a commit may fail and a rollback
+  # *still* be necessary. Currently I am not aware of such scenarious, but I
+  # also know the deferred constraint handling is *severely* undertested.
+  # Making the change of "fire txn and never come back to this" in order to
+  # address RT#107159, but this *MUST* be reevaluated later.
   $self->{inactivated} = 1;
+  $self->{storage}->txn_commit;
 }
 
 sub DESTROY {
-  my $self = shift;
+  return if &detected_reinvoked_destructor;
 
-  return if $self->{inactivated};
+  return if $_[0]->{inactivated};
 
-  # if our dbh is not ours anymore, the $dbh weakref will go undef
-  $self->{storage}->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
-  return unless $self->{dbh};
 
-  my $exception = $@ if (
+  # grab it before we've done volatile stuff below
+  my $current_exception = (
     is_exception $@
       and
     (
-      ! defined $self->{existing_exception_ref}
+      ! defined $_[0]->{existing_exception_ref}
         or
-      refaddr( ref($@) eq '' ? \$@ : $@ ) != refaddr($self->{existing_exception_ref})
+      refaddr( (length ref $@) ? $@ : \$@ ) != refaddr($_[0]->{existing_exception_ref})
     )
-  );
-
-  {
-    local $@;
-
-    carp 'A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back.'
-      unless defined $exception;
-
-    my $rollback_exception;
-    # do minimal connectivity check due to weird shit like
-    # https://rt.cpan.org/Public/Bug/Display.html?id=62370
-    try { $self->{storage}->_seems_connected && $self->{storage}->txn_rollback }
-    catch { $rollback_exception = shift };
-
-    if ( $rollback_exception and (
-      ! defined blessed $rollback_exception
-          or
-      ! $rollback_exception->isa('DBIx::Class::Storage::NESTED_ROLLBACK_EXCEPTION')
-    ) ) {
-      # append our text - THIS IS A TEMPORARY FIXUP!
-      # a real stackable exception object is in the works
-      if (ref $exception eq 'DBIx::Class::Exception') {
-        $exception->{msg} = "Transaction aborted: $exception->{msg} "
-          ."Rollback failed: ${rollback_exception}";
-      }
-      elsif ($exception) {
-        $exception = "Transaction aborted: ${exception} "
-          ."Rollback failed: ${rollback_exception}";
-      }
-      else {
-        carp (join ' ',
-          "********************* ROLLBACK FAILED!!! ********************",
-          "\nA rollback operation failed after the guard went out of scope.",
-          'This is potentially a disastrous situation, check your data for',
-          "consistency: $rollback_exception"
-        );
-      }
-    }
+  )
+    ? $@
+    : undef
+  ;
+
+
+  # if our dbh is not ours anymore, the $dbh weakref will go undef
+  $_[0]->{storage}->_verify_pid unless DBIx::Class::_ENV_::BROKEN_FORK;
+  return unless defined $_[0]->{dbh};
+
+
+  carp 'A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back'
+    unless defined $current_exception;
+
+
+  if (
+    my $rollback_exception = $_[0]->{storage}->__delicate_rollback(
+      defined $current_exception
+        ? \$current_exception
+        : ()
+    )
+      and
+    ! defined $current_exception
+  ) {
+    carp (join ' ',
+      "********************* ROLLBACK FAILED!!! ********************",
+      "\nA rollback operation failed after the guard went out of scope.",
+      'This is potentially a disastrous situation, check your data for',
+      "consistency: $rollback_exception"
+    );
   }
 
-  $@ = $exception;
+  $@ = $current_exception
+    if DBIx::Class::_ENV_::UNSTABLE_DOLLARAT;
+
+  # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+  # collected before leaving this scope. Depending on the code above, this
+  # may very well be just a preventive measure guarding future modifications
+  undef;
 }
 
 1;
@@ -154,13 +157,15 @@ the transaction is rolled back, via L<DBIx::Class::Storage/txn_rollback>
 
 L<DBIx::Class::Schema/txn_scope_guard>.
 
-=head1 AUTHOR
+L<Scope::Guard> by chocolateboy (inspiration for this module)
 
-Ash Berlin, 2008.
+=head1 FURTHER QUESTIONS?
 
-Inspired by L<Scope::Guard> by chocolateboy.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-This module is free software. It may be used, redistributed and/or modified
-under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 52df664..793c1bc 100644 (file)
@@ -162,13 +162,16 @@ sub _is_utf8_column {
   return ($_[0]->utf8_columns || {})->{$_[1]};
 }
 
-=head1 AUTHORS
+=head1 FURTHER QUESTIONS?
 
-See L<DBIx::Class/CONTRIBUTORS>.
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-=head1 LICENSE
+=head1 COPYRIGHT AND LICENSE
 
-You may distribute this code under the same terms as Perl itself.
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
 
 =cut
 
index 3e3b68f..4afa4c2 100644 (file)
@@ -4,7 +4,7 @@ package # hide from PAUSE
 use warnings;
 use strict;
 
-use constant SPURIOUS_VERSION_CHECK_WARNINGS => ($] < 5.010 ? 1 : 0);
+use constant SPURIOUS_VERSION_CHECK_WARNINGS => ( "$]" < 5.010 ? 1 : 0);
 
 BEGIN {
   package # hide from pause
@@ -17,29 +17,40 @@ BEGIN {
     # but of course
     BROKEN_FORK => ($^O eq 'MSWin32') ? 1 : 0,
 
+    BROKEN_GOTO => ( "$]" < 5.008003 ) ? 1 : 0,
+
     HAS_ITHREADS => $Config{useithreads} ? 1 : 0,
 
-    # ::Runmode would only be loaded by DBICTest, which in turn implies t/
-    DBICTEST => eval { DBICTest::RunMode->is_author } ? 1 : 0,
+    UNSTABLE_DOLLARAT => ( "$]" < 5.013002 ) ? 1 : 0,
 
-    # During 5.13 dev cycle HELEMs started to leak on copy
-    PEEPEENESS =>
-      # request for all tests would force "non-leaky" illusion and vice-versa
-      defined $ENV{DBICTEST_ALL_LEAKS}                                              ? !$ENV{DBICTEST_ALL_LEAKS}
-      # otherwise confess that this perl is busted ONLY on smokers
-    : eval { DBICTest::RunMode->is_smoker } && ($] >= 5.013005 and $] <= 5.013006)  ? 1
-      # otherwise we are good
-                                                                                    : 0
-    ,
+    DBICTEST => $INC{"DBICTest/Util.pm"} ? 1 : 0,
 
-    ASSERT_NO_INTERNAL_WANTARRAY => $ENV{DBIC_ASSERT_NO_INTERNAL_WANTARRAY} ? 1 : 0,
+    # During 5.13 dev cycle HELEMs started to leak on copy
+    # add an escape for these perls ON SMOKERS - a user will still get death
+    PEEPEENESS => ( eval { DBICTest::RunMode->is_smoker } && ( "$]" >= 5.013005 and "$]" <= 5.013006) ),
+
+    ( map
+      #
+      # the "DBIC_" prefix below is crucial - this is what makes CI pick up
+      # all envvars without further adjusting its scripts
+      # DO NOT CHANGE to the more logical { $_ => !!( $ENV{"DBIC_$_"} ) }
+      #
+      { substr($_, 5) => !!( $ENV{$_} ) }
+      qw(
+        DBIC_SHUFFLE_UNORDERED_RESULTSETS
+        DBIC_ASSERT_NO_INTERNAL_WANTARRAY
+        DBIC_ASSERT_NO_INTERNAL_INDIRECT_CALLS
+        DBIC_STRESSTEST_UTF8_UPGRADE_GENERATED_COLLAPSER_SOURCE
+        DBIC_STRESSTEST_COLUMN_INFO_UNAWARE_STORAGE
+      )
+    ),
 
     IV_SIZE => $Config{ivsize},
 
     OS_NAME => $^O,
   };
 
-  if ($] < 5.009_005) {
+  if ( "$]" < 5.009_005) {
     require MRO::Compat;
     constant->import( OLD_MRO => 1 );
   }
@@ -53,11 +64,28 @@ BEGIN {
 # Carp::Skip to the rescue soon
 use DBIx::Class::Carp '^DBIx::Class|^DBICTest';
 
+use B ();
 use Carp 'croak';
-use Scalar::Util qw(weaken blessed reftype);
+use Storable 'nfreeze';
+use Scalar::Util qw(weaken blessed reftype refaddr);
+use List::Util qw(first);
+use Sub::Quote qw(qsub quote_sub);
+
+# Already correctly prototyped: perlbrew exec perl -MStorable -e 'warn prototype \&Storable::dclone'
+BEGIN { *deep_clone = \&Storable::dclone }
 
 use base 'Exporter';
-our @EXPORT_OK = qw(sigwarn_silencer modver_gt_or_eq fail_on_internal_wantarray refcount hrefaddr is_exception);
+our @EXPORT_OK = qw(
+  sigwarn_silencer modver_gt_or_eq modver_gt_or_eq_and_lt
+  fail_on_internal_wantarray fail_on_internal_call
+  refdesc refcount hrefaddr
+  scope_guard detected_reinvoked_destructor
+  is_exception dbic_internal_try
+  quote_sub qsub perlstring serialize deep_clone
+  UNRESOLVABLE_CONDITION
+);
+
+use constant UNRESOLVABLE_CONDITION => \ '1 = 0';
 
 sub sigwarn_silencer ($) {
   my $pattern = shift;
@@ -69,20 +97,67 @@ sub sigwarn_silencer ($) {
   return sub { &$orig_sig_warn unless $_[0] =~ $pattern };
 }
 
-sub hrefaddr ($) { sprintf '0x%x', &Scalar::Util::refaddr }
+sub perlstring ($) { q{"}. quotemeta( shift ). q{"} };
+
+sub hrefaddr ($) { sprintf '0x%x', &refaddr||0 }
+
+sub refdesc ($) {
+  croak "Expecting a reference" if ! length ref $_[0];
+
+  # be careful not to trigger stringification,
+  # reuse @_ as a scratch-pad
+  sprintf '%s%s(0x%x)',
+    ( defined( $_[1] = blessed $_[0]) ? "$_[1]=" : '' ),
+    reftype $_[0],
+    refaddr($_[0]),
+  ;
+}
 
 sub refcount ($) {
   croak "Expecting a reference" if ! length ref $_[0];
 
-  require B;
   # No tempvars - must operate on $_[0], otherwise the pad
   # will count as an extra ref
   B::svref_2object($_[0])->REFCNT;
 }
 
+sub serialize ($) {
+  local $Storable::canonical = 1;
+  nfreeze($_[0]);
+}
+
+sub scope_guard (&) {
+  croak 'Calling scope_guard() in void context makes no sense'
+    if ! defined wantarray;
+
+  # no direct blessing of coderefs - DESTROY is buggy on those
+  bless [ $_[0] ], 'DBIx::Class::_Util::ScopeGuard';
+}
+{
+  package #
+    DBIx::Class::_Util::ScopeGuard;
+
+  sub DESTROY {
+    &DBIx::Class::_Util::detected_reinvoked_destructor;
+
+    local $@ if DBIx::Class::_ENV_::UNSTABLE_DOLLARAT;
+
+    eval {
+      $_[0]->[0]->();
+      1;
+    }
+      or
+    Carp::cluck(
+      "Execution of scope guard $_[0] resulted in the non-trappable exception:\n\n$@"
+    );
+  }
+}
+
+
 sub is_exception ($) {
   my $e = $_[0];
 
+  # FIXME
   # this is not strictly correct - an eval setting $@ to undef
   # is *not* the same as an eval setting $@ to ''
   # but for the sake of simplicity assume the following for
@@ -93,7 +168,10 @@ sub is_exception ($) {
   {
     local $@;
     eval {
-      $not_blank = ($e ne '') ? 1 : 0;
+      # The ne() here is deliberate - a plain length($e), or worse "$e" ne
+      # will entirely obviate the need for the encolsing eval{}, as the
+      # condition we guard against is a missing fallback overload
+      $not_blank = ( $e ne '' );
       1;
     } or $suberror = $@;
   }
@@ -101,8 +179,8 @@ sub is_exception ($) {
   if (defined $suberror) {
     if (length (my $class = blessed($e) )) {
       carp_unique( sprintf(
-        'External exception object %s=%s(%s) implements partial (broken) '
-      . 'overloading preventing it from being used in simple ($x eq $y) '
+        'External exception class %s implements partial (broken) overloading '
+      . 'preventing its instances from being used in simple ($x eq $y) '
       . 'comparisons. Given Perl\'s "globally cooperative" exception '
       . 'handling this type of brokenness is extremely dangerous on '
       . 'exception objects, as it may (and often does) result in silent '
@@ -114,15 +192,13 @@ sub is_exception ($) {
       . 'is saner application-wide. What follows is the actual error text '
       . "as generated by Perl itself:\n\n%s\n ",
         $class,
-        reftype $e,
-        hrefaddr $e,
         $class,
         'http://v.gd/DBIC_overload_tempfix/',
         $suberror,
       ));
 
       # workaround, keeps spice flowing
-      $not_blank = ("$e" ne '') ? 1 : 0;
+      $not_blank = !!( length $e );
     }
     else {
       # not blessed yet failed the 'ne'... this makes 0 sense...
@@ -130,30 +206,213 @@ sub is_exception ($) {
       die $suberror
     }
   }
+  elsif (
+    # a ref evaluating to '' is definitively a "null object"
+    ( not $not_blank )
+      and
+    length( my $class = ref $e )
+  ) {
+    carp_unique( sprintf(
+      "Objects of external exception class '%s' stringify to '' (the "
+    . 'empty string), implementing the so called null-object-pattern. '
+    . 'Given Perl\'s "globally cooperative" exception handling using this '
+    . 'class of exceptions is extremely dangerous, as it may (and often '
+    . 'does) result in silent discarding of errors. DBIx::Class tries to '
+    . 'work around this as much as possible, but other parts of your '
+    . 'software stack may not be even aware of the problem. Please submit '
+    . 'a bugreport against the distribution containing %s',
+
+      ($class) x 2,
+    ));
+
+    $not_blank = 1;
+  }
 
   return $not_blank;
 }
 
+{
+  my $callstack_state;
+
+  # Recreate the logic of try(), while reusing the catch()/finally() as-is
+  #
+  # FIXME: We need to move away from Try::Tiny entirely (way too heavy and
+  # yes, shows up ON TOP of profiles) but this is a batle for another maint
+  sub dbic_internal_try (&;@) {
+
+    my $try_cref = shift;
+    my $catch_cref = undef;  # apparently this is a thing... https://rt.perl.org/Public/Bug/Display.html?id=119311
+
+    for my $arg (@_) {
+
+      if( ref($arg) eq 'Try::Tiny::Catch' ) {
+
+        croak 'dbic_internal_try() may not be followed by multiple catch() blocks'
+          if $catch_cref;
+
+        $catch_cref = $$arg;
+      }
+      elsif ( ref($arg) eq 'Try::Tiny::Finally' ) {
+        croak 'dbic_internal_try() does not support finally{}';
+      }
+      else {
+        croak(
+          'dbic_internal_try() encountered an unexpected argument '
+        . "'@{[ defined $arg ? $arg : 'UNDEF' ]}' - perhaps "
+        . 'a missing semi-colon before or ' # trailing space important
+        );
+      }
+    }
+
+    my $wantarray = wantarray;
+    my $preexisting_exception = $@;
+
+    my @ret;
+    my $all_good = eval {
+      $@ = $preexisting_exception;
+
+      local $callstack_state->{in_internal_try} = 1
+        unless $callstack_state->{in_internal_try};
+
+      # always unset - someone may have snuck it in
+      local $SIG{__DIE__}
+        if $SIG{__DIE__};
+
+
+      if( $wantarray ) {
+        @ret = $try_cref->();
+      }
+      elsif( defined $wantarray ) {
+        $ret[0] = $try_cref->();
+      }
+      else {
+        $try_cref->();
+      }
+
+      1;
+    };
+
+    my $exception = $@;
+    $@ = $preexisting_exception;
+
+    if ( $all_good ) {
+      return $wantarray ? @ret : $ret[0]
+    }
+    elsif ( $catch_cref ) {
+      for ( $exception ) {
+        return $catch_cref->($exception);
+      }
+    }
+
+    return;
+  }
+
+  sub in_internal_try { !! $callstack_state->{in_internal_try} }
+}
+
+{
+  my $destruction_registry = {};
+
+  sub CLONE {
+    $destruction_registry = { map
+      { defined $_ ? ( refaddr($_) => $_ ) : () }
+      values %$destruction_registry
+    };
+
+    # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+    # collected before leaving this scope. Depending on the code above, this
+    # may very well be just a preventive measure guarding future modifications
+    undef;
+  }
+
+  # This is almost invariably invoked from within DESTROY
+  # throwing exceptions won't work
+  sub detected_reinvoked_destructor {
+
+    # quick "garbage collection" pass - prevents the registry
+    # from slowly growing with a bunch of undef-valued keys
+    defined $destruction_registry->{$_} or delete $destruction_registry->{$_}
+      for keys %$destruction_registry;
+
+    if (! length ref $_[0]) {
+      printf STDERR '%s() expects a blessed reference %s',
+        (caller(0))[3],
+        Carp::longmess,
+      ;
+      return undef; # don't know wtf to do
+    }
+    elsif (! defined $destruction_registry->{ my $addr = refaddr($_[0]) } ) {
+      weaken( $destruction_registry->{$addr} = $_[0] );
+      return 0;
+    }
+    else {
+      carp_unique ( sprintf (
+        'Preventing *MULTIPLE* DESTROY() invocations on %s - an *EXTREMELY '
+      . 'DANGEROUS* condition which is *ALMOST CERTAINLY GLOBAL* within your '
+      . 'application, affecting *ALL* classes without active protection against '
+      . 'this. Diagnose and fix the root cause ASAP!!!%s',
+      refdesc $_[0],
+        ( ( $INC{'Devel/StackTrace.pm'} and ! do { local $@; eval { Devel::StackTrace->VERSION(2) } } )
+          ? " (likely culprit Devel::StackTrace\@@{[ Devel::StackTrace->VERSION ]} found in %INC, http://is.gd/D_ST_refcap)"
+          : ''
+        )
+      ));
+
+      return 1;
+    }
+  }
+}
+
+my $module_name_rx = qr/ \A [A-Z_a-z] [0-9A-Z_a-z]* (?: :: [0-9A-Z_a-z]+ )* \z /x;
+my $ver_rx =         qr/ \A [0-9]+ (?: \. [0-9]+ )* (?: \_ [0-9]+ )*        \z /x;
+
 sub modver_gt_or_eq ($$) {
   my ($mod, $ver) = @_;
 
   croak "Nonsensical module name supplied"
-    if ! defined $mod or ! length $mod;
+    if ! defined $mod or $mod !~ $module_name_rx;
 
   croak "Nonsensical minimum version supplied"
-    if ! defined $ver or $ver =~ /[^0-9\.\_]/;
+    if ! defined $ver or $ver !~ $ver_rx;
+
+  no strict 'refs';
+  my $ver_cache = ${"${mod}::__DBIC_MODULE_VERSION_CHECKS__"} ||= ( $mod->VERSION
+    ? {}
+    : croak "$mod does not seem to provide a version (perhaps it never loaded)"
+  );
+
+  ! defined $ver_cache->{$ver}
+    and
+  $ver_cache->{$ver} = do {
+
+    local $SIG{__WARN__} = sigwarn_silencer( qr/\Qisn't numeric in subroutine entry/ )
+      if SPURIOUS_VERSION_CHECK_WARNINGS;
+
+    local $@;
+    local $SIG{__DIE__};
+    eval { $mod->VERSION($ver) } ? 1 : 0;
+  };
+
+  $ver_cache->{$ver};
+}
 
-  local $SIG{__WARN__} = sigwarn_silencer( qr/\Qisn't numeric in subroutine entry/ )
-    if SPURIOUS_VERSION_CHECK_WARNINGS;
+sub modver_gt_or_eq_and_lt ($$$) {
+  my ($mod, $v_ge, $v_lt) = @_;
 
-  local $@;
-  eval { $mod->VERSION($ver) } ? 1 : 0;
+  croak "Nonsensical maximum version supplied"
+    if ! defined $v_lt or $v_lt !~ $ver_rx;
+
+  return (
+    modver_gt_or_eq($mod, $v_ge)
+      and
+    ! modver_gt_or_eq($mod, $v_lt)
+  ) ? 1 : 0;
 }
 
 {
   my $list_ctx_ok_stack_marker;
 
-  sub fail_on_internal_wantarray {
+  sub fail_on_internal_wantarray () {
     return if $list_ctx_ok_stack_marker;
 
     if (! defined wantarray) {
@@ -161,7 +420,7 @@ sub modver_gt_or_eq ($$) {
     }
 
     my $cf = 1;
-    while ( ( (caller($cf+1))[3] || '' ) =~ / :: (?:
+    while ( ( (CORE::caller($cf+1))[3] || '' ) =~ / :: (?:
 
       # these are public API parts that alter behavior on wantarray
       search | search_related | slice | search_literal
@@ -176,14 +435,23 @@ sub modver_gt_or_eq ($$) {
       $cf++;
     }
 
+    my ($fr, $want, $argdesc);
+    {
+      package DB;
+      $fr = [ CORE::caller($cf) ];
+      $want = ( CORE::caller($cf-1) )[5];
+      $argdesc = ref $DB::args[0]
+        ? DBIx::Class::_Util::refdesc($DB::args[0])
+        : 'non '
+      ;
+    };
+
     if (
-      (caller($cf))[0] =~ /^(?:DBIx::Class|DBICx::)/
+      $want and $fr->[0] =~ /^(?:DBIx::Class|DBICx::)/
     ) {
-      my $obj = shift;
-
       DBIx::Class::Exception->throw( sprintf (
-        "Improper use of %s(%s) instance in list context at %s line %d\n\n\tStacktrace starts",
-        ref($obj), hrefaddr($obj), (caller($cf))[1,2]
+        "Improper use of %s instance in list context at %s line %d\n\n    Stacktrace starts",
+        $argdesc, @{$fr}[1,2]
       ), 'with_stacktrace');
     }
 
@@ -193,4 +461,33 @@ sub modver_gt_or_eq ($$) {
   }
 }
 
+sub fail_on_internal_call {
+  my ($fr, $argdesc);
+  {
+    package DB;
+    $fr = [ CORE::caller(1) ];
+    $argdesc = ref $DB::args[0]
+      ? DBIx::Class::_Util::refdesc($DB::args[0])
+      : undef
+    ;
+  };
+
+  if (
+    $argdesc
+      and
+    $fr->[0] =~ /^(?:DBIx::Class|DBICx::)/
+      and
+    $fr->[1] !~ /\b(?:CDBICompat|ResultSetProxy)\b/  # no point touching there
+  ) {
+    DBIx::Class::Exception->throw( sprintf (
+      "Illegal internal call of indirect proxy-method %s() with argument %s: examine the last lines of the proxy method deparse below to determine what to call directly instead at %s on line %d\n\n%s\n\n    Stacktrace starts",
+      $fr->[3], $argdesc, @{$fr}[1,2], ( $fr->[6] || do {
+        require B::Deparse;
+        no strict 'refs';
+        B::Deparse->new->coderef2text(\&{$fr->[3]})
+      }),
+    ), 'with_stacktrace');
+  }
+}
+
 1;
index d8f5344..4cc21f0 100644 (file)
@@ -15,7 +15,9 @@ $DEBUG = 0 unless defined $DEBUG;
 use Exporter;
 use SQL::Translator::Utils qw(debug normalize_name);
 use DBIx::Class::Carp qw/^SQL::Translator|^DBIx::Class|^Try::Tiny/;
+use DBIx::Class::_Util 'dbic_internal_try';
 use DBIx::Class::Exception;
+use Class::C3::Componentised;
 use Scalar::Util 'blessed';
 use Try::Tiny;
 use namespace::clean;
@@ -53,8 +55,11 @@ sub parse {
     DBIx::Class::Exception->throw('No DBIx::Class::Schema') unless ($dbicschema);
 
     if (!ref $dbicschema) {
-      eval "require $dbicschema"
-        or DBIx::Class::Exception->throw("Can't load $dbicschema: $@");
+      dbic_internal_try {
+        Class::C3::Componentised->ensure_class_loaded($dbicschema)
+      } catch {
+        DBIx::Class::Exception->throw("Can't load $dbicschema: $_");
+      }
     }
 
     if (
@@ -163,17 +168,17 @@ sub parse {
         # global add_fk_index set in parser_args
         my $add_fk_index = (exists $args->{add_fk_index} && ! $args->{add_fk_index}) ? 0 : 1;
 
-        foreach my $rel (sort @rels)
-        {
+        REL:
+        foreach my $rel (sort @rels) {
 
             my $rel_info = $source->relationship_info($rel);
 
             # Ignore any rel cond that isn't a straight hash
             next unless ref $rel_info->{cond} eq 'HASH';
 
-            my $relsource = try { $source->related_source($rel) };
+            my $relsource = dbic_internal_try { $source->related_source($rel) };
             unless ($relsource) {
-              carp "Ignoring relationship '$rel' - related resultsource '$rel_info->{class}' is not registered with this schema\n";
+              carp "Ignoring relationship '$rel' on '$moniker' - related resultsource '$rel_info->{class}' is not registered with this schema\n";
               next;
             };
 
@@ -186,13 +191,18 @@ sub parse {
             # support quoting properly to be signaled about this
             $rel_table = $$rel_table if ref $rel_table eq 'SCALAR';
 
-            my $reverse_rels = $source->reverse_relationship_info($rel);
-            my ($otherrelname, $otherrelationship) = each %{$reverse_rels};
-
             # Force the order of @cond to match the order of ->add_columns
             my $idx;
             my %other_columns_idx = map {'foreign.'.$_ => ++$idx } $relsource->columns;
-            my @cond = sort { $other_columns_idx{$a} cmp $other_columns_idx{$b} } keys(%{$rel_info->{cond}});
+
+            for ( keys %{$rel_info->{cond}} ) {
+              unless (exists $other_columns_idx{$_}) {
+                carp "Ignoring relationship '$rel' on '$moniker' - related resultsource '@{[ $relsource->source_name ]}' does not contain one of the specified columns: '$_'\n";
+                next REL;
+              }
+            }
+
+            my @cond = sort { $other_columns_idx{$a} <=> $other_columns_idx{$b} } keys(%{$rel_info->{cond}});
 
             # Get the key information, mapping off the foreign/self markers
             my @refkeys = map {/^\w+\.(\w+)$/} @cond;
@@ -217,6 +227,8 @@ sub parse {
                 $fk_constraint = not $source->_compare_relationship_keys(\@keys, \@primary);
             }
 
+            my ($otherrelname, $otherrelationship) = %{ $source->reverse_relationship_info($rel) };
+
             my $cascade;
             for my $c (qw/delete update/) {
                 if (exists $rel_info->{attrs}{"on_$c"}) {
@@ -252,9 +264,12 @@ sub parse {
                     $tables{$table_name}{foreign_table_deps}{$rel_table}++;
                   }
 
+                  # trim schema before generating constraint/index names
+                  (my $table_abbrev = $table_name) =~ s/ ^ [^\.]+ \. //x;
+
                   $table->add_constraint(
                     type             => 'foreign_key',
-                    name             => join('_', $table_name, 'fk', @keys),
+                    name             => join('_', $table_abbrev, 'fk', @keys),
                     fields           => \@keys,
                     reference_fields => \@refkeys,
                     reference_table  => $rel_table,
@@ -275,8 +290,9 @@ sub parse {
                   next if join("\x00", @keys) eq join("\x00", @primary);
 
                   if ($add_fk_index_rel) {
+                      (my $idx_name = $table_name) =~ s/ ^ [^\.]+ \. //x;
                       my $index = $table->add_index(
-                          name   => join('_', $table_name, 'idx', @keys),
+                          name   => join('_', $table_abbrev, 'idx', @keys),
                           fields => \@keys,
                           type   => 'NORMAL',
                       );
@@ -517,12 +533,13 @@ Limit the amount of parsed sources by supplying an explicit list of source names
 
 L<SQL::Translator>, L<DBIx::Class::Schema>
 
-=head1 AUTHORS
-
-See L<DBIx::Class/CONTRIBUTORS>.
+=head1 FURTHER QUESTIONS?
 
-=head1 LICENSE
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
 
-You may distribute this code under the same terms as Perl itself.
+=head1 COPYRIGHT AND LICENSE
 
-=cut
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
index 0af77d7..90c61fd 100644 (file)
@@ -16,6 +16,17 @@ SQL::Translator::Producer::DBIx::Class::File - DBIx::Class file producer
 
 Creates a DBIx::Class::Schema for use with DBIx::Class
 
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+
 =cut
 
 use strict;
index d29b1bc..7760de2 100644 (file)
@@ -1,24 +1,21 @@
 require File::Spec;
 require File::Find;
 
-my $xt_dirs;
+my $xt_dist_dirs;
 File::Find::find(sub {
-  return if $xt_dirs->{$File::Find::dir};
-  $xt_dirs->{$File::Find::dir} = 1 if (
+  return if $xt_dist_dirs->{$File::Find::dir};
+  $xt_dist_dirs->{$File::Find::dir} = 1 if (
     $_ =~ /\.t$/ and -f $_
   );
-}, 'xt');
+}, 'xt/dist');
 
-my @xt_tests = map { File::Spec->catfile($_, '*.t') } sort keys %$xt_dirs;
-
-# this will add the xt tests to the `make test` target among other things
-Meta->tests(join (' ', map { $_ || () } @xt_tests, Meta->tests ) );
+my @xt_dist_tests = map { File::Spec->catfile($_, '*.t') } sort keys %$xt_dist_dirs;
 
 # inject an explicit xt test run, mainly to check the contents of
 # lib and the generated POD's *before* anything is copied around
 #
-# at the end rerun the whitespace test in the distdir, to make sure everything
-# is pristine
+# at the end rerun the whitespace and footer tests in the distdir
+# to make sure everything is pristine
 postamble <<"EOP";
 
 dbic_clonedir_copy_generated_pod : test_xt
@@ -38,14 +35,14 @@ test_xt : pm_to_blib
     ),
     # test list
     join( ' ',
-      map { $mm_proto->quote_literal($_) } @xt_tests
+      map { $mm_proto->quote_literal($_) } @xt_dist_tests
     ),
   )
 ]}
 
-create_distdir : dbic_distdir_retest_whitespace
+create_distdir : dbic_distdir_retest_ws_and_footers
 
-dbic_distdir_retest_whitespace :
+dbic_distdir_retest_ws_and_footers :
 \t@{[
   $mm_proto->cd (
     '$(DISTVNAME)',
@@ -55,7 +52,7 @@ dbic_distdir_retest_whitespace :
         '$(ABSPERLRUN)',
         map { $mm_proto->quote_literal($_) } qw(-Ilib -e $ENV{RELEASE_TESTING}=1;$ENV{DBICTEST_NO_MAKEFILE_VERIFICATION}=1;)
       ),
-      'xt/whitespace.t'
+      'xt/dist/postdistdir/*.t',
     )
   )
 ]}
index 152a390..e83e03d 100644 (file)
@@ -32,12 +32,19 @@ else {
 EOW
 
   require DBIx::Class::Optional::Dependencies;
-  my %reqs_for_group = %{DBIx::Class::Optional::Dependencies->req_group_list};
 
   # exclude the rdbms_* groups which are for DBIC users
-  $opt_testdeps = {
-    map { %{$reqs_for_group{$_}} } grep { !/^rdbms_|^dist_/ } keys %reqs_for_group
-  };
+  # and the moose-related stuff iff we are under 5.8.3
+  $opt_testdeps = DBIx::Class::Optional::Dependencies->req_list_for([
+    grep {
+      !/^rdbms_|^dist_/
+        and
+      ( "$]" > 5.008002 or !/^ (?: test_ )? (?: admin | admin_script | replicated ) $/x )
+    } keys %{DBIx::Class::Optional::Dependencies->req_group_list}
+  ]);
+
+  # this one is "special" - we need it both in optdeps and as a hard dep
+  delete $opt_testdeps->{'DBD::SQLite'};
 
   print "Including all optional deps\n";
   $reqs->{test_requires} = {
@@ -91,6 +98,9 @@ END {
 
   if (keys %removed_build_requires) {
     print "Regenerating META with author requires excluded\n";
+    # M::I understands unicode in meta but does not write with the right
+    # layers - fhtagn!!!
+    local $SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /Wide character in print/ };
     Meta->write;
   }
 
diff --git a/maint/Makefile.PL.inc/21_meta_noindex.pl b/maint/Makefile.PL.inc/21_meta_noindex.pl
deleted file mode 100644 (file)
index 062e74c..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-print "Appending to the no_index META list\n";
-
-# Deprecated/internal modules need no exposure when building the meta
-no_index directory => $_ for (qw|
-  lib/DBIx/Class/Admin
-  lib/DBIx/Class/PK/Auto
-  lib/DBIx/Class/CDBICompat
-  maint
-|);
-no_index package => $_ for (qw/
-  DBIx::Class::Storage::DBIHacks
-  DBIx::Class::Storage::BlockRunner
-  DBIx::Class::Carp
-  DBIx::Class::_Util
-  DBIx::Class::ResultSet::Pager
-/);
-
-# keep the Makefile.PL eval happy
-1;
diff --git a/maint/Makefile.PL.inc/21_set_meta.pl b/maint/Makefile.PL.inc/21_set_meta.pl
new file mode 100644 (file)
index 0000000..cfb12e7
--- /dev/null
@@ -0,0 +1,68 @@
+# principal author list is kinda mandated by spec, luckily is rather static
+author 'mst: Matt S Trout <mst@shadowcat.co.uk> (project founder - original idea, architecture and implementation)';
+author 'castaway: Jess Robinson <castaway@desert-island.me.uk> (lions share of the reference documentation and manuals)';
+author 'ribasushi: Peter Rabbitson <ribasushi@cpan.org> (present day maintenance and controlled evolution)';
+
+# pause sanity
+Meta->{values}{x_authority} = 'cpan:RIBASUSHI';
+
+# !!!experimental!!!
+#
+# <ribasushi> am wondering if an x_parallel_test => 1 and x_parallel_depchain_test => 1 would be of use in meta
+# <ribasushi> to signify "project keeps tabs on itself and depchain to be in good health wrt running tests in parallel"
+# <ribasushi> and having cpan(m) tack a -j6 automatically for that
+# <ribasushi> it basically allows you to first consider any "high level intermediate dist" advertising "all my stuff works" so that larger swaths of CPAN get installed first under parallel
+# <ribasushi> note - this is not "spur of the moment" - I first started testing my depchain in parallel 3 years ago
+# <ribasushi> and have had it stable ( religiously tested on travis on any commit ) for about 2 years now
+#
+Meta->{values}{x_parallel_test_certified} = 1;
+Meta->{values}{x_dependencies_parallel_test_certified} = 1;
+
+# populate x_contributors
+# a direct dump of the sort is ok - xt/authors.t guarantees source sanity
+Meta->{values}{x_contributors} = [ do {
+  # according to #p5p this is how one safely reads random unicode
+  # this set of boilerplate is insane... wasn't perl unicode-king...?
+  no warnings 'once';
+  require Encode;
+  require PerlIO::encoding;
+  local $PerlIO::encoding::fallback = Encode::FB_CROAK();
+
+  open (my $fh, '<:encoding(UTF-8)', 'AUTHORS') or die "Unable to open AUTHORS - can't happen: $!\n";
+  map { chomp; ( (! $_ or $_ =~ /^\s*\#/) ? () : $_ ) } <$fh>;
+
+}];
+
+# legalese
+license 'perl';
+resources 'license' => 'http://dev.perl.org/licenses/';
+
+# misc resources
+abstract_from 'lib/DBIx/Class.pm';
+resources 'homepage'    => 'http://www.dbix-class.org/';
+resources 'IRC'         => 'irc://irc.perl.org/#dbix-class';
+resources 'repository'  => 'https://github.com/dbsrgits/DBIx-Class';
+resources 'MailingList' => 'http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/dbix-class';
+resources 'bugtracker'  => 'http://rt.cpan.org/NoAuth/Bugs.html?Dist=DBIx-Class';
+
+# nothing determined at runtime, except for possibly SQLT dep
+# (see the check around DBICTEST_SQLT_DEPLOY in Makefile.PL)
+dynamic_config 0;
+
+# Deprecated/internal modules need no exposure when building the meta
+no_index directory => $_ for (qw|
+  lib/DBIx/Class/Admin
+  lib/DBIx/Class/PK/Auto
+  lib/DBIx/Class/CDBICompat
+  maint
+|);
+no_index package => $_ for (qw/
+  DBIx::Class::Storage::DBIHacks
+  DBIx::Class::Storage::BlockRunner
+  DBIx::Class::Carp
+  DBIx::Class::_Util
+  DBIx::Class::ResultSet::Pager
+/);
+
+# keep the Makefile.PL eval happy
+1;
index 22d21fd..b97c2f0 100644 (file)
@@ -1,29 +1,24 @@
 
-my $dbic_ver_re = qr/ (\d) \. (\d{2}) (\d{3}) (?: _ (\d{2}) )? /x; # not anchored!!!
+my $dbic_ver_re = qr/ 0 \. (\d{2}) (\d{2}) (\d{2}) (?: _ (\d{2}) )? /x; # not anchored!!!
 
 my $version_string = Meta->version;
 my $version_value = eval $version_string;
 
 my ($v_maj, $v_min, $v_point, $v_dev) = $version_string =~ /^$dbic_ver_re$/
   or die sprintf (
-    "Invalid version %s (as specified in %s)\nCurrently valid version formats are M.VVPPP or M.VVPPP_DD\n",
+    "Invalid version %s (as specified in %s)\nCurrently valid version formats are 0.MMVVPP or 0.MMVVPP_DD\n",
     $version_string,
     Meta->{values}{version_from} || Meta->{values}{all_from} || 'Makefile.PL',
   )
 ;
 
-if ($v_maj != 0 or $v_min > 8) {
+if ($v_maj > 8) {
   die "Illegal version $version_string - we are still in the 0.08 cycle\n"
 }
 
-if ($v_point >= 300) {
-  die "Illegal version $version_string - we are still in the 0.082xx cycle\n"
-}
-
-Meta->makemaker_args->{DISTVNAME} = Meta->name . "-$version_string-TRIAL" if (
-  # all odd releases *after* 0.08200 generate a -TRIAL, no exceptions
-  ( $v_point > 200 and int($v_point / 100) % 2 )
-);
+#Meta->makemaker_args->{DISTVNAME} = Meta->name . "-$version_string-TRIAL" if (
+#  ( $v_point > 89 )
+#);
 
 
 my $tags = { map { chomp $_; $_ => 1} `git tag` };
@@ -32,13 +27,15 @@ if (keys %$tags) {
   my $shipped_versions;
   my $shipped_dev_versions;
 
+  my $legacy_re = qr/^ v 0 \. (\d{2}) (\d{2}) (\d) (?: _ (\d{2}) )? $/x;
+
   for (keys %$tags) {
-    if ($_ =~ /^v$dbic_ver_re$/) {
+    if ($_ =~ /^v$dbic_ver_re$/ or $_ =~ $legacy_re ) {
       if (defined $4) {
-        $shipped_dev_versions->{"$1.$2$3$4"} = 1;
+        $shipped_dev_versions->{"0.$1$2$3$4"} = 1;
       }
       else {
-        $shipped_versions->{"$1.$2$3"} = 1;
+        $shipped_versions->{"0.$1$2$3"} = 1;
       }
       delete $tags->{$_};
     }
index 873e669..251c184 100644 (file)
@@ -4,6 +4,7 @@
   package MY;
   sub distdir {
     (my $snippet = shift->SUPER::distdir(@_)) =~ s/^create_distdir :/create_distdir_copy_manifested :/;
+    no warnings 'qw';
     return <<"EOM";
 $snippet
 
@@ -23,7 +24,7 @@ clonedir_cleanup_generated_files :
 
 check_create_distdir_prereqs :
 \t\$(NOECHO) @{[
-  $mm_proto->oneliner("DBIx::Class::Optional::Dependencies->die_unless_req_ok_for(q(dist_dir))", [qw/-Ilib -MDBIx::Class::Optional::Dependencies/])
+  $mm_proto->oneliner("1", [qw( -Ilib -MDBIx::Class::Optional::Dependencies=-die_without,dist_dir )])
 ]}
 
 EOM
@@ -37,6 +38,7 @@ EOM
 
   sub postamble {
     my $snippet = shift->SUPER::postamble(@_);
+    no warnings 'qw';
     return <<"EOM";
 $snippet
 
@@ -44,7 +46,7 @@ upload :: check_create_distdir_prereqs check_upload_dist_prereqs
 
 check_upload_dist_prereqs :
 \t\$(NOECHO) @{[
-  $mm_proto->oneliner("DBIx::Class::Optional::Dependencies->die_unless_req_ok_for(q(dist_upload))", [qw/-Ilib -MDBIx::Class::Optional::Dependencies/])
+  $mm_proto->oneliner("1", [qw( -Ilib -MDBIx::Class::Optional::Dependencies=-die_without,dist_upload )])
 ]}
 
 EOM
diff --git a/maint/Makefile.PL.inc/52_autogen_README.pl b/maint/Makefile.PL.inc/52_autogen_README.pl
deleted file mode 100644 (file)
index 0f4a38c..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# When a long-standing branch is updated a README may still linger around
-unlink 'README' if -f 'README';
-
-# Makefile syntax allows adding extra dep-specs for already-existing targets,
-# and simply appends them on *LAST*-come *FIRST*-serve basis.
-# This allows us to inject extra depenencies for standard EUMM targets
-
-require File::Spec;
-my $dir = File::Spec->catdir(qw(maint .Generated_Pod));
-my $fn = File::Spec->catfile($dir, 'README');
-
-postamble <<"EOP";
-
-clonedir_generate_files : dbic_clonedir_gen_readme
-
-dbic_clonedir_gen_readme :
-\t@{[ $mm_proto->oneliner('mkpath', ['-MExtUtils::Command']) ]} $dir
-\tpod2text lib/DBIx/Class.pm > $fn
-
-EOP
-
-# keep the Makefile.PL eval happy
-1;
index ec6c1a1..ff72fd9 100644 (file)
@@ -56,6 +56,7 @@ EOP
   my $great_success;
   {
     local @ARGV = ('--documentation-as-pod', $pod_fn);
+    local $0 = 'dbicadmin';
     local *CORE::GLOBAL::exit = sub { $great_success++; die; };
     do 'script/dbicadmin';
   }
@@ -92,6 +93,32 @@ EOP
 }
 
 
+# generate the DBIx/Class.pod only during distdir
+{
+  my $dist_pod_fn = File::Spec->catfile($pod_dir, qw(lib DBIx Class.pod));
+
+  postamble <<"EOP";
+
+clonedir_generate_files : dbic_distdir_gen_dbic_pod
+
+dbic_distdir_gen_dbic_pod :
+
+\tperldoc -u lib/DBIx/Class.pm > $dist_pod_fn
+\t@{[ $mm_proto->oneliner(
+  "s!^.*?this line is replaced with the author list.*! qq{List of the awesome contributors who made DBIC v$ver possible\n\n} . qx(\$^X -Ilib maint/gen_pod_authors)!me",
+  [qw( -0777 -p -i )]
+) ]} $dist_pod_fn
+
+create_distdir : dbic_distdir_defang_authors
+
+# Remove the maintainer-only warning (be nice ;)
+dbic_distdir_defang_authors :
+\t@{[ $mm_proto->oneliner('s/ ^ \s* \# \s* \*\*\* .+ \n ( ^ \s* \# \s*? \n )? //xmg', [qw( -0777 -p -i )] ) ]} \$(DISTVNAME)/AUTHORS
+
+EOP
+}
+
+
 # on some OSes generated files may have an incorrect \n - fix it
 # so that the xt tests pass on a fresh checkout (also shipping a
 # dist with CRLFs is beyond obnoxious)
diff --git a/maint/Makefile.PL.inc/54_autogen_legalese_and_README.pl b/maint/Makefile.PL.inc/54_autogen_legalese_and_README.pl
new file mode 100644 (file)
index 0000000..8b96f50
--- /dev/null
@@ -0,0 +1,42 @@
+# When a long-standing branch is updated a README may still linger around
+unlink 'README' if -f 'README';
+
+# Makefile syntax allows adding extra dep-specs for already-existing targets,
+# and simply appends them on *LAST*-come *FIRST*-serve basis.
+# This allows us to inject extra depenencies for standard EUMM targets
+
+require File::Spec;
+my $dir = File::Spec->catdir(qw(maint .Generated_Pod));
+my $r_fn = File::Spec->catfile($dir, 'README');
+
+my $start_file = sub {
+  my $fn = $mm_proto->quote_literal(shift);
+  return join "\n",
+    qq{\t\$(NOECHO) \$(RM_F) $fn},
+    ( map { qq(\t\$(NOECHO) \$(ECHO) "$_" >> $fn) } (
+      "DBIx::Class is Copyright (c) 2005-@{[ (gmtime)[5] + 1900  ]} by mst, castaway, ribasushi, and others.",
+      "See AUTHORS and LICENSE included with this distribution. All rights reserved.",
+      "",
+    )),
+  ;
+};
+
+postamble <<"EOP";
+
+clonedir_generate_files : dbic_clonedir_gen_readme
+
+dbic_clonedir_gen_readme : dbic_distdir_gen_dbic_pod
+@{[ $start_file->($r_fn) ]}
+\tpod2text $dir/lib/DBIx/Class.pod >> $r_fn
+
+create_distdir : dbic_distdir_regen_license
+
+dbic_distdir_regen_license :
+@{[ $start_file->( File::Spec->catfile( Meta->name . '-' . Meta->version, 'LICENSE') ) ]}
+\t@{[ $mm_proto->oneliner('cat', ['-MExtUtils::Command']) ]} LICENSE >> \$(DISTVNAME)/LICENSE
+
+EOP
+
+
+# keep the Makefile.PL eval happy
+1;
diff --git a/maint/gen_pod_authors b/maint/gen_pod_authors
new file mode 100755 (executable)
index 0000000..e814dc5
--- /dev/null
@@ -0,0 +1,27 @@
+#!/usr/bin/env perl
+
+use warnings;
+use strict;
+
+# we will be outputting *ENCODED* utf8, hence the raw open below
+# the file is already sanity-checked by xt/authors.t
+my @known_authors = do {
+  open (my $fh, '<:raw', 'AUTHORS') or die "Unable to open AUTHORS - can't happen: $!\n";
+  map { chomp; ( ( ! $_ or $_ =~ /^\s*\#/ ) ? () : $_ ) } <$fh>;
+} or die "Known AUTHORS file seems empty... can't happen...";
+
+$_ =~ s!^ ( [^\:]+ ) : \s !B<$1>: !x
+  for @known_authors;
+
+$_ =~ s!( \b https? :// [^\s\>]+ )!L<$1|$1>!x
+  for @known_authors;
+
+print join "\n\n",
+  '=encoding utf8',
+  '=over',
+  @known_authors,
+  '=back',
+  '',
+;
+
+1;
index db0f65a..e441e88 100755 (executable)
@@ -3,6 +3,8 @@
 use warnings;
 use strict;
 
+use DBIx::Class::_Util; # load early in case any shims are needed
+
 my $lib_dir = 'lib';
 my $pod_dir = 'maint/.Generated_Pod';
 
diff --git a/maint/getstatus b/maint/getstatus
new file mode 100755 (executable)
index 0000000..f49c410
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Config;
+use Term::ANSIColor ':constants';
+my $CRST = RESET;
+my $CCODE = BOLD;
+my $CSTAT = BOLD . GREEN;
+my $CCORE = BOLD . CYAN;
+my $CSIG = CYAN;
+
+if (@ARGV) {
+  my $code = system (@ARGV);
+
+  if ($code < 0) {
+    exit 127;
+  }
+  elsif ($code > 0) {
+
+    my $status = $code >> 8;
+    my $signum = $code & 127;
+    my $core = $code & 128;
+
+    my %sig_idx;
+    @sig_idx{split /\s+/, $Config{sig_num}} = split /\s/, $Config{sig_name};
+
+    printf STDERR (
+<<EOF
+
+Results of execution: `%s`
+----------------------
+System exit code:$CCODE %d $CRST$CSIG %s $CRST
+ ($CSTAT%08b$CRST$CCORE%b$CRST$CSIG%07b$CRST)
+
+Status: %3s ($CSTAT%08b$CRST)
+Signal: %3s ($CSIG%08b$CRST)
+Core:   %3s
+----------------------
+EOF
+    , (join ' ', @ARGV),
+      $code, ($signum ? "(SIG-$sig_idx{$signum})" : ''),
+      $status, $core, $signum,
+      ($status) x 2,
+      ($signum) x 2,
+      ($core ? 'Yes': 'No')
+    );
+
+    exit ($status);
+  }
+}
diff --git a/maint/git_config_dbic.inc b/maint/git_config_dbic.inc
new file mode 100644 (file)
index 0000000..7eb1f0e
--- /dev/null
@@ -0,0 +1,44 @@
+[remote "ghpr"]
+  url = https://github.com/dbsrgits/DBIx-Class
+  pushurl = DISALLOWED
+  fetch = +refs/pull/*/head:refs/remotes/ghpr/*
+
+[remote "historic"]
+  url = git://git.shadowcat.co.uk/dbsrgits/DBIx-Class-Historic.git
+  pushurl = ssh://dbsrgits@git.shadowcat.co.uk/DBIx-Class-Historic.git
+  fetch = +refs/heads/*:refs/remotes/historic/*
+
+[remote "debian"]
+  url = git://anonscm.debian.org/pkg-perl/packages/libdbix-class-perl.git
+  pushurl = DISALLOWED
+  fetch = +refs/heads/master:refs/remotes/debian/master
+  tagopt = --no-tags
+
+[alias]
+  # Lines after gitk in order:
+  #
+  # --exclude all refs matching the for loop
+  # all known refs (branches/tags) excepth what we excluded
+  # add all individual stashes
+  # add all github PR heads without a matching historic/ghpr/* entry
+  #
+  # the /bin/true at the end is there to eat away any args to 'vis'
+  # ( otherwise they will be treated as commands to execute after the & )
+  vis = "!gitk \
+    $( for r in historic/ghpr ghpr debian ; do echo "--exclude=refs/remotes/$r/*" ; done ) \
+    --all \
+    $(git stash list | cut -f 1 -d ':') \
+    $(/bin/bash -c \"/usr/bin/comm -23 \
+      <(git for-each-ref --sort=objectname --format='%(objectname:short)' refs/remotes/ghpr/ ) \
+      <(git for-each-ref --sort=objectname --format='%(objectname:short)' refs/remotes/historic/ghpr/ refs/remotes/origin/ ) \
+    \") \
+  \"$@\" & /bin/true"
+
+
+  # same but only for GitHub PRs
+  prvis = "!gitk \
+    $(/bin/bash -c \"/usr/bin/comm -23 \
+      <(git for-each-ref --sort=objectname --format='%(objectname:short)' refs/remotes/ghpr/ ) \
+      <(git for-each-ref --sort=objectname --format='%(objectname:short)' refs/remotes/historic/ghpr/ refs/remotes/origin/ ) \
+    \") \
+  \"$@\" & /bin/true"
index f861b0e..6674259 100755 (executable)
 #!/bin/bash
 
-source maint/travis-ci_scripts/common.bash
-if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then return ; fi
+export SHORT_CIRCUIT_SMOKE
+
+if have_sudo ; then
 
-# Different boxes we run on may have different amount of hw threads
-# Hence why we need to query
-# Originally we used to read /sys/devices/system/cpu/online
-# but it is not available these days (odd). Thus we fall to
-# the alwas-present /proc/cpuinfo
-# The oneliner is a tad convoluted - basicaly what we do is
-# slurp the entire file and get the index off the last
-# `processor    : XX` line
-export NUMTHREADS="$(( $(perl -0777 -n -e 'print (/ (?: .+ ^ processor \s+ : \s+ (\d+) ) (?! ^ processor ) /smx)' < /proc/cpuinfo) + 1 ))"
+  # Stop pre-started RDBMS, move their data back to disk (save RAM)
+  # sync for some settle time (not available on all platforms)
+  for d in mysql postgresql ; do
+    # maybe not even running
+    run_or_err "Stopping $d" "sudo /etc/init.d/$d stop || /bin/true"
+
+    # no longer available on newer build systems
+    if [[ -d /var/ramfs/$d ]] ; then
+      sudo rm -rf /var/lib/$d
+      sudo mv /var/ramfs/$d /var/lib/
+      sudo ln -s /var/lib/$d /var/ramfs/$d
+    fi
+  done
+  /bin/sync
+fi
 
-export CACHE_DIR="/tmp/poormanscache"
+# Sanity check VM before continuing
+echo "
+=============================================================================
+
+= Startup Meminfo
+$(free -m -t)
+
+============================================================================="
+
+CI_VM_MIN_FREE_MB=2000
+if [[ "$(free -m | grep 'buffers/cache:' | perl -p -e '$_ = (split /\s+/, $_)[3]')" -lt "$CI_VM_MIN_FREE_MB" ]]; then
+  SHORT_CIRCUIT_SMOKE=1
+  echo_err "
+=============================================================================
+
+CI virtual machine stuck in a state with a lot of memory locked for no reason.
+Under Travis this state usually results in a failed build.
+Short-circuiting buildjob to avoid false negatives, please restart it manually.
+
+============================================================================="
+
+# pull requests are always scrutinized after the fact anyway - run a
+# a simpler matrix
+elif [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then
+  if [[ -n "$BREWVER" ]]; then
+    # just don't brew anything
+    SHORT_CIRCUIT_SMOKE=1
+  else
+    # running PRs with 1 thread is non-sensical
+    VCPU_USE=""
+  fi
+fi
 
-# install some common tools from APT, more below unless CLEANTEST
-apt_install libapp-nopaste-perl tree apt-transport-https
+if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then return ; fi
+
+# Previously we were going off the OpenVZ vcpu count and dividing by 3
+# With the new infrastructure, somply go with "something high"
+export VCPU_AVAILABLE=10
+
+if [[ -z "$VCPU_USE" ]] ; then
+  export VCPU_USE="$VCPU_AVAILABLE"
+fi
 
-# FIXME - the debian package is oddly broken - uses a bin/env based shebang
-# so nothing works under a brew. Fix here until #debian-perl patches it up
-sudo /usr/bin/perl -p -i -e 's|#!/usr/bin/env perl|#!/usr/bin/perl|' $(which nopaste)
 
 if [[ "$CLEANTEST" != "true" ]]; then
-### apt-get invocation - faster to grab everything at once
+
+  if [[ -z "$(tail -n +2 /proc/swaps)" ]] ; then
+    run_or_err "Configuring swap (for Oracle)" \
+      "sudo bash -c 'dd if=/dev/zero of=/swap.img bs=256M count=5 && chmod 600 /swap.img && mkswap /swap.img && swapon /swap.img'"
+  fi
+
+  export CACHE_DIR="/tmp/poormanscache"
+
   #
   # FIXME these debconf lines should automate the firebird config but do not :(((
   sudo bash -c 'echo -e "firebird2.5-super\tshared/firebird/enabled\tboolean\ttrue" | debconf-set-selections'
   sudo bash -c 'echo -e "firebird2.5-super\tshared/firebird/sysdba_password/new_password\tpassword\t123" | debconf-set-selections'
 
-  # add extra APT repo for Oracle
-  # (https is critical - apt-get update can't seem to follow the 302)
-  sudo bash -c 'echo -e "\ndeb [arch=i386] https://oss.oracle.com/debian unstable main non-free" >> /etc/apt/sources.list'
+  # these APT sources do not mean anything to us anyway
+  sudo rm -rf /etc/apt/sources.list.d/*
+
+  # the actual package is built for lucid, installs fine on both precise and trusty
+  sudo bash -c 'echo "deb http://archive.canonical.com/ubuntu precise partner" >> /etc/apt/sources.list'
 
-  run_or_err "Cloning poor man's cache from github" "git clone --depth=1 --branch=poor_mans_travis_cache https://github.com/ribasushi/travis_futzing.git $CACHE_DIR && $CACHE_DIR/reassemble"
+  # never installed, this looks like trusty
+  if [[ ! -d /var/lib/mysql ]] ; then
+    sudo dpkg --add-architecture i386
+    extra_debs="$extra_debs postgresql mysql-server"
+  fi
 
-  run_or_err "Priming up the APT cache with $(echo $(ls -d $CACHE_DIR/apt_cache/*.deb))" "sudo cp $CACHE_DIR/apt_cache/*.deb /var/cache/apt/archives"
+  # FIXME - by default db2 eats too much memory, we won't be able to test on legacy infra
+  # someone needs to add a minimizing configuration akin to 9367d187
+  if [[ "$(free -m | grep 'Mem:' | perl -p -e '$_ = (split /\s+/, $_)[1]')" -gt 4000 ]] ; then
+    extra_debs="$extra_debs db2exc"
+  fi
 
-  apt_install memcached firebird2.5-super firebird2.5-dev unixodbc-dev expect oracle-xe
+  run_or_err "Updating APT sources" "sudo apt-get update"
+
+  apt_install $extra_debs libmysqlclient-dev memcached firebird2.5-super firebird2.5-dev expect
+
+  # needs to happen separately and *after* db2exc, as the former shits all over /usr/include (wtf?!)
+  # for more info look at /opt/ibm/db2/V9.7/instance/db2iutil :: create_links()
+  apt_install unixodbc-dev
+
+  # need to stop them again, in case we installed them above (trusty)
+  for d in mysql postgresql ; do
+    run_or_err "Stopping $d" "sudo /etc/init.d/$d stop || /bin/true"
+  done
+
+  run_or_err "Cloning poor man's cache from github" "git clone --depth=1 --single-branch --branch=oracle/10.2.0 https://github.com/poormanscache/poormanscache.git $CACHE_DIR && $CACHE_DIR/reassemble"
+  run_or_err "Installing OracleXE manually from deb" \
+    "sudo dpkg -i $CACHE_DIR/apt_cache/bc-multiarch-travis_1.0_all.deb $CACHE_DIR/apt_cache/oracle-xe_10.2.0.1-1.1_i386.deb || sudo bash -c 'source maint/travis-ci_scripts/common.bash && apt_install -f'"
 
 ### config memcached
   run_or_err "Starting memcached" "sudo /etc/init.d/memcached start"
   export DBICTEST_MEMCACHED=127.0.0.1:11211
 
 ### config mysql
-  run_or_err "Creating MySQL TestDB" "mysql -e 'create database dbic_test;'"
+  run_or_err "Installing minimizing MySQL config" "\
+     sudo bash -c 'rm /var/lib/mysql/ib*' \
+  && sudo cp maint/travis-ci_scripts/configs/minimal_mysql_travis.cnf /etc/mysql/conf.d/ \
+  && sudo chmod 644 /etc/mysql/conf.d/*.cnf \
+  "
+
+  run_or_err "Starting MySQL" "sudo /etc/init.d/mysql start"
+  run_or_err "Creating MySQL TestDB" "mysql -u root -e 'create database dbic_test;'"
   export DBICTEST_MYSQL_DSN='dbi:mysql:database=dbic_test;host=127.0.0.1'
   export DBICTEST_MYSQL_USER=root
 
 ### config pg
+  run_or_err "Starting PostgreSQL" "sudo /etc/init.d/postgresql start"
   run_or_err "Creating PostgreSQL TestDB" "psql -c 'create database dbic_test;' -U postgres"
   export DBICTEST_PG_DSN='dbi:Pg:database=dbic_test;host=127.0.0.1'
   export DBICTEST_PG_USER=postgres
@@ -61,27 +143,19 @@ if [[ "$CLEANTEST" != "true" ]]; then
     send "\177\177\177\177yes\r"
     expect "Password for SYSDBA"
     send "123\r"
-    sleep 1
+    sleep 2
     expect eof
   '
   # creating testdb
   # FIXME - this step still fails from time to time >:(((
   # has to do with the FB reconfiguration I suppose
   # for now if it fails twice - simply skip FB testing
-  for i in 1 2 ; do
+  for i in 1 2 3 ; do
 
     run_or_err "Re-configuring Firebird" "
       sync
+      sleep 5
       DEBIAN_FRONTEND=text sudo expect -c '$EXPECT_FB_SCRIPT'
-      sleep 1
-      sync
-      # restart the server for good measure
-      sudo /etc/init.d/firebird2.5-super stop || true
-      sleep 1
-      sync
-      sudo /etc/init.d/firebird2.5-super start
-      sleep 1
-      sync
     "
 
     if run_or_err "Creating Firebird TestDB" \
@@ -185,4 +259,24 @@ FileUsage       = 1
   '"
 
   export ORACLE_HOME="$CACHE_DIR/ora_instaclient/x86-64/oracle_instaclient_10.2.0.5.0"
+
+### config db2exc
+  # we may have skipped installation due to low memory
+  if dpkg -l db2exc &>/dev/null ; then
+    # WTF is this world-writable?
+    # Strip the write bit so it doesn't trip Ubuntu's symlink-in-/tmp attack mitigation
+    sudo chmod -R o-w ~dasusr1/das
+
+    export DB2_HOME=/opt/ibm/db2/V9.7
+    export DBICTEST_DB2_DSN=dbi:DB2:DATABASE=dbictest
+    export DBICTEST_DB2_USER=db2inst1
+    export DBICTEST_DB2_PASS=abc123456
+
+    run_or_err "Set up DB2 users" \
+      "echo -e '$DBICTEST_DB2_PASS\n$DBICTEST_DB2_PASS' | sudo passwd $DBICTEST_DB2_USER"
+
+    run_or_err "Create DB2 database" \
+      "sudo -u $DBICTEST_DB2_USER -i db2 'CREATE DATABASE dbictest' && sudo -u $DBICTEST_DB2_USER -i db2 'ACTIVATE DATABASE dbictest'"
+  fi
+
 fi
index 79e75cd..515b176 100755 (executable)
@@ -1,42 +1,42 @@
 #!/bin/bash
 
-source maint/travis-ci_scripts/common.bash
 if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then return ; fi
 
-CPAN_MIRROR=$(echo "$PERL_CPANM_OPT" | grep -oP -- '--mirror\s+\S+' | head -n 1 | cut -d ' ' -f 2)
-if ! [[ "$CPAN_MIRROR" =~ "http://" ]] ; then
-  echo_err "Unable to extract primary cpan mirror from PERL_CPANM_OPT - something is wrong"
-  echo_err "PERL_CPANM_OPT: $PERL_CPANM_OPT"
-  CPAN_MIRROR="https://cpan.metacpan.org/"
-  PERL_CPANM_OPT="$PERL_CPANM_OPT --mirror $CPAN_MIRROR"
-  echo_err "Using $CPAN_MIRROR for the time being"
-fi
+# we need a mirror that both has the standard index and a backpan version rolled
+# into one, due to MDV testing
+CPAN_MIRROR="http://cpan.metacpan.org/"
+
+PERL_CPANM_OPT="$PERL_CPANM_OPT --mirror $CPAN_MIRROR"
 
-export PERL_MM_USE_DEFAULT=1 PERL_MM_NONINTERACTIVE=1 PERL_AUTOINSTALL_PREFER_CPAN=1 PERLBREW_CPAN_MIRROR="$CPAN_MIRROR" HARNESS_TIMER=1 MAKEFLAGS="-j$NUMTHREADS"
+# do not set PERLBREW_CPAN_MIRROR - not all backpan-like mirrors have the perl tarballs
+export PERL_MM_USE_DEFAULT=1 PERL_MM_NONINTERACTIVE=1 PERL_AUTOINSTALL_PREFER_CPAN=1 HARNESS_TIMER=1 MAKEFLAGS="-j$VCPU_USE"
 
 # try CPAN's latest offering if requested
 if [[ "$DEVREL_DEPS" == "true" ]] ; then
 
   PERL_CPANM_OPT="$PERL_CPANM_OPT --dev"
 
-  # FIXME inline-upgrade cpanm, work around https://github.com/travis-ci/travis-ci/issues/1477
-  cpanm_loc="$(which cpanm)"
-  run_or_err "Upgrading cpanm ($cpanm_loc) to latest stable" \
-    "wget -q -O $cpanm_loc cpanmin.us && chmod a+x $cpanm_loc"
 fi
 
 # Fixup CPANM_OPT to behave more like a traditional cpan client
 export PERL_CPANM_OPT="--verbose --no-interactive --no-man-pages $( echo $PERL_CPANM_OPT | sed 's/--skip-satisfied//' )"
 
 if [[ -n "$BREWVER" ]] ; then
+
   # since perl 5.14 a perl can safely be built concurrently with -j$large
   # (according to brute force testing and my power bill)
-  if [[ "$BREWVER" == "blead" ]] || perl -Mversion -e "exit !!(version->new(q($BREWVER)) < 5.014)" ; then
-    perlbrew_jopt="$NUMTHREADS"
+  if [[ "$BREWVER" =~ [A-Za-z] ]] || perl -Mversion -e "exit !!(version->new(q($BREWVER)) < 5.014)" ; then
+    perlbrew_jopt="$VCPU_USE"
+  fi
+
+  BREWSRC="$BREWVER"
+
+  if [[ "$BREWVER" == "schmorp_stableperl" ]] ; then
+    BREWSRC="http://stableperl.schmorp.de/dist/stableperl-5.22.0-1.001.tar.gz"
   fi
 
   run_or_err "Compiling/installing Perl $BREWVER (without testing, using ${perlbrew_jopt:-1} threads, may take up to 5 minutes)" \
-    "perlbrew install --as $BREWVER --notest --noman --verbose $BREWOPTS -j${perlbrew_jopt:-1}  $BREWVER"
+    "perlbrew install --as $BREWVER --notest --noman --verbose $BREWOPTS -j${perlbrew_jopt:-1}  $BREWSRC"
 
   # can not do 'perlbrew uss' in the run_or_err subshell above, or a $()
   # furthermore `perlbrew use` returns 0 regardless of whether the perl is
@@ -51,25 +51,8 @@ if [[ -n "$BREWVER" ]] ; then
 
 # no brewver - this means a travis perl, which means we want to clean up
 # the presently installed libs
-# Idea stolen from
-# https://github.com/kentfredric/Dist-Zilla-Plugin-Prereqs-MatchInstalled-All/blob/master/maint-travis-ci/sterilize_env.pl
 elif [[ "$CLEANTEST" == "true" ]] && [[ "$POISON_ENV" != "true" ]] ; then
-
-  echo_err "$(tstamp) Cleaning precompiled Travis-Perl"
-  perl -MConfig -MFile::Find -e '
-    my $sitedirs = {
-      map { $Config{$_} => 1 }
-        grep { $_ =~ /site(lib|arch)exp$/ }
-          keys %Config
-    };
-    find({ bydepth => 1, no_chdir => 1, follow_fast => 1, wanted => sub {
-      ! $sitedirs->{$_} and ( -d _ ? rmdir : unlink )
-    } }, keys %$sitedirs )
-  '
-
-  echo_err "Post-cleanup contents of sitelib of the pre-compiled Travis-Perl $TRAVIS_PERL_VERSION:"
-  echo_err "$(tree $(perl -MConfig -e 'print $Config{sitelib_stem}'))"
-  echo_err
+  purge_sitelib
 fi
 
 # configure CPAN.pm - older versions go into an endless loop
@@ -84,3 +67,73 @@ CPAN_CFG_SCRIPT="
   CPAN::Config->commit;
 "
 run_or_err "Configuring CPAN.pm" "perl -e '$CPAN_CFG_SCRIPT'"
+
+
+# These envvars are always set, more *maybe* below
+export DBIC_SHUFFLE_UNORDERED_RESULTSETS=1
+
+# bogus nonexisting DBI_*
+export DBI_DSN="dbi:ODBC:server=NonexistentServerAddress"
+export DBI_DRIVER="ADO"
+
+# some people do in fact set this - boggle!!!
+# it of course won't work before 5.8.4
+if perl -M5.008004 -e 1 &>/dev/null ; then
+  export PERL_STRICTURES_EXTRA=1
+fi
+
+
+# poison the environment
+if [[ "$POISON_ENV" = "true" ]] ; then
+
+  # look through lib, find all mentioned DBIC* ENVvars and set them to true and see if anything explodes
+  toggle_booleans=( $( grep -ohP '\bDBIC_[0-9_A-Z]+' -r lib/ --exclude-dir Optional | sort -u | grep -vP '^(DBIC_TRACE(_PROFILE)?|DBIC_.+_DEBUG)$' ) )
+
+  # some extra pollutants
+  toggle_booleans+=( \
+    DBICTEST_ASSERT_NO_SPURIOUS_EXCEPTION_ACTION \
+    DBICTEST_SQLITE_USE_FILE \
+    DBICTEST_RUN_ALL_TESTS \
+    DBICTEST_SQLITE_REVERSE_DEFAULT_ORDER \
+  )
+
+  # if we have Moose - try to run everything under replicated
+  # FIXME - when switching to Moo kill this
+  if [[ "$CLEANTEST" != "true" ]] && perl -M5.008003 -e 1 &>/dev/null ; then
+    toggle_booleans+=( DBICTEST_VIA_REPLICATED )
+  fi
+
+  for var in "${toggle_booleans[@]}"
+  do
+    if [[ -z "${!var}" ]] ; then
+      export $var=1
+      echo "POISON_ENV: setting $var to 1"
+    fi
+  done
+
+
+### emulate a local::lib-like env
+  # trick cpanm into executing true as shell - we just need the find+unpack
+  run_or_err "Downloading latest stable DBIC from CPAN" \
+    "SHELL=/bin/true cpanm --look DBIx::Class"
+
+  # move it somewhere as following cpanm will clobber it
+  run_or_err "Moving latest stable DBIC from CPAN to /tmp" "mv ~/.cpanm/latest-build/DBIx-Class-*/lib /tmp/stable_dbic_lib"
+
+  export PERL5LIB="/tmp/stable_dbic_lib:$PERL5LIB"
+
+  # perldoc -l <mod> searches $(pwd)/lib in addition to PERL5LIB etc, hence the cd /
+  echo_err "Latest stable DBIC (without deps) locatable via \$PERL5LIB at $(cd / && perldoc -l DBIx::Class)"
+
+fi
+
+if [[ "$CLEANTEST" != "true" ]] ; then
+  # using SQLT if will be available
+  # not doing later because we will be running in a subshell
+  export DBICTEST_SQLT_DEPLOY=1
+
+fi
+
+# FIXME - work around https://github.com/miyagawa/cpanminus/issues/462
+# seriously...
+perl -p -i -e 's/\blocal\$self->\{notest\}=1;//' $(which cpanm)
index 10f380c..6033440 100755 (executable)
@@ -1,42 +1,24 @@
 #!/bin/bash
 
+# this file is executed in a subshell - set up the common stuff
 source maint/travis-ci_scripts/common.bash
-if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then return ; fi
 
-# poison the environment
-if [[ "$POISON_ENV" = "true" ]] ; then
-
-  # look through lib, find all mentioned ENVvars and set them
-  # to true and see if anything explodes
-  for var in $(grep -P '\$ENV\{' -r lib/ | grep -oP 'DBIC_\w+' | sort -u | grep -v DBIC_TRACE) ; do
-    if [[ -z "${!var}" ]] ; then
-      export $var=1
-    fi
-  done
-
-  # bogus nonexisting DBI_*
-  export DBI_DSN="dbi:ODBC:server=NonexistentServerAddress"
-  export DBI_DRIVER="ADO"
-
-  # make sure tests do not rely on implicid order of returned results
-  export DBICTEST_SQLITE_REVERSE_DEFAULT_ORDER=1
-
-  # emulate a local::lib-like env
-  # trick cpanm into executing true as shell - we just need the find+unpack
-  run_or_err "Downloading latest stable DBIC from CPAN" \
-    "SHELL=/bin/true cpanm --look DBIx::Class"
+if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then exit 0 ; fi
 
-  export PERL5LIB="$( ls -d ~/.cpanm/latest-build/DBIx-Class-*/lib | tail -n1 ):$PERL5LIB"
+# The prereq-install stage will not work with both POISON and DEVREL
+# DEVREL wins
+if [[ "$DEVREL_DEPS" = "true" ]] ; then
+  export POISON_ENV=""
+fi
 
-  # perldoc -l <mod> searches $(pwd)/lib in addition to PERL5LIB etc, hence the cd /
-  echo_err "Latest stable DBIC (without deps) locatable via \$PERL5LIB at $(cd / && perldoc -l DBIx::Class)"
+# FIXME - this is a kludge in place of proper MDV testing. For the time
+# being simply use the minimum versions of our DBI/DBDstack, to avoid
+# fuckups like 0.08260 (went unnoticed for 5 months)
+if [[ "$POISON_ENV" = "true" ]] ; then
 
-  # FIXME - this is a kludge in place of proper MDV testing. For the time
-  # being simply use the minimum versions of our DBI/DBDstack, to avoid
-  # fuckups like 0.08260 (went unnoticed for 5 months)
-  #
   # use url-spec for DBI due to https://github.com/miyagawa/cpanminus/issues/328
-  if perl -M5.013003 -e1 &>/dev/null ; then
+  if [[ "$CLEANTEST" != "true" ]] || perl -M5.013003 -e1 &>/dev/null ; then
+    # the fulltest may re-upgrade DBI, be conservative only on cleantests
     # earlier DBI will not compile without PERL_POLLUTE which was gone in 5.14
     parallel_installdeps_notest T/TI/TIMB/DBI-1.614.tar.gz
   else
@@ -44,12 +26,29 @@ if [[ "$POISON_ENV" = "true" ]] ; then
   fi
 
   # Test both minimum DBD::SQLite and minimum BigInt SQLite
+  # reverse the logic from above for this (low on full, higher on clean)
   if [[ "$CLEANTEST" = "true" ]]; then
     parallel_installdeps_notest DBD::SQLite@1.37
   else
     parallel_installdeps_notest DBD::SQLite@1.29
   fi
 
+  # also try minimal tested installs *without* a compiler
+  if [[ "$CLEANTEST" = "true" ]]; then
+
+    # Clone and P::S::XS are both bugs
+    # File::Spec can go away as soon as I dump Path::Class
+    # File::Path is there because of RT#107392 (sigh)
+    # List::Util can be excised after that as well (need to make my own max() routine for older perls)
+
+    installdeps Sub::Name Clone Package::Stash::XS \
+                $( perl -MFile::Spec\ 3.26 -e1 &>/dev/null || echo "File::Path File::Spec" ) \
+                $( perl -MList::Util\ 1.16 -e1 &>/dev/null || echo "List::Util" )
+
+    mkdir -p "$HOME/bin" # this is already in $PATH, just doesn't exist
+    run_or_err "Linking ~/bin/cc to /bin/false - thus essentially BREAKING the C compiler" \
+               "ln -s /bin/false $HOME/bin/cc"
+  fi
 fi
 
 if [[ "$CLEANTEST" = "true" ]]; then
@@ -73,59 +72,46 @@ if [[ "$CLEANTEST" = "true" ]]; then
   # So instead we still use our stock (possibly old) CPAN, and add some
   # handholding
 
-  if [[ "$DEVREL_DEPS" == "true" ]] ; then
-    # Many dists still do not pass tests under tb1.5 properly (and it itself
-    # does not even install on things like 5.10). Install the *stable-dev*
-    # latest T::B here, so that it will not show up as a dependency, and
-    # hence it will not get installed a second time as an unsatisfied dep
-    # under cpanm --dev
-    #
-    # We are also not "quite ready" for SQLA 1.99, do not consider it
-    #
-    installdeps 'Test::Builder~<1.005' 'SQL::Abstract~<1.99'
-
+  if [[ "$DEVREL_DEPS" = "true" ]] ; then
+    # nothing for now
+    /bin/true
   elif ! CPAN_is_sane ; then
     # no configure_requires - we will need the usual suspects anyway
-    # without pre-installing these in one pass things like extract_prereqs won't work
-    installdeps ExtUtils::MakeMaker ExtUtils::CBuilder Module::Build
-
+    # without pre-installing these in one pass things won't yet work
+    installdeps Module::Build
   fi
 
 else
   # we will be running all dbic tests - preinstall lots of stuff, run basic tests
-  # using SQLT and set up whatever databases necessary
-  export DBICTEST_SQLT_DEPLOY=1
-
-  # FIXME - need new TB1.5 devrel
-  # if we run under --dev install latest github of TB1.5 first
-  # (unreleased workaround for precedence warnings)
-  if [[ "$DEVREL_DEPS" == "true" ]] ; then
-    parallel_installdeps_notest git://github.com/nthykier/test-more.git@fix-return-precedence-issue
-  fi
 
   # do the preinstall in several passes to minimize amount of cross-deps installing
   # multiple times, and to avoid module re-architecture breaking another install
   # (e.g. once Carp is upgraded there's no more Carp::Heavy,
   # while a File::Path upgrade may cause a parallel EUMM run to fail)
   #
-  parallel_installdeps_notest ExtUtils::MakeMaker
   parallel_installdeps_notest File::Path
   parallel_installdeps_notest Carp
   parallel_installdeps_notest Module::Build
-  parallel_installdeps_notest File::Spec Data::Dumper Module::Runtime
+  parallel_installdeps_notest File::Spec Module::Runtime
   parallel_installdeps_notest Test::Exception Encode::Locale Test::Fatal
   parallel_installdeps_notest Test::Warn B::Hooks::EndOfScope Test::Differences HTTP::Status
   parallel_installdeps_notest Test::Pod::Coverage Test::EOL Devel::GlobalDestruction Sub::Name MRO::Compat Class::XSAccessor URI::Escape HTML::Entities
-  parallel_installdeps_notest YAML LWP Class::Trigger JSON::XS DateTime::Format::Builder Class::Accessor::Grouped Package::Variant
-  parallel_installdeps_notest 'SQL::Abstract~<1.99' Moose Module::Install JSON SQL::Translator File::Which
+  parallel_installdeps_notest YAML LWP Class::Trigger DateTime::Format::Builder Class::Accessor::Grouped Package::Variant
+  parallel_installdeps_notest SQL::Abstract Moose Module::Install@1.15 JSON SQL::Translator File::Which Class::DBI::Plugin git://github.com/dbsrgits/perl-pperl.git
 
+  # the official version is very much outdated and does not compile on 5.14+
+  # use this rather updated source tree (needs to go to PAUSE):
+  # https://github.com/pilcrow/perl-dbd-interbase
   if [[ -n "$DBICTEST_FIREBIRD_INTERBASE_DSN" ]] ; then
-    # the official version is very much outdated and does not compile on 5.14+
-    # use this rather updated source tree (needs to go to PAUSE):
-    # https://github.com/pilcrow/perl-dbd-interbase
     parallel_installdeps_notest git://github.com/dbsrgits/perl-dbd-interbase.git
   fi
 
+  # SCGI does not install under < 5.8.8 perls nor under parallel make
+  # FIXME: The 5.8.8 thing is likely fixable, something to do with
+  # #define speedy_new(s,n,t) Newx(s,n,t)
+  if perl -M5.008008 -e 1 &>/dev/null ; then
+    MAKEFLAGS="" bash -c "parallel_installdeps_notest git://github.com/dbsrgits/cgi-speedycgi.git"
+  fi
 fi
 
 # generate the makefile which will have different deps depending on
@@ -134,105 +120,38 @@ run_or_err "Configure on current branch" "perl Makefile.PL"
 
 # install (remaining) dependencies, sometimes with a gentle push
 if [[ "$CLEANTEST" = "true" ]]; then
-  # we may need to prepend some stuff to that list
-  HARD_DEPS="$(echo $(make listdeps))"
-
-##### TEMPORARY WORKAROUNDS needed in case we will be using CPAN.pm
-  if [[ "$DEVREL_DEPS" != "true" ]] && ! CPAN_is_sane ; then
-    # combat dzillirium on harness-wide level, otherwise breakage happens weekly
-    echo_err "$(tstamp) Ancient CPAN.pm: engaging TAP::Harness::IgnoreNonessentialDzilAutogeneratedTests during dep install"
-    perl -MTAP::Harness\ 3.18 -e1 &>/dev/null || run_or_err "Upgrading TAP::Harness for HARNESS_SUBCLASS support" "cpan TAP::Harness"
-    export PERL5LIB="$(pwd)/maint/travis-ci_scripts/lib:$PERL5LIB"
-    export HARNESS_SUBCLASS="TAP::Harness::IgnoreNonessentialDzilAutogeneratedTests"
-    # sanity check, T::H does not report sensible errors when the subclass fails to load
-    perl -MTAP::Harness::IgnoreNonessentialDzilAutogeneratedTests -e1
-
-    # DBD::SQLite reasonably wants DBI at config time
-    perl -MDBI -e1 &>/dev/null || HARD_DEPS="DBI $HARD_DEPS"
-
-    # this is a fucked CPAN - won't understand configure_requires of
-    # various pieces we may run into
-    # FIXME - need to get these off metacpan or something instead
-    HARD_DEPS="ExtUtils::Depends B::Hooks::OP::Check $HARD_DEPS"
-
-    # FIXME
-    # parent is temporary due to Carp https://rt.cpan.org/Ticket/Display.html?id=88494
-    HARD_DEPS="parent $HARD_DEPS"
-
-    if CPAN_supports_BUILDPL ; then
-      # We will invoke a posibly MBT based BUILD-file, but we do not support
-      # configure requires. So we not only need to install MBT but its prereqs
-      # FIXME This is madness
-      HARD_DEPS="$(extract_prereqs Module::Build::Tiny) Module::Build::Tiny $HARD_DEPS"
-    else
-      # FIXME
-      # work around Params::Validate not having a Makefile.PL so really old
-      # toolchains can not figure out what the prereqs are ;(
-      # Need to do more research before filing a bug requesting Makefile inclusion
-      HARD_DEPS="$(extract_prereqs Params::Validate) $HARD_DEPS"
-    fi
-  fi
-##### END TEMPORARY WORKAROUNDS
 
-  installdeps $HARD_DEPS
+  # we are doing a devrel pass - try to upgrade *everything* (we will be using cpanm so safe-ish)
+  if [[ "$DEVREL_DEPS" == "true" ]] ; then
+
+    HARD_DEPS="$(make listalldeps | sort -R)"
+
+  else
+
+    HARD_DEPS="$(make listdeps | sort -R)"
+
+##### TEMPORARY WORKAROUNDS needed in case we will be using a fucked CPAN.pm
+    if ! CPAN_is_sane ; then
+
+      # DBD::SQLite reasonably wants DBI at config time
+      perl -MDBI -e1 &>/dev/null || HARD_DEPS="DBI $HARD_DEPS"
 
-### FIXME in case we set it earlier in a workaround
-  if [[ -n "$HARNESS_SUBCLASS" ]] ; then
-
-    INSTALLDEPS_SKIPPED_TESTLIST=$(perl -0777 -e '
-my $curmod_re = qr{
-^
-  (?:
-    \QBuilding and testing\E
-      |
-    [\x20\t]* CPAN\.pm: [^\n]*? (?i:build)\S*
-  )
-
-  [\x20\t]+ (\S+)
-$}mx;
-
-my $curskip_re = qr{^ === \x20 \QSkipping nonessential autogenerated tests: \E([^\n]+) }mx;
-
-my (undef, @chunks) = (split qr/$curmod_re/, <>);
-while (@chunks) {
-  my ($mod, $log) = splice @chunks, 0, 2;
-  print "!!! Skipped nonessential tests while installing $mod:\n\t$1\n"
-    if $log =~ $curskip_re;
-}
-' <<< "$LASTOUT")
-
-    if [[ -n "$INSTALLDEPS_SKIPPED_TESTLIST" ]] ; then
-      POSTMORTEM="$POSTMORTEM$(
-        echo
-        echo "The following non-essential tests were skipped during deps installation"
-        echo "============================================================="
-        echo "$INSTALLDEPS_SKIPPED_TESTLIST"
-        echo "============================================================="
-        echo
-      )"
     fi
 
-    unset HARNESS_SUBCLASS
+##### END TEMPORARY WORKAROUNDS
   fi
 
-else
+  installdeps $HARD_DEPS
 
-  # listalldeps is deliberate - will upgrade everything it can find
-  # we exclude SQLA specifically, since we do not want to pull
-  # in 1.99_xx on bleadcpan runs
-  deplist="$(make listalldeps | grep -vP '^(SQL::Abstract)$')"
+else
 
-  # assume MDV on POISON_ENV, do not touch DBI/SQLite
-  if [[ "$POISON_ENV" = "true" ]] ; then
-    deplist="$(grep -vP '^(DBI|DBD::SQLite)$' <<< "$deplist")"
-  fi
+  parallel_installdeps_notest "$(make listdeps | sort -R)"
 
-  parallel_installdeps_notest "$deplist"
 fi
 
 echo_err "$(tstamp) Dependency installation finished"
-# this will display list of available versions
-perl Makefile.PL
+
+run_or_err "Re-configure" "perl Makefile.PL"
 
 # make sure we got everything we need
 if [[ -n "$(make listdeps)" ]] ; then
@@ -250,30 +169,14 @@ if [[ "$POISON_ENV" = "true" ]] && ( perl -MDBD::SQLite\ 1.38 -e1 || perl -MDBI\
   exit 1
 fi
 
+if [[ "$CLEANTEST" = "true" ]] && perl -MModule::Build::Tiny -e1 &>/dev/null ; then
+  echo_err "Module::Build::Tiny pulled in during the basic depchain install - this must not happen"
+  exit 1
+fi
 
 # announce what are we running
 echo_err "
 ===================== DEPENDENCY CONFIGURATION COMPLETE =====================
 $(tstamp) Configuration phase seems to have taken $(date -ud "@$SECONDS" '+%H:%M:%S') (@$SECONDS)
 
-= CPUinfo
-$(perl -0777 -p -e 's/.+\n\n(?!\z)//s' < /proc/cpuinfo)
-
-= Meminfo
-$(free -m -t)
-
-= Kernel info
-$(uname -a)
-
-= Network Configuration
-$(ip addr)
-
-= Network Sockets Status
-$(sudo netstat -an46p | grep -Pv '\s(CLOSING|(FIN|TIME|CLOSE)_WAIT.?|LAST_ACK)\s')
-
-= Environment
-$(env | grep -P 'TEST|HARNESS|MAKE|TRAVIS|PERL|DBIC' | LC_ALL=C sort | cat -v)
-
-= Perl in use
-$(perl -V)
-============================================================================="
+$(ci_vm_state_text)"
index 8cb9048..25a35ff 100755 (executable)
@@ -1,32 +1,56 @@
 #!/bin/bash
 
+# this file is executed in a subshell - set up the common stuff
 source maint/travis-ci_scripts/common.bash
-if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then return ; fi
+
+if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then exit 0 ; fi
 
 run_harness_tests() {
-  local -x HARNESS_OPTIONS=c:j$NUMTHREADS
+  local -x HARNESS_OPTIONS=c:j$VCPU_USE
+  # if we run under docker (! have_sudo) the logic below won't work
+  # it seems as if ulimit acts globally, across the entire OS
+  # and is thus not served properly by a localised `ps xH`
+  if [[ "$VCPU_USE" == 1 ]] && have_sudo ; then
+    ulim=$(( ( $(ps xH | wc -l) - 3 ) + 4 )) # (real count excluding header + ps + wc) + space for ( make + tee + harness + <actual test> )
+    echo_err "$(tstamp) Setting process/thread limit to $ulim"
+    ulimit -u $ulim
+    sleep 5 # needed to settle things down a bit
+  fi
   make test 2> >(tee "$TEST_STDERR_LOG")
 }
 
+# announce everything we have on this box
+TRAVIS="" perl -Ilib t/00describe_environment.t >/dev/null
+
 TEST_T0=$SECONDS
 if [[ "$CLEANTEST" = "true" ]] ; then
   echo_err "$(tstamp) Running tests with plain \`make test\`"
   run_or_err "Prepare blib" "make pure_all"
   run_harness_tests
 else
-  PROVECMD="prove -lrswj$NUMTHREADS xt t"
+  PROVECMD="prove -lrswj$VCPU_USE xt t"
 
   # FIXME - temporary, until Package::Stash is fixed
   if perl -M5.010 -e 1 &>/dev/null ; then
     PROVECMD="$PROVECMD -T"
   fi
 
+  # List every single SKIP/TODO when they are visible
+  if [[ "$VCPU_USE" == 1 ]] ; then
+    PROVECMD="$PROVECMD --directives"
+  fi
+
   echo_err "$(tstamp) running tests with \`$PROVECMD\`"
   $PROVECMD 2> >(tee "$TEST_STDERR_LOG")
 fi
 TEST_T1=$SECONDS
 
-if [[ -z "$DBICTRACE" ]] && [[ -z "$POISON_ENV" ]] && [[ -s "$TEST_STDERR_LOG" ]] ; then
+if \
+   [[ -z "$DBIC_TRACE" ]] \
+&& [[ -z "$DBIC_MULTICREATE_DEBUG" ]] \
+&& [[ -z "$DBICTEST_DEBUG_CONCURRENCY_LOCKS" ]] \
+&& [[ -z "$DBICTEST_VERSION_WARNS_INDISCRIMINATELY" ]] \
+&& [[ -s "$TEST_STDERR_LOG" ]] ; then
   STDERR_LOG_SIZE=$(wc -l < "$TEST_STDERR_LOG")
 
   # prepend STDERR log
@@ -47,6 +71,6 @@ echo "${POSTMORTEM:- \o/ No notable smoke run issues \o/ }"
 echo
 echo "$(tstamp) Testing took a total of $(( $TEST_T1 - $TEST_T0 ))s"
 if [[ -n "$INSTALLDEPS_OUT" ]] ; then
-  echo "$(tstamp) Full dep install log at $(/usr/bin/nopaste -q -s Shadowcat -d DepInstall <<< "$INSTALLDEPS_OUT")"
+  echo "$(tstamp) Full dep install log at $(/usr/bin/perl /usr/bin/nopaste -q -s Shadowcat -d DepInstall <<< "$INSTALLDEPS_OUT")"
 fi
 echo
index 4935763..ba92421 100755 (executable)
@@ -1,11 +1,11 @@
 #!/bin/bash
 
-# !!! Nothing here will be executed !!!
-# The source-line calling this script is commented out in .travis.yml
-
+# this file is executed in a subshell - set up the common stuff
 source maint/travis-ci_scripts/common.bash
-if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then return ; fi
 
-echo_err "Nothing to do"
+if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then exit 0 ; fi
 
-return 0
+if [[ "$(dmesg)" =~ $( echo "\\bOOM\\b" ) ]] ; then
+  echo_err "=== dmesg ringbuffer"
+  echo_err "$(dmesg)"
+fi
index c8d2bac..9642c3e 100755 (executable)
 #!/bin/bash
 
+# this file is executed in a subshell - set up the common stuff
 source maint/travis-ci_scripts/common.bash
-if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then return ; fi
 
-if [[ "$CLEANTEST" != "true" ]] ; then
-  parallel_installdeps_notest $(perl -Ilib -MDBIx::Class -e 'print join " ", keys %{DBIx::Class::Optional::Dependencies->req_list_for("dist_dir")}')
-  run_or_err "Attempt to build a dist with all prereqs present" "make dist"
+if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] || [[ "$TRAVIS_PULL_REQUEST" != "false" ]] ; then exit 0 ; fi
+
+# this part needs to run in parallel unconditionally
+export VCPU_USE="$VCPU_AVAILABLE"
+export HARNESS_OPTIONS="j$VCPU_USE"
+
+
+if [[ "$DEVREL_DEPS" == "true" ]] && perl -M5.008003 -e1 &>/dev/null ; then
+  # FIXME - Devel::Cover (brought by Test::Strict, but soon needed anyway)
+  # does not test cleanly on 5.8.7 - just get it directly
+  if perl -M5.008007 -e1 &>/dev/null && ! perl -M5.008008 -e1 &>/dev/null; then
+    parallel_installdeps_notest Devel::Cover
+  fi
+
+  # FIXME - workaround for YAML/RT#81120 and L::SRH/RT#107681
+  # We don't actually need these modules, only there because of SQLT (which will be fixed)
+  # does not test cleanly on 5.8.7 - just get them directly
+  if ! perl -M5.008008 -e1 &>/dev/null; then
+    parallel_installdeps_notest YAML Lexical::SealRequireHints
+  fi
+
+  # FIXME Change when Moose goes away
+  installdeps Moose $(perl -Ilib -MDBIx::Class::Optional::Dependencies=-list_missing,dist_dir)
+
+  run_or_err "Attempt to build a dist" "rm -rf inc/ && perl Makefile.PL --skip-author-deps && make dist"
+  tarball_assembled=1
+
+elif [[ "$CLEANTEST" != "true" ]] ; then
+  parallel_installdeps_notest $(perl -Ilib -MDBIx::Class::Optional::Dependencies=-list_missing,dist_dir)
+
+  run_or_err "Attempt to build a dist from original checkout" "make dist"
+  tarball_assembled=1
+fi
+
+
+if [[ -n "$tarball_assembled" ]] ; then
+
   echo "Contents of the resulting dist tarball:"
   echo "==========================================="
   tar -vzxf DBIx-Class-*.tar.gz
   echo "==========================================="
-  run_or_err 'Attempt to configure from re-extracted distdir' \
-    'bash -c "cd \$(find DBIx-Class-* -maxdepth 0 -type d | head -n 1) && perl Makefile.PL"'
+
+  # kill as much as possible with fire
+  purge_sitelib
+
+
+  # undo some of the pollution (if any) affecting the plain install deps
+  # FIXME - this will go away once we move off Moose, and a new SQLT
+  # with much less recommends ships
+  export DBICTEST_SQLT_DEPLOY=""
+  export DBICTEST_VIA_REPLICATED=""
+
+
+  # make sure we are retrying with newest CPAN possible
+  #
+  # not running tests on CPAN.pm - they are not terribly slow,
+  # but https://rt.cpan.org/Ticket/Display.html?id=96437 sucks
+  parallel_installdeps_notest CPAN
+  run_or_err "Make sure CPAN was upgraded to at least 2.10" "perl -M'CPAN 2.010' -e1"
+
+  run_or_err "Re-Configuring CPAN.pm" "perl -MCPAN -e '\
+    CPAN::Config->load;
+
+    # For the time being smoking with this setting is not realistic
+    # https://rt.cpan.org/Ticket/Display.html?id=103280
+    # https://rt.cpan.org/Ticket/Display.html?id=37641
+    # https://rt.cpan.org/Ticket/Display.html?id=77708
+    # https://rt.cpan.org/Ticket/Display.html?id=87474
+    #\$CPAN::Config->{build_requires_install_policy} = q{no};
+
+    \$CPAN::Config->{recommends_policy} = q{yes};
+    CPAN::Config->commit;
+  '"
+
+  cd "$(find DBIx-Class-* -maxdepth 0 -type d | head -n 1)"
+
+  # only run a full test cycle on devrel_deps, as they are all marked
+  # as "allow fails" in the travis matrix
+  if [[ "$DEVREL_DEPS" == "true" ]] ; then
+
+    for e in $( env | grep 'DBICTEST.*DSN' | cut -f 1 -d '=' ) ; do
+      echo "Unsetting $e"
+      export $e=""
+    done
+
+    run_or_err \
+      "Attempt to configure/test/build/install dist using latest CPAN@$(perl -MCPAN -e 'print CPAN->VERSION')" \
+      "cpan ."
+
+  else
+    run_or_err \
+      "Attempt to configure/build/install dist using latest CPAN@$(perl -MCPAN -e 'print CPAN->VERSION')" \
+      "perl -MCPAN -e 'notest( install => q{.} )'"
+  fi
 fi
index 4935763..102291d 100755 (executable)
@@ -1,11 +1,11 @@
 #!/bin/bash
 
 # !!! Nothing here will be executed !!!
-# The source-line calling this script is commented out in .travis.yml
+# The line calling this script is commented out in .travis.yml
 
+# this file is executed in a subshell - set up the common stuff
 source maint/travis-ci_scripts/common.bash
-if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then return ; fi
 
-echo_err "Nothing to do"
+if [[ -n "$SHORT_CIRCUIT_SMOKE" ]] ; then exit 0 ; fi
 
-return 0
+echo_err "Nothing to do"
index 9af6877..f73adde 100755 (executable)
@@ -1,9 +1,10 @@
 #!/bin/bash
 
+# "autodie"
 set -e
 
 TEST_STDERR_LOG=/tmp/dbictest.stderr
-TIMEOUT_CMD="/usr/bin/timeout --kill-after=9.5m --signal=TERM 9m"
+TIMEOUT_CMD="/usr/bin/timeout --kill-after=16m --signal=TERM 15m"
 
 echo_err() { echo "$@" 1>&2 ; }
 
@@ -14,9 +15,45 @@ fi
 
 tstamp() { echo -n "[$(date '+%H:%M:%S')]" ; }
 
+ci_vm_state_text() {
+  echo "
+========================== CI System information ============================
+
+= CPUinfo
+$(perl -0777 -p -e 's/.+\n\n(?!\z)//s' < /proc/cpuinfo)
+
+= Meminfo
+$(free -m -t)
+
+= Diskinfo
+$(df -h)
+
+$(mount | grep '^/')
+
+= Kernel info
+$(uname -a)
+
+= Network Configuration
+$(ip addr)
+
+= Network Sockets Status
+$( (sudo netstat -an46p || netstat -an46p) | grep -Pv '\s(CLOSING|(FIN|TIME|CLOSE)_WAIT.?|LAST_ACK)\s')
+
+= Processlist
+$(ps fuxa)
+
+= Environment
+$(env | grep -P 'TEST|HARNESS|MAKE|TRAVIS|PERL|DBIC|PATH|SHELL' | LC_ALL=C sort | cat -v)
+
+= Perl in use
+$(perl -V)
+============================================================================="
+}
+
 run_or_err() {
   echo_err -n "$(tstamp) $1 ... "
 
+  LASTCMD="$2"
   LASTEXIT=0
   START_TIME=$SECONDS
 
@@ -24,9 +61,7 @@ run_or_err() {
   # the double bash is to hide the job control messages
   bash -c "bash -c 'echo \$\$ >> $PRMETER_PIDFILE; while true; do sleep 10; echo -n \"\${SECONDS}s ... \"; done' &"
 
-  # the tee is a handy debugging tool when stumpage is exceedingly strong
-  #LASTOUT=$( bash -c "$2" 2>&1 | tee /dev/stderr) || LASTEXIT=$?
-  LASTOUT=$( bash -c "$2" 2>&1 ) || LASTEXIT=$?
+  LASTOUT=$( eval "$2" 2>&1 ) || LASTEXIT=$?
 
   # stop progress meter
   for p in $(cat "$PRMETER_PIDFILE"); do kill $p ; done
@@ -34,11 +69,17 @@ run_or_err() {
   DELTA_TIME=$(( $SECONDS - $START_TIME ))
 
   if [[ "$LASTEXIT" != "0" ]] ; then
-    echo_err "FAILED !!! (after ${DELTA_TIME}s)"
-    echo_err "Command executed:"
-    echo_err "$2"
-    echo_err "STDOUT+STDERR:"
-    echo_err "$LASTOUT"
+    if [[ -z "$3" ]] ; then
+      echo_err "FAILED !!! (after ${DELTA_TIME}s)"
+      echo_err "Command executed:"
+      echo_err "$LASTCMD"
+      echo_err "STDOUT+STDERR:"
+      echo_err "$LASTOUT"
+      if [[ "$(dmesg)" =~ $( echo "\\bOOM\\b" ) ]] ; then
+        echo_err "=== dmesg ringbuffer"
+        echo_err "$(dmesg)"
+      fi
+    fi
 
     return $LASTEXIT
   else
@@ -50,9 +91,6 @@ apt_install() {
   # flatten
   pkgs="$@"
 
-  # Need to do this at every step, the sources list may very well have changed
-  run_or_err "Updating APT available package list" "sudo apt-get update"
-
   run_or_err "Installing Debian APT packages: $pkgs" "sudo apt-get install --allow-unauthenticated  --no-install-recommends -y $pkgs"
 }
 
@@ -98,7 +136,7 @@ parallel_installdeps_notest() {
   if [[ -z "$@" ]] ; then return; fi
 
   # one module spec per line
-  MODLIST="$(printf '%s\n' "$@")"
+  MODLIST="$(printf '%s\n' "$@" | sort -R)"
 
   # We want to trap the output of each process and serially append them to
   # each other as opposed to just dumping a jumbled up mass-log that would
@@ -121,55 +159,34 @@ parallel_installdeps_notest() {
   run_or_err "Installing (without testing) $(echo $MODLIST)" \
     "echo \\
 \"$MODLIST\" \\
-      | xargs -d '\\n' -n 1 -P $NUMTHREADS bash -c \\
-        'OUT=\$($TIMEOUT_CMD cpanm --notest \"\$@\" 2>&1 ) || (LASTEXIT=\$?; echo \"\$OUT\"; exit \$LASTEXIT)' \\
+      | xargs -d '\\n' -n 1 -P $VCPU_USE bash -c \\
+        'OUT=\$(maint/getstatus $TIMEOUT_CMD cpanm --notest \"\$@\" 2>&1 ) || (LASTEXIT=\$?; echo \"\$OUT\"; exit \$LASTEXIT)' \\
         'giant space monkey penises'
     "
 }
 
+export -f parallel_installdeps_notest run_or_err echo_err tstamp
+
 installdeps() {
   if [[ -z "$@" ]] ; then return; fi
 
-  echo_err "$(tstamp) Processing dependencies: $@"
+  MODLIST=$(printf "%q " "$@" | perl -pe 's/^\s+|\s+$//g')
 
   local -x HARNESS_OPTIONS
 
-  HARNESS_OPTIONS="j$NUMTHREADS"
-
-  echo_err -n "Attempting install of $# modules under parallel ($HARNESS_OPTIONS) testing ... "
+  HARNESS_OPTIONS="j$VCPU_USE"
 
-  LASTEXIT=0
-  START_TIME=$SECONDS
-  LASTOUT=$( _dep_inst_with_test "$@" ) || LASTEXIT=$?
-  DELTA_TIME=$(( $SECONDS - $START_TIME ))
+  if ! run_or_err "Attempting install of $# modules under parallel ($HARNESS_OPTIONS) testing ($MODLIST)" "_dep_inst_with_test $MODLIST" quiet_fail ; then
+    local errlog="failed after ${DELTA_TIME}s Exit:$LASTEXIT Log:$(/usr/bin/perl /usr/bin/nopaste -q -s Shadowcat -d "Parallel testfail" <<< "$LASTOUT")"
+    echo "$errlog"
 
-  if [[ "$LASTEXIT" = "0" ]] ; then
-    echo_err "done (took ${DELTA_TIME}s)"
-  else
-    local errlog="after ${DELTA_TIME}s Exit:$LASTEXIT Log:$(/usr/bin/nopaste -q -s Shadowcat -d "Parallel testfail" <<< "$LASTOUT")"
-    echo_err -n "failed ($errlog) retrying with sequential testing ... "
     POSTMORTEM="$POSTMORTEM$(
       echo
-      echo "Depinstall under $HARNESS_OPTIONS parallel testing failed $errlog"
-      echo "============================================================="
-      echo "Attempted installation of: $@"
-      echo "============================================================="
+      echo "Depinstall of $MODLIST under $HARNESS_OPTIONS parallel testing $errlog"
     )"
 
     HARNESS_OPTIONS=""
-    LASTEXIT=0
-    START_TIME=$SECONDS
-    LASTOUT=$( _dep_inst_with_test "$@" ) || LASTEXIT=$?
-    DELTA_TIME=$(( $SECONDS - $START_TIME ))
-
-    if [[ "$LASTEXIT" = "0" ]] ; then
-      echo_err "done (took ${DELTA_TIME}s)"
-    else
-      echo_err "FAILED !!! (after ${DELTA_TIME}s)"
-      echo_err "STDOUT+STDERR:"
-      echo_err "$LASTOUT"
-      exit 1
-    fi
+    run_or_err "Retrying same $# modules without parallel testing" "_dep_inst_with_test $MODLIST"
   fi
 
   INSTALLDEPS_OUT="${INSTALLDEPS_OUT}${LASTOUT}"
@@ -178,23 +195,61 @@ installdeps() {
 _dep_inst_with_test() {
   if [[ "$DEVREL_DEPS" == "true" ]] ; then
     # --dev is already part of CPANM_OPT
-    $TIMEOUT_CMD cpanm "$@" 2>&1
+    LASTCMD="$TIMEOUT_CMD cpanm $@"
+    $LASTCMD 2>&1 || return 1
   else
-    $TIMEOUT_CMD cpan "$@" 2>&1
+    LASTCMD="$TIMEOUT_CMD cpan $@"
+    $LASTCMD 2>&1 || return 1
 
     # older perls do not have a CPAN which can exit with error on failed install
     for m in "$@"; do
       if ! perl -e '
 
+$ARGV[0] =~ s/-TRIAL\.//;
+
 my $mod = (
-  $ARGV[0] =~ m{ \/ .*? ([^\/]+) $ }x
+  # abuse backtrack
+  $ARGV[0] =~ m{ / .*? ( [^/]+ ) $ }x
     ? do { my @p = split (/\-/, $1); pop @p; join "::", @p }
     : $ARGV[0]
 );
 
-$mod = q{List::Util} if $mod eq q{Scalar::List::Utils};
+# map some install-names to a module/version combo
+# serves both as a grandfathered title-less tarball, and
+# as a minimum version check for upgraded core modules
+my $eval_map = {
+
+  # this is temporary, will need something more robust down the road
+  # (perhaps by then Module::CoreList will be dep-free)
+  "Module::Build" => { ver => "0.4214" },
+  "podlators" => { mod => "Pod::Man", ver => "2.17" },
+
+  "File::Spec" => { ver => "3.47" },
+  "Cwd" => { ver => "3.47" },
+
+  "List::Util" => { ver => "1.42" },
+  "Scalar::Util" => { ver => "1.42" },
+  "Scalar::List::Utils" => { mod => "List::Util", ver => "1.42" },
+};
+
+my $m = $eval_map->{$mod}{mod} || $mod;
+
+eval(
+  "require $m"
 
-eval qq{require($mod)} or ( print $@ and exit 1)
+  .
+
+  ($eval_map->{$mod}{ver}
+    ? "; $m->VERSION(\$eval_map->{\$mod}{ver}) "
+    : ""
+  )
+
+  .
+
+  "; 1"
+)
+  or
+( print $@ and exit 1)
 
       ' "$m" 2> /dev/null ; then
         echo -e "$m installation seems to have failed"
@@ -204,6 +259,72 @@ eval qq{require($mod)} or ( print $@ and exit 1)
   fi
 }
 
+# Idea stolen from
+# https://github.com/kentfredric/Dist-Zilla-Plugin-Prereqs-MatchInstalled-All/blob/master/maint-travis-ci/sterilize_env.pl
+# Only works on 5.12+ (where sitelib was finally properly fixed)
+purge_sitelib() {
+  echo_err "$(tstamp) Sterilizing the Perl installation (cleaning up sitelib)"
+
+  if perl -M5.012 -e1 &>/dev/null ; then
+
+    perl -M5.012 -MConfig -MFile::Find -e '
+      my $sitedirs = {
+        map { $Config{$_} => 1 }
+          grep { $_ =~ /site(lib|arch)exp$/ }
+            keys %Config
+      };
+      find({ bydepth => 1, no_chdir => 1, follow_fast => 1, wanted => sub {
+        ! $sitedirs->{$_} and ( -d _ ? rmdir : unlink )
+      } }, keys %$sitedirs )
+    '
+  else
+
+    cl_fn="/tmp/${TRAVIS_BUILD_ID}_Module_CoreList.pm";
+
+    [[ -s "$cl_fn" ]] || run_or_err \
+      "Downloading latest Module::CoreList" \
+      "curl -s --compress -o '$cl_fn' https://api.metacpan.org/source/Module::CoreList"
+
+    perl -0777 -Ilib -MDBIx::Class::Optional::Dependencies -e '
+
+      # this is horrible, but really all we want is "has this ever been used"
+      # so a grep without a load is quite legit (and horrible)
+      my $mcl_source = <>;
+
+      my @all_possible_never_been_core_modpaths = map
+        { (my $mp = $_ . ".pm" ) =~ s|::|/|g; $mp }
+        grep
+          { $mcl_source !~ / ^ \s+ \x27 $_ \x27 \s* \=\> /mx }
+          (
+            qw(
+              Module::Build::Tiny
+            ),
+            keys %{ DBIx::Class::Optional::Dependencies->modreq_list_for([
+              keys %{ DBIx::Class::Optional::Dependencies->req_group_list }
+            ])}
+          )
+      ;
+
+      # now that we have the list we can go ahead and destroy every single one
+      # of these modules without being concerned about breaking the base ability
+      # to install things
+      for my $mp ( sort { lc($a) cmp lc($b) } @all_possible_never_been_core_modpaths ) {
+        for my $incdir (@INC) {
+          -e "$incdir/$mp"
+            and
+          unlink "$incdir/$mp"
+            and
+          print "Nuking $incdir/$mp\n"
+        }
+      }
+    ' "$cl_fn"
+
+  fi
+}
+
+
 CPAN_is_sane() { perl -MCPAN\ 1.94_56 -e 1 &>/dev/null ; }
 
 CPAN_supports_BUILDPL() { perl -MCPAN\ 1.9205 -e1 &>/dev/null; }
+
+have_sudo() { sudo /bin/true &>/dev/null ; }
diff --git a/maint/travis-ci_scripts/configs/minimal_mysql_travis.cnf b/maint/travis-ci_scripts/configs/minimal_mysql_travis.cnf
new file mode 100644 (file)
index 0000000..e7ac7a6
--- /dev/null
@@ -0,0 +1,47 @@
+[mysqld]
+
+# the DBIC test suite does hold up to 3 concurrent connections
+# (t/94versioning.t), otherwise this could conceivably go lower (~50MB vsize
+# per connection)
+max_connections = 3
+
+thread_cache_size = 0
+thread_stack = 128K
+
+# mysql >= 5.5.16
+#thread_pool_size = 1
+
+net_buffer_length = 4K
+read_buffer_size = 32K
+join_buffer_size = 128K
+sort_buffer_size = 128K
+bulk_insert_buffer_size = 0
+
+table_definition_cache = 256
+performance_schema = 0
+
+query_cache_type = 0
+query_cache_size = 0
+query_cache_limit = 16K
+
+myisam_sort_buffer_size = 16K
+tmp_table_size = 1M
+key_buffer_size = 64K
+
+innodb_data_file_path           = ibdata1:10M:autoextend
+innodb_autoextend_increment     = 1
+innodb_buffer_pool_size         = 512K
+
+innodb_stats_on_metadata        = 0
+innodb_file_per_table           = 0
+
+innodb_log_file_size            = 1M
+innodb_log_buffer_size          = 512K
+innodb_buffer_pool_size         = 512K
+
+innodb_use_sys_malloc           = 0
+innodb_additional_mem_pool_size = 256K
+innodb_flush_method             = O_DIRECT
+
+innodb_read_io_threads          = 1
+innodb_write_io_threads         = 1
diff --git a/maint/travis-ci_scripts/lib/TAP/Harness/IgnoreNonessentialDzilAutogeneratedTests.pm b/maint/travis-ci_scripts/lib/TAP/Harness/IgnoreNonessentialDzilAutogeneratedTests.pm
deleted file mode 100644 (file)
index 7f01716..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-package TAP::Harness::IgnoreNonessentialDzilAutogeneratedTests;
-
-use warnings;
-use strict;
-
-use base 'TAP::Harness';
-use File::Spec ();
-use IPC::Open3 'open3';
-use File::Temp ();
-use List::Util 'first';
-
-my $frivolous_test_map = {
-# Test based on the extremely dep-heavy, *prone to failures* Test::CheckDeps
-#
-  qr|^t/00-check-deps.t$| => [
-    qr|^\Q# this test was generated with Dist::Zilla::Plugin::Test::CheckDeps|m,
-
-    # older non-annotated versions
-    qr|use \s+ Test::CheckDeps .*? ^\Qcheck_dependencies('suggests')\E .*? \QBAIL_OUT("Missing dependencies") if !Test::More->builder->is_passing|smx,
-  ],
-
-# "does everything compile" tests are useless by definition - this is what the
-# rest of the test suite is for
-#
-  qr|^t/00-compile.t$| => [
-    qr|^\Q# this test was generated with Dist::Zilla::Plugin::Test::Compile|m,
-  ],
-
-# The report prereq test managed to become fatal as well
-#
-  qr|^t/00-report-prereqs.t$| => [
-    qr|^\Q# This test was generated by Dist::Zilla::Plugin::Test::ReportPrereqs|m,
-  ],
-
-# Just future-proof the thing, catch anything autogened by dzil for a bit
-  qr|^t/00-| => [
-    qr|^\Q# This test was generated by Dist::Zilla::|m,
-  ]
-};
-
-sub aggregate_tests {
-  my ($self, $aggregate, @all_tests) = @_;
-
-  my ($run_tests, $skip_tests);
-
-  TESTFILE:
-  for (@all_tests) {
-    my $fn = File::Spec::Unix->catpath( File::Spec->splitpath( $_ ) );
-
-    if (my $REs = $frivolous_test_map->{
-      (first { $fn =~ $_ } keys %$frivolous_test_map ) || ''
-    }) {
-      my $slurptest = do { local (@ARGV, $/) = $fn; <> };
-      $slurptest =~ $_ and push @$skip_tests, $fn and next TESTFILE for @$REs;
-    }
-
-    push @$run_tests, $fn;
-  }
-
-  if ($skip_tests) {
-
-    for my $tfn (@$skip_tests) {
-
-      (my $tfn_flattened = $tfn) =~ s|/|_|g;
-
-      my $log_file = File::Temp->new(
-        DIR => '/tmp',
-        TEMPLATE => "AutoGenTest_${tfn_flattened}_XXXXX",
-        SUFFIX => '.txt',
-      );
-
-      # FIXME I have no idea why the fileno dance is necessary - will investigate later
-      # All I know is that if I pass in just $log_file - open3 ignores it >:(
-      my $pid = open3(undef, '>&'.fileno($log_file), undef, $^X, qw(-I blib -I arch/lib), $tfn );
-      waitpid ($pid, 0);
-      my $ex = $?;
-
-      if ($ex) {
-        # use qx as opposed to another open3 until I figure out the above
-        close $log_file or die "Unable to close $log_file: $!";
-        chomp( my $url = `/usr/bin/nopaste -q -s Shadowcat -d $log_file < $log_file` );
-
-        $tfn .= "[would NOT have passed: $ex / $url]";
-      }
-    }
-
-    print STDERR "=== Skipping nonessential autogenerated tests: @$skip_tests\n";
-  }
-
-  return $self->SUPER::aggregate_tests($aggregate, @$run_tests);
-}
-
-1;
index e6066fb..bdd618c 100755 (executable)
@@ -4,11 +4,10 @@ use strict;
 use warnings;
 
 BEGIN {
-  use DBIx::Class;
-  die (  'The following modules are required for the dbicadmin utility: '
-       . DBIx::Class::Optional::Dependencies->req_missing_for ('admin_script')
-       . "\n"
-  ) unless DBIx::Class::Optional::Dependencies->req_ok_for ('admin_script');
+  require DBIx::Class::Optional::Dependencies;
+  if (my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('admin_script') ) {
+    die "The following modules are required for the dbicadmin utility: $missing\n";
+  }
 }
 
 use DBIx::Class::Admin::Descriptive;
diff --git a/t/00describe_environment.t b/t/00describe_environment.t
new file mode 100644 (file)
index 0000000..35e6b02
--- /dev/null
@@ -0,0 +1,660 @@
+###
+### This version is rather 5.8-centric, because DBIC itself is 5.8
+### It certainly can be rewritten to degrade well on 5.6
+###
+
+# Very important to grab the snapshot early, as we will be reporting
+# the INC indices from the POV of whoever ran the script, *NOT* from
+# the POV of the internals
+my @initial_INC;
+BEGIN {
+  @initial_INC = @INC;
+}
+
+BEGIN {
+  unshift @INC, 't/lib';
+
+  if ( "$]" < 5.010) {
+
+    # Pre-5.10 perls pollute %INC on unsuccesfull module
+    # require, making it appear as if the module is already
+    # loaded on subsequent require()s
+    # Can't seem to find the exact RT/perldelta entry
+    #
+    # The reason we can't just use a sane, clean loader, is because
+    # if a Module require()s another module the %INC will still
+    # get filled with crap and we are back to square one. A global
+    # fix is really the only way for this test, as we try to load
+    # each available module separately, and have no control (nor
+    # knowledge) over their common dependencies.
+    #
+    # we want to do this here, in the very beginning, before even
+    # warnings/strict are loaded
+
+    require DBICTest::Util::OverrideRequire;
+
+    DBICTest::Util::OverrideRequire::override_global_require( sub {
+      my $res = eval { $_[0]->() };
+      if ($@ ne '') {
+        delete $INC{$_[1]};
+        die $@;
+      }
+      return $res;
+    } );
+  }
+}
+
+use strict;
+use warnings;
+
+use Test::More 'no_plan';
+use Config;
+use File::Find 'find';
+use Digest::MD5 ();
+use Cwd 'abs_path';
+use File::Spec;
+use List::Util 'max';
+use ExtUtils::MakeMaker;
+
+use DBICTest::RunMode;
+use DBICTest::Util 'visit_namespaces';
+use DBIx::Class::Optional::Dependencies;
+
+my $known_paths = {
+  SA => {
+    config_key => 'sitearch',
+  },
+  SL => {
+    config_key => 'sitelib',
+  },
+  SS => {
+    config_key => 'sitelib_stem',
+    match_order => 1,
+  },
+  SP => {
+    config_key => 'siteprefix',
+    match_order => 2,
+  },
+  VA => {
+    config_key => 'vendorarch',
+  },
+  VL => {
+    config_key => 'vendorlib',
+  },
+  VS => {
+    config_key => 'vendorlib_stem',
+    match_order => 3,
+  },
+  VP => {
+    config_key => 'vendorprefix',
+    match_order => 4,
+  },
+  PA => {
+    config_key => 'archlib',
+  },
+  PL => {
+    config_key => 'privlib',
+  },
+  PP => {
+    config_key => 'prefix',
+    match_order => 5,
+  },
+  BLA => {
+    rel_path => './blib/arch',
+    skip_unversioned_modules => 1,
+  },
+  BLL => {
+    rel_path => './blib/lib',
+    skip_unversioned_modules => 1,
+  },
+  INC => {
+    rel_path => './inc',
+  },
+  LIB => {
+    rel_path => './lib',
+    skip_unversioned_modules => 1,
+  },
+  T => {
+    rel_path => './t',
+    skip_unversioned_modules => 1,
+  },
+  XT => {
+    rel_path => './xt',
+    skip_unversioned_modules => 1,
+  },
+  CWD => {
+    rel_path => '.',
+  },
+  HOME => {
+    rel_path => '~',
+    abs_unix_path => abs_unix_path (
+      eval { require File::HomeDir and File::HomeDir->my_home }
+        ||
+      $ENV{USERPROFILE}
+        ||
+      $ENV{HOME}
+        ||
+      glob('~')
+    ),
+  },
+};
+
+for my $k (keys %$known_paths) {
+  my $v = $known_paths->{$k};
+
+  # never use home as a found-in-dir marker - it is too broad
+  # HOME is only used by the shortener
+  $v->{marker} = $k unless $k eq 'HOME';
+
+  unless ( $v->{abs_unix_path} ) {
+    if ( $v->{rel_path} ) {
+      $v->{abs_unix_path} = abs_unix_path( $v->{rel_path} );
+    }
+    elsif ( $Config{ $v->{config_key} || '' } ) {
+      $v->{abs_unix_path} = abs_unix_path (
+        $Config{"$v->{config_key}exp"} || $Config{$v->{config_key}}
+      );
+    }
+  }
+
+  delete $known_paths->{$k} unless $v->{abs_unix_path} and -d $v->{abs_unix_path};
+}
+my $seen_markers = {};
+
+# first run through lib/ and *try* to load anything we can find
+# within our own project
+find({
+  wanted => sub {
+    -f $_ or return;
+
+    # can't just `require $fn`, as we need %INC to be
+    # populated properly
+    my ($mod) = $_ =~ /^ lib [\/\\] (.+) \.pm $/x
+      or return;
+
+    try_module_require(join ('::', File::Spec->splitdir($mod)) )
+  },
+  no_chdir => 1,
+}, 'lib' );
+
+
+
+# now run through OptDeps and attempt loading everything else
+#
+# some things needs to be sorted before other things
+# positive - load first
+# negative - load last
+my $load_weights = {
+  # Make sure oracle is tried last - some clients (e.g. 10.2) have symbol
+  # clashes with libssl, and will segfault everything coming after them
+  "DBD::Oracle" => -999,
+};
+
+my @known_modules = sort
+  { ($load_weights->{$b}||0) <=> ($load_weights->{$a}||0) }
+  keys %{
+    DBIx::Class::Optional::Dependencies->req_list_for([
+      grep
+        # some DBDs are notoriously problematic to load
+        # hence only show stuff based on test_rdbms which will
+        # take into account necessary ENVs
+        { $_ !~ /^ (?: rdbms | dist )_ /x }
+        keys %{DBIx::Class::Optional::Dependencies->req_group_list}
+    ])
+  }
+;
+
+try_module_require($_) for @known_modules;
+
+my $has_versionpm = eval { require version };
+
+
+# At this point we've loaded everything we ever could, but some modules
+# (understandably) crapped out. For an even more thorough report, note
+# everthing present in @INC we excplicitly know about (via OptDeps)
+# *even though* it didn't load
+my $known_failed_loads;
+
+for my $mod (@known_modules) {
+  my $inc_key = module_notional_filename($mod);
+  next if defined $INC{$inc_key};
+
+  if (defined( my $idx = module_found_at_inc_index( $mod, \@INC ) ) ) {
+    $known_failed_loads->{$mod} = abs_unix_path( "$INC[$idx]/$inc_key" );
+  }
+
+}
+
+my $perl = 'perl';
+
+# This is a cool idea, but the line is too long even with shortening :(
+#
+#for my $i ( 1 .. $Config{config_argc} ) {
+#  my $conf_arg = $Config{"config_arg$i"};
+#  $conf_arg =~ s!
+#    \= (.+)
+#  !
+#    '=' . shorten_fn($1)
+#  !ex;
+#
+#  $perl .= " $conf_arg";
+#}
+
+my $interesting_modules = {
+  # pseudo module
+  $perl => {
+    version => $],
+    abs_unix_path => abs_unix_path($^X),
+  }
+};
+
+
+# drill through the *ENTIRE* symtable and build a map of interesting modules
+visit_namespaces( action => sub {
+  no strict 'refs';
+  my $pkg = shift;
+
+  # keep going, but nothing to see here
+  return 1 if $pkg eq 'main';
+
+  # private - not interested, including no further descent
+  return 0 if $pkg =~ / (?: ^ | :: ) _ /x;
+
+  my $inc_key = module_notional_filename($pkg);
+
+  my $abs_unix_path = (
+    $INC{$inc_key}
+      and
+    -f $INC{$inc_key}
+      and
+    -r $INC{$inc_key}
+      and
+    abs_unix_path($INC{$inc_key})
+  );
+
+  # handle versions first (not interested in synthetic classes)
+  if (
+    defined ${"${pkg}::VERSION"}
+      and
+    ${"${pkg}::VERSION"} !~ /\Qset by base.pm/
+  ) {
+
+    # make sure a version can be extracted, be noisy when it doesn't work
+    # do this even if we are throwing away the result below in lieu of EUMM
+    my $mod_ver = eval { $pkg->VERSION };
+
+    if (my $err = $@) {
+      $err =~ s/^/  /mg;
+      say_err (
+        "Calling `$pkg->VERSION` resulted in an exception, which should never "
+      . "happen - please file a bug with the distribution containing $pkg. "
+      . "Complete exception text below:\n\n$err"
+      );
+    }
+    elsif( ! defined $mod_ver or ! length $mod_ver ) {
+      my $ret = defined $mod_ver
+        ? "the empty string ''"
+        : "'undef'"
+      ;
+
+      say_err (
+        "Calling `$pkg->VERSION` returned $ret, even though \$${pkg}::VERSION "
+      . "is defined, which should never happen - please file a bug with the "
+      . "distribution containing $pkg."
+      );
+
+      undef $mod_ver;
+    }
+
+    if (
+      $abs_unix_path
+        and
+      defined ( my $eumm_ver = eval { MM->parse_version( $abs_unix_path ) } )
+    ) {
+
+      # can only run the check reliably if v.pm is there
+      if (
+        $has_versionpm
+          and
+        defined $mod_ver
+          and
+        $eumm_ver ne $mod_ver
+          and
+        (
+          ( eval { version->parse( do { (my $v = $eumm_ver) =~ s/_//g; $v } ) } || 0 )
+            !=
+          ( eval { version->parse( do { (my $v = $mod_ver) =~ s/_//g; $v } ) } || 0 )
+        )
+      ) {
+        say_err (
+          "Mismatch of versions '$mod_ver' and '$eumm_ver', obtained respectively "
+        . "via `$pkg->VERSION` and parsing the version out of @{[ shorten_fn( $abs_unix_path ) ]} "
+        . "with ExtUtils::MakeMaker\@@{[ ExtUtils::MakeMaker->VERSION ]}. "
+        . "This should never happen - please check whether this is still present "
+        . "in the latest version, and then file a bug with the distribution "
+        . "containing $pkg."
+        );
+      }
+
+      $interesting_modules->{$pkg}{version} = $eumm_ver;
+    }
+    elsif( defined $mod_ver ) {
+
+      $interesting_modules->{$pkg}{version} = $mod_ver;
+    }
+  }
+  elsif ( $known_failed_loads->{$pkg} ) {
+    $abs_unix_path = $known_failed_loads->{$pkg};
+    $interesting_modules->{$pkg}{version} = '!! LOAD FAIL !!';
+  }
+
+  if ($abs_unix_path) {
+    my ($marker, $initial_inc_idx);
+
+    my $current_inc_idx = module_found_at_inc_index($pkg, \@INC);
+    my $p = subpath_of_known_path( $abs_unix_path );
+
+    if (
+      defined $current_inc_idx
+        and
+      $p->{marker}
+        and
+      abs_unix_path($INC[$current_inc_idx]) eq $p->{abs_unix_path}
+    ) {
+      $marker = $p->{marker};
+    }
+    elsif (defined ( $initial_inc_idx = module_found_at_inc_index($pkg, \@initial_INC) ) ) {
+      $marker = "\$INC[$initial_inc_idx]";
+    }
+
+    # we are only interested if there was a declared version already above
+    # OR if the module came from somewhere other than skip_unversioned_modules
+    if (
+      $marker
+        and
+      (
+        $interesting_modules->{$pkg}
+          or
+        !$p->{skip_unversioned_modules}
+      )
+    ) {
+      $interesting_modules->{$pkg}{source_marker} = $marker;
+      $seen_markers->{$marker} = 1;
+    }
+
+    # at this point only fill in the path (md5 calc) IFF it is interesting
+    # in any respect
+    $interesting_modules->{$pkg}{abs_unix_path} = $abs_unix_path
+      if $interesting_modules->{$pkg};
+  }
+
+  1;
+});
+
+# compress identical versions sourced from ./blib, ./lib, ./t and ./xt
+# as close to the root of a namespace as we can
+purge_identically_versioned_submodules_with_markers([ map {
+  ( $_->{skip_unversioned_modules} && $_->{marker} ) || ()
+} values %$known_paths ]);
+
+ok 1, (scalar keys %$interesting_modules) . " distinctly versioned modules found";
+
+# do not announce anything under ci - we are watching for STDERR silence
+exit 0 if DBICTest::RunMode->is_ci;
+
+
+# diag the result out
+my $max_ver_len = max map
+  { length "$_" }
+  ( 'xxx.yyyzzz_bbb', map { $_->{version} || '' } values %$interesting_modules )
+;
+my $max_marker_len = max map { length $_ } ( '$INC[999]', keys %$seen_markers );
+
+my $discl = <<'EOD';
+
+List of loadable modules within both the core and *OPTIONAL* dependency chains
+present on this system (modules sourced from ./blib, ./lib, ./t, and ./xt
+with versions identical to their parent namespace were omitted for brevity)
+
+    *** Note that *MANY* of these modules will *NEVER* be loaded ***
+            *** during normal operation of DBIx::Class ***
+EOD
+
+# pre-assemble everything and print it in one shot
+# makes it less likely for parallel test execution to insert bogus lines
+my $final_out = "\n$discl\n";
+
+$final_out .= "\@INC at startup (does not reflect manipulation at runtime):\n";
+
+my $in_inc_skip;
+for (0.. $#initial_INC) {
+
+  my $shortname = shorten_fn( $initial_INC[$_] );
+
+  # when *to* print a line of INC
+  if (
+    ! $ENV{AUTOMATED_TESTING}
+      or
+    @initial_INC < 11
+      or
+    $seen_markers->{"\$INC[$_]"}
+      or
+    ! -e $shortname
+      or
+    ! File::Spec->file_name_is_absolute($shortname)
+  ) {
+    $in_inc_skip = 0;
+    $final_out .= sprintf ( "% 3s: %s\n",
+      $_,
+      $shortname
+    );
+  }
+  elsif(! $in_inc_skip++) {
+    $final_out .= "  ...\n";
+  }
+}
+
+$final_out .= "\n";
+
+if (my @seen_known_paths = grep { $known_paths->{$_} } keys %$seen_markers) {
+
+  $final_out .= join "\n", 'Sourcing markers:', (map
+    {
+      sprintf "%*s: %s",
+        $max_marker_len => $_->{marker},
+        ($_->{config_key} ? "\$Config{$_->{config_key}}" : "$_->{rel_path}/" )
+    }
+    sort
+      {
+        !!$b->{config_key} cmp !!$a->{config_key}
+          or
+        ( $a->{marker}||'') cmp ($b->{marker}||'')
+      }
+      @{$known_paths}{@seen_known_paths}
+  ), '', '';
+
+}
+
+$final_out .= "=============================\n";
+
+$final_out .= join "\n", (map
+  { sprintf (
+    "%*s  %*s  %*s%s",
+    $max_marker_len => $interesting_modules->{$_}{source_marker} || '',
+    $max_ver_len => ( defined $interesting_modules->{$_}{version}
+      ? $interesting_modules->{$_}{version}
+      : ''
+    ),
+    -78 => $_,
+    ($interesting_modules->{$_}{abs_unix_path}
+      ? "  [ MD5: @{[ get_md5( $interesting_modules->{$_}{abs_unix_path} ) ]} ]"
+      : "! -f \$INC{'@{[ module_notional_filename($_) ]}'}"
+    ),
+  ) }
+  sort { lc($a) cmp lc($b) } keys %$interesting_modules
+), '';
+
+$final_out .= "=============================\n$discl\n\n";
+
+diag $final_out;
+
+exit 0;
+
+
+
+sub say_err { print STDERR "\n", @_, "\n\n" };
+
+# do !!!NOT!!! use Module::Runtime's require_module - it breaks CORE::require
+sub try_module_require {
+  # trap deprecation warnings and whatnot
+  local $SIG{__WARN__} = sub {};
+  local $@;
+  eval "require $_[0]";
+}
+
+sub abs_unix_path {
+  return '' unless (
+    defined $_[0]
+      and
+    ( -e $_[0] or File::Spec->file_name_is_absolute($_[0]) )
+  );
+
+  # File::Spec's rel2abs does not resolve symlinks
+  # we *need* to look at the filesystem to be sure
+  my $abs_fn = abs_path($_[0]);
+
+  if ( $^O eq 'MSWin32' and $abs_fn ) {
+
+    # sometimes we can get a short/longname mix, normalize everything to longnames
+    $abs_fn = Win32::GetLongPathName($abs_fn);
+
+    # Fixup (native) slashes in Config not matching (unixy) slashes in INC
+    $abs_fn =~ s|\\|/|g;
+  }
+
+  $abs_fn;
+}
+
+sub shorten_fn {
+  my $fn = shift;
+
+  my $abs_fn = abs_unix_path($fn);
+
+  if (my $p = subpath_of_known_path( $fn ) ) {
+    $abs_fn =~ s| (?<! / ) $|/|x
+      if -d $abs_fn;
+
+    if ($p->{rel_path}) {
+      $abs_fn =~ s!\Q$p->{abs_unix_path}!$p->{rel_path}!
+        and return $abs_fn;
+    }
+    elsif ($p->{config_key}) {
+      $abs_fn =~ s!\Q$p->{abs_unix_path}!<<$p->{marker}>>!
+        and
+      $seen_markers->{$p->{marker}} = 1
+        and
+      return $abs_fn;
+    }
+  }
+
+  # we got so far - not a known path
+  # return the unixified version it if was absolute, leave as-is otherwise
+  my $rv = ( $abs_fn and File::Spec->file_name_is_absolute( $fn ) )
+    ? $abs_fn
+    : $fn
+  ;
+
+  $rv = "( ! -e ) $rv" unless -e $rv;
+
+  return $rv;
+}
+
+sub subpath_of_known_path {
+  my $abs_fn = abs_unix_path( $_[0] )
+    or return '';
+
+  for my $p (
+    sort {
+      length( $b->{abs_unix_path} ) <=> length( $a->{abs_unix_path} )
+        or
+      ( $a->{match_order} || 0 ) <=> ( $b->{match_order} || 0 )
+    }
+    values %$known_paths
+  ) {
+    # run through the matcher twice - first always append a /
+    # then try without
+    # important to avoid false positives
+    for my $suff ( '/', '' ) {
+      return { %$p } if 0 == index( $abs_fn, "$p->{abs_unix_path}$suff" );
+    }
+  }
+}
+
+sub module_found_at_inc_index {
+  my ($mod, $inc_dirs) = @_;
+
+  return undef unless @$inc_dirs;
+
+  my $fn = module_notional_filename($mod);
+
+  for my $i ( 0 .. $#$inc_dirs ) {
+
+    # searching from here on out won't mean anything
+    # FIXME - there is actually a way to interrogate this safely, but
+    # that's a fight for another day
+    return undef if length ref $inc_dirs->[$i];
+
+    if (
+      -d $inc_dirs->[$i]
+        and
+      -f "$inc_dirs->[$i]/$fn"
+        and
+      -r "$inc_dirs->[$i]/$fn"
+    ) {
+      return $i;
+    }
+  }
+
+  return undef;
+}
+
+sub purge_identically_versioned_submodules_with_markers {
+  my $markers = shift;
+
+  return unless @$markers;
+
+  for my $mod ( sort { length($b) <=> length($a) } keys %$interesting_modules ) {
+
+    next unless defined $interesting_modules->{$mod}{version};
+
+    my $marker = $interesting_modules->{$mod}{source_marker}
+      or next;
+
+    next unless grep { $marker eq $_ } @$markers;
+
+    my $parent = $mod;
+
+    while ( $parent =~ s/ :: (?: . (?! :: ) )+ $ //x ) {
+      $interesting_modules->{$parent}
+        and
+      ($interesting_modules->{$parent}{version}||'') eq $interesting_modules->{$mod}{version}
+        and
+      ($interesting_modules->{$parent}{source_marker}||'') eq $interesting_modules->{$mod}{source_marker}
+        and
+    delete $interesting_modules->{$mod}
+        and
+      last
+    }
+  }
+}
+
+sub module_notional_filename {
+  (my $fn = $_[0] . '.pm') =~ s|::|/|g;
+  $fn;
+}
+
+sub get_md5 {
+  # we already checked for -r/-f, just bail if can't open
+  open my $fh, '<:raw', $_[0] or return '';
+  Digest::MD5->new->addfile($fh)->hexdigest;
+}
index 177231a..4b7f929 100644 (file)
@@ -6,11 +6,23 @@ use Test::Exception;
 use Test::Warn;
 use lib qw(t/lib);
 use DBICTest;
-use DBIx::Class::_Util 'sigwarn_silencer';
-use Path::Class::File ();
+use DBIx::Class::_Util qw(sigwarn_silencer serialize);
 use Math::BigInt;
 use List::Util qw/shuffle/;
-use Storable qw/nfreeze dclone/;
+
+{
+  package DBICTest::StringifiesOnly;
+  use overload
+    '""' => sub { $_[0]->[0] },
+    fallback => 0,
+  ;
+}
+{
+  package DBICTest::StringifiesViaFallback;
+  use overload
+    'bool' => sub { $_[0]->[0] },
+  ;
+}
 
 my $schema = DBICTest->init_schema();
 
@@ -88,8 +100,17 @@ is($link4->id, 4, 'Link 4 id');
 is($link4->url, undef, 'Link 4 url');
 is($link4->title, 'dtitle', 'Link 4 title');
 
+## variable size dataset
+@links = $schema->populate('Link', [
+[ qw/id title url/ ],
+[ 41 ],
+[ 42, undef, 'url42' ],
+]);
+is(scalar @links, 2);
+is($links[0]->url, undef);
+is($links[1]->url, 'url42');
 
-## make sure populate -> insert_bulk honors fields/orders in void context
+## make sure populate -> _insert_bulk honors fields/orders in void context
 ## schema order
 $schema->populate('Link', [
 [ qw/id url title/ ],
@@ -120,12 +141,69 @@ is($link7->id, 7, 'Link 7 id');
 is($link7->url, undef, 'Link 7 url');
 is($link7->title, 'gtitle', 'Link 7 title');
 
+## variable size dataset in void ctx
+$schema->populate('Link', [
+[ qw/id title url/ ],
+[ 71 ],
+[ 72, undef, 'url72' ],
+]);
+@links = $schema->resultset('Link')->search({ id => [71, 72]}, { order_by => 'id' })->all;
+is(scalar @links, 2);
+is($links[0]->url, undef);
+is($links[1]->url, 'url72');
+
+## variable size dataset in void ctx, hash version
+$schema->populate('Link', [
+  { id => 73 },
+  { id => 74, title => 't74' },
+  { id => 75, url => 'u75' },
+]);
+@links = $schema->resultset('Link')->search({ id => [73..75]}, { order_by => 'id' })->all;
+is(scalar @links, 3);
+is($links[0]->url, undef);
+is($links[0]->title, undef);
+is($links[1]->url, undef);
+is($links[1]->title, 't74');
+is($links[2]->url, 'u75');
+is($links[2]->title, undef);
+
+## Make sure the void ctx trace is sane
+{
+  for (
+    [
+      [ qw/id title url/ ],
+      [ 81 ],
+      [ 82, 't82' ],
+      [ 83, undef, 'url83' ],
+    ],
+    [
+      { id => 91 },
+      { id => 92, title => 't92' },
+      { id => 93, url => 'url93' },
+    ]
+  ) {
+    $schema->is_executed_sql_bind(
+      sub {
+        $schema->populate('Link', $_);
+      },
+      [
+        [ 'BEGIN' ],
+        [
+          'INSERT INTO link( id, title, url ) VALUES( ?, ?, ? )',
+          "__BULK_INSERT__"
+        ],
+        [ 'COMMIT' ],
+      ]
+    );
+  }
+}
+
 # populate with literals
 {
   my $rs = $schema->resultset('Link');
   $rs->delete;
 
-  # test insert_bulk with all literal sql (no binds)
+  # test populate with all literal sql (no binds)
 
   $rs->populate([
     (+{
@@ -163,7 +241,7 @@ is($link7->title, 'gtitle', 'Link 7 title');
   my $rs = $schema->resultset('Link');
   $rs->delete;
 
-  # test insert_bulk with all literal/bind sql
+  # test populate with all literal/bind sql
   $rs->populate([
     (+{
         url => \['?', [ {} => 'cpan.org' ] ],
@@ -178,7 +256,7 @@ is($link7->title, 'gtitle', 'Link 7 title');
 
   $rs->delete;
 
-  # test insert_bulk with mix literal and literal/bind
+  # test populate with mix literal and literal/bind
   $rs->populate([
     (+{
         url => \"'cpan.org'",
@@ -196,7 +274,7 @@ is($link7->title, 'gtitle', 'Link 7 title');
   # test mixed binds with literal sql/bind
 
   $rs->populate([ map { +{
-    url => \[ '? || ?', [ {} => 'cpan.org_' ], [ undef, $_ ] ],
+    url => \[ '? || ?', [ {} => 'cpan.org_' ], $_ ],
     title => "The 'best of' cpan",
   } } (1 .. 5) ]);
 
@@ -317,109 +395,137 @@ lives_ok {
 } 'literal+bind with semantically identical attrs works after normalization';
 
 # test all kinds of population with stringified objects
+# or with empty sets
 warnings_like {
-  local $ENV{DBIC_RT79576_NOWARN};
-
   my $rs = $schema->resultset('Artist')->search({}, { columns => [qw(name rank)], order_by => 'artistid' });
 
   # the stringification has nothing to do with the artist name
   # this is solely for testing consistency
-  my $fn = Path::Class::File->new ('somedir/somefilename.tmp');
-  my $fn2 = Path::Class::File->new ('somedir/someotherfilename.tmp');
+  my $fn = bless [ 'somedir/somefilename.tmp' ], 'DBICTest::StringifiesOnly';
+  my $fn2 = bless [ 'somedir/someotherfilename.tmp' ], 'DBICTest::StringifiesViaFallback';
   my $rank = Math::BigInt->new(42);
 
   my $args = {
-    'stringifying objects after regular values' => [ map
-      { { name => $_, rank => $rank } }
-      (
+    'stringifying objects after regular values' => { AoA => [
+      [qw( name rank )],
+      ( map { [ $_, $rank ] } (
         'supplied before stringifying objects',
         'supplied before stringifying objects 2',
         $fn,
         $fn2,
-      )
-    ],
-    'stringifying objects before regular values' => [ map
-      { { name => $_, rank => $rank } }
-      (
+      )),
+    ]},
+
+    'stringifying objects before regular values' => { AoA => [
+      [qw( rank name )],
+      ( map { [ $rank, $_ ] } (
         $fn,
         $fn2,
         'supplied after stringifying objects',
         'supplied after stringifying objects 2',
-      )
-    ],
-    'stringifying objects between regular values' => [ map
-      { { name => $_, rank => $rank } }
-      (
+      )),
+    ]},
+
+    'stringifying objects between regular values' => { AoA => [
+      [qw( name rank )],
+      ( map { [ $_, $rank ] } (
         'supplied before stringifying objects',
         $fn,
         $fn2,
         'supplied after stringifying objects',
-      )
-    ],
-    'stringifying objects around regular values' => [ map
-      { { name => $_, rank => $rank } }
-      (
+      ))
+    ]},
+
+    'stringifying objects around regular values' => { AoA => [
+      [qw( rank name )],
+      ( map { [ $rank, $_ ] } (
         $fn,
         'supplied between stringifying objects',
         $fn2,
-      )
-    ],
+      ))
+    ]},
+
+    'single stringifying object' => { AoA => [
+      [qw( rank name )],
+      [ $rank, $fn ],
+    ]},
+
+    'empty set' => { AoA => [
+      [qw( name rank )],
+    ]},
   };
 
-  local $Storable::canonical = 1;
-  my $preimage = nfreeze([$fn, $fn2, $rank, $args]);
+  # generate the AoH equivalent based on the AoAs above
+  # also generate the expected HRI output ( is_deeply is too smart for its own good )
+  for my $bag (values %$args) {
+    $bag->{AoH} = [];
+    $bag->{Expected} = [];
+    my @hdr = @{$bag->{AoA}[0]};
+    for my $v ( @{$bag->{AoA}}[1..$#{$bag->{AoA}}] ) {
+      push @{$bag->{AoH}}, my $h = {};
+      @{$h}{@hdr} = @$v;
+
+      push @{$bag->{Expected}}, my $hs = {};
+      @{$hs}{@hdr} = map { "$_" } @$v;
+    }
+  }
 
-  for my $tst (keys %$args) {
+  local $Storable::canonical = 1;
+  my $preimage = serialize($args);
 
-    # test void ctx
-    $rs->delete;
-    $rs->populate($args->{$tst});
-    is_deeply(
-      $rs->all_hri,
-      $args->{$tst},
-      "Populate() $tst in void context"
-    );
 
-    # test non-void ctx
-    $rs->delete;
-    my $dummy = $rs->populate($args->{$tst});
-    is_deeply(
-      $rs->all_hri,
-      $args->{$tst},
-      "Populate() $tst in non-void context"
-    );
+  for my $tst (keys %$args) {
+    for my $type (qw(AoA AoH)) {
+
+      # test void ctx
+      $rs->delete;
+      $rs->populate($args->{$tst}{$type});
+      is_deeply(
+        $rs->all_hri,
+        $args->{$tst}{Expected},
+        "Populate() $tst in void context"
+      );
+
+      # test scalar ctx
+      $rs->delete;
+      my $dummy = $rs->populate($args->{$tst}{$type});
+      is_deeply(
+        $rs->all_hri,
+        $args->{$tst}{Expected},
+        "Populate() $tst in non-void context"
+      );
+
+      # test list ctx
+      $rs->delete;
+      my @dummy = $rs->populate($args->{$tst}{$type});
+      is_deeply(
+        $rs->all_hri,
+        $args->{$tst}{Expected},
+        "Populate() $tst in non-void context"
+      );
+    }
 
     # test create() as we have everything set up already
     $rs->delete;
-    $rs->create($_) for @{$args->{$tst}};
+    $rs->create($_) for @{$args->{$tst}{AoH}};
 
     is_deeply(
       $rs->all_hri,
-      $args->{$tst},
+      $args->{$tst}{Expected},
       "Create() $tst"
     );
   }
 
   ok (
-    ($preimage eq nfreeze( [$fn, $fn2, $rank, $args] )),
+    ($preimage eq serialize($args)),
     'Arguments fed to populate()/create() unchanged'
   );
 
   $rs->delete;
-} [
-  # warning to be removed around Apr 1st 2015
-  # smokers start failing a month before that
-  (
-    ( DBICTest::RunMode->is_author and ( time() > 1427846400 ) )
-      or
-    ( DBICTest::RunMode->is_smoker and ( time() > 1425168000 ) )
-  )
-    ? ()
-    # one unique for populate() and create() each
-    : (qr/\QPOSSIBLE *PAST* DATA CORRUPTION detected \E.+\QTrigger condition encountered at @{[ __FILE__ ]} line\E \d/) x 2
-], 'Data integrity warnings as planned';
+} [], 'Data integrity warnings gone as planned';
 
-lives_ok {
+$schema->is_executed_sql_bind(
+  sub {
    $schema->resultset('TwoKeys')->populate([{
       artist => 1,
       cd     => 5,
@@ -437,7 +543,26 @@ lives_ok {
             autopilot => 'b',
       }]
    }])
-} 'multicol-PK has_many populate works';
+  },
+  [
+    [ 'BEGIN' ],
+    [ 'INSERT INTO twokeys ( artist, cd)
+        VALUES ( ?, ? )',
+      '__BULK_INSERT__'
+    ],
+    [ 'INSERT INTO fourkeys_to_twokeys ( autopilot, f_bar, f_foo, f_goodbye, f_hello, t_artist, t_cd)
+        VALUES (
+          ?, ?, ?, ?, ?,
+          ( SELECT me.artist FROM twokeys me WHERE artist = ? AND cd = ? ),
+          ( SELECT me.cd FROM twokeys me WHERE artist = ? AND cd = ? )
+        )
+      ',
+      '__BULK_INSERT__'
+    ],
+    [ 'COMMIT' ],
+  ],
+  'multicol-PK has_many populate expected trace'
+);
 
 lives_ok ( sub {
   $schema->populate('CD', [
index 56e87f0..5686c3e 100644 (file)
@@ -12,6 +12,8 @@ use strict;
 use warnings;
 
 use Test::More;
+use Test::Warn;
+use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
 
@@ -24,7 +26,7 @@ my $schema  = DBICTest->init_schema();
 my $art_rs  = $schema->resultset('Artist');
 my $cd_rs  = $schema->resultset('CD');
 
-my $restricted_art_rs  = $art_rs->search({rank => 42});
+my $restricted_art_rs  = $art_rs->search({ -and => [ rank => 42, charfield => { '=', \['(SELECT MAX(artistid) FROM artist) + ?', 6] } ] });
 
 ok( $schema, 'Got a Schema object');
 ok( $art_rs, 'Got Good Artist Resultset');
@@ -37,10 +39,10 @@ ok( $cd_rs, 'Got Good CD Resultset');
 
 SCHEMA_POPULATE1: {
 
-  ## Test to make sure that the old $schema->populate is using the new method
-  ## for $resultset->populate when in void context and with sub objects.
+  # throw a monkey wrench
+  my $post_jnap_monkeywrench = $schema->resultset('Artist')->find(1)->update({ name => undef });
 
-  $schema->populate('Artist', [
+  warnings_exist { $schema->populate('Artist', [
 
     [qw/name cds/],
     ["001First Artist", [
@@ -55,13 +57,13 @@ SCHEMA_POPULATE1: {
     [undef, [
       {title=>"004Title1", year=>2010}
     ]],
-  ]);
+  ]) } qr/\QFast-path populate() of non-uniquely identifiable rows with related data is not possible/;
 
   isa_ok $schema, 'DBIx::Class::Schema';
 
-  my ($undef, $artist1, $artist2, $artist3 ) = $schema->resultset('Artist')->search({
+  my ( $preexisting_undef, $artist1, $artist2, $artist3, $undef ) = $schema->resultset('Artist')->search({
     name=>["001First Artist","002Second Artist","003Third Artist", undef]},
-    {order_by=>'name ASC'})->all;
+    {order_by => { -asc => 'artistid' }})->all;
 
   isa_ok  $artist1, 'DBICTest::Artist';
   isa_ok  $artist2, 'DBICTest::Artist';
@@ -78,6 +80,8 @@ SCHEMA_POPULATE1: {
   ok $artist3->cds->count eq 1, "Got Right number of CDs for Artist3";
   ok $undef->cds->count eq 1, "Got Right number of CDs for Artist4";
 
+  $post_jnap_monkeywrench->delete;
+
   ARTIST1CDS: {
 
     my ($cd1, $cd2, $cd3) = $artist1->cds->search(undef, {order_by=>'year ASC'});
@@ -343,7 +347,9 @@ ARRAY_CONTEXT: {
     ]);
 
     ## Did it use the condition in the resultset?
+    $more_crap->discard_changes;
     cmp_ok( $more_crap->rank, '==', 42, "Got Correct rank for result object");
+    cmp_ok( $more_crap->charfield, '==', $more_crap->id + 5, "Got Correct charfield for result object");
   }
 }
 
@@ -473,7 +479,9 @@ VOID_CONTEXT: {
       },
     ];
 
-    $cd_rs->populate($cds);
+    warnings_exist {
+      $cd_rs->populate($cds)
+    } qr/\QFast-path populate() of belongs_to relationship data is not possible/;
 
     my ($cdA, $cdB) = $cd_rs->search(
       {title=>[sort map {$_->{title}} @$cds]},
@@ -513,7 +521,9 @@ VOID_CONTEXT: {
       },
     ];
 
-    $cd_rs->populate($cds);
+    warnings_exist {
+      $cd_rs->populate($cds);
+    } qr/\QFast-path populate() of belongs_to relationship data is not possible/;
 
     my ($cdA, $cdB, $cdC) = $cd_rs->search(
       {title=>[sort map {$_->{title}} @$cds]},
@@ -626,7 +636,9 @@ VOID_CONTEXT: {
     })->first;
 
     ## Did it use the condition in the resultset?
+    $more_crap->discard_changes;
     cmp_ok( $more_crap->rank, '==', 42, "Got Correct rank for result object");
+    cmp_ok( $more_crap->charfield, '==', $more_crap->id + 5, "Got Correct charfield for result object");
   }
 }
 
@@ -655,7 +667,11 @@ ARRAYREF_OF_ARRAYREF_STYLE: {
   is $cooler->name, 'Cooler', 'Correct Name';
   is $lamer->name, 'Lamer', 'Correct Name';
 
-  cmp_ok $cooler->rank, '==', 42, 'Correct Rank';
+  for ($cooler, $lamer) {
+    $_->discard_changes;
+    cmp_ok( $_->rank, '==', 42, "Got Correct rank for result object");
+    cmp_ok( $_->charfield, '==', $_->id + 5, "Got Correct charfield for result object");
+  }
 
   ARRAY_CONTEXT_WITH_COND_FROM_RS: {
 
@@ -666,7 +682,9 @@ ARRAYREF_OF_ARRAYREF_STYLE: {
     ]);
 
     ## Did it use the condition in the resultset?
+    $mega_lamer->discard_changes;
     cmp_ok( $mega_lamer->rank, '==', 42, "Got Correct rank for result object");
+    cmp_ok( $mega_lamer->charfield, '==', $mega_lamer->id + 5, "Got Correct charfield for result object");
   }
 
   VOID_CONTEXT_WITH_COND_FROM_RS: {
@@ -683,9 +701,30 @@ ARRAYREF_OF_ARRAYREF_STYLE: {
 
     ## Did it use the condition in the resultset?
     cmp_ok( $mega_lamer->rank, '==', 42, "Got Correct rank for result object");
+    cmp_ok( $mega_lamer->charfield, '==', $mega_lamer->id + 5, "Got Correct charfield for result object");
   }
 }
 
-ok(eval { $art_rs->populate([]); 1 }, "Empty populate runs but does nothing");
+EMPTY_POPULATE: {
+  foreach(
+    [ empty         => [] ],
+    [ columns_only  => [ [qw(name rank charfield)] ] ],
+  ) {
+    my ($desc, $arg) = @{$_};
+
+    $schema->is_executed_sql_bind( sub {
+
+      my $rs = $art_rs;
+      lives_ok { $rs->populate($arg); 1 } "$desc populate in void context lives";
+
+      my @r = $art_rs->populate($arg);
+      is_deeply( \@r, [], "$desc populate in list context returns empty list" );
+
+      my $r = $art_rs->populate($arg);
+      is( $r, undef, "$desc populate in scalar context returns undef" );
+
+    }, [], "$desc populate executed no statements" );
+  }
+}
 
 done_testing;
index a13ea00..4abe7e8 100644 (file)
@@ -29,6 +29,7 @@ is_deeply (
       {
         result_class => 'DBIx::Class::ResultClass::HashRefInflator',
         prefetch => ['artist', { tracks => [qw/cd year1999cd year2000cd/] } ],
+        order_by => 'tracks.trackid',
       },
     )->all
   ],
@@ -39,6 +40,7 @@ is_deeply (
         result_class => 'DBIx::Class::ResultClass::HashRefInflator',
         prefetch => ['artist', { tracks => [qw/cd year1999cd year2000cd/] } ],
         columns => [qw/cdid single_track title/],   # to match the columns retrieved by the virtview
+        order_by => 'tracks.trackid',
       },
     )->all
   ],
index cd49cec..17657cc 100644 (file)
@@ -2,11 +2,8 @@ use strict;
 use warnings;
 
 use Test::More;
-use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::DebugObj;
-use DBIC::SqlMakerTest;
 
 my $schema = DBICTest->init_schema();
 $schema->storage->sql_maker->quote_char('"');
@@ -15,27 +12,12 @@ my $rs = $schema->resultset ('Artist');
 my $last_obj = $rs->search ({}, { order_by => { -desc => 'artistid' }, rows => 1})->single;
 my $last_id = $last_obj ? $last_obj->artistid : 0;
 
-
-my ($sql, @bind);
-my $orig_debugobj = $schema->storage->debugobj;
-my $orig_debug = $schema->storage->debug;
-
-$schema->storage->debugobj (DBIC::DebugObj->new (\$sql, \@bind) );
-$schema->storage->debug (1);
-
 my $obj;
-lives_ok { $obj = $rs->create ({}) } 'Default insert successful';
-
-$schema->storage->debugobj ($orig_debugobj);
-$schema->storage->debug ($orig_debug);
-
-is_same_sql_bind (
-  $sql,
-  \@bind,
-  'INSERT INTO "artist" DEFAULT VALUES',
-  [],
-  'Default-value insert correct SQL',
-);
+$schema->is_executed_sql_bind( sub {
+  $obj = $rs->create ({})
+}, [[
+  'INSERT INTO "artist" DEFAULT VALUES'
+]], 'Default-value insert correct SQL' );
 
 ok ($obj, 'Insert defaults ( $rs->create ({}) )' );
 
index fdee230..3b351ab 100644 (file)
@@ -3,6 +3,7 @@ use warnings;
 
 use Test::More;
 use Test::Exception;
+use Test::Warn;
 
 use lib qw(t/lib);
 
@@ -23,4 +24,29 @@ is_deeply (
   'Exception-arrayref contents preserved',
 );
 
+for my $ap (qw(
+  DBICTest::AntiPattern::TrueZeroLen
+  DBICTest::AntiPattern::NullObject
+)) {
+  eval "require $ap";
+
+  warnings_like {
+    eval {
+      $schema->txn_do (sub { die $ap->new });
+    };
+
+    isa_ok $@, $ap;
+  } qr/\QObjects of external exception class '$ap' stringify to '' (the empty string)/,
+    'Proper warning on encountered antipattern';
+
+  warnings_are {
+    $@ = $ap->new;
+    $schema->txn_do (sub { 1 });
+
+    $@ = $ap->new;
+    $schema->txn_scope_guard->commit;
+  } [], 'No spurious PSA warnings on pre-existing antipatterns in $@';
+
+}
+
 done_testing;
index 6de03f0..d7885d5 100644 (file)
@@ -28,8 +28,18 @@ throws_ok { $e->rethrow }
 isa_ok( $@, 'DBIx::Class::Exception' );
 
 # Now lets rethrow via exception_action
-$schema->exception_action(sub { die @_ });
-throws_ok \&$throw, $ex_regex;
+{
+  my $handler_execution_counter = 0;
+
+  $schema->exception_action(sub {
+    $handler_execution_counter++;
+    like $_[0], $ex_regex, "Exception is precisely passed to exception_action";
+    die @_
+  });
+
+  throws_ok \&$throw, $ex_regex;
+  is $handler_execution_counter, 1, "exception_action handler executed exactly once";
+}
 
 #
 # This should have never worked!!!
@@ -80,4 +90,30 @@ throws_ok \&$throw,
 throws_ok { $schema->storage->throw_exception('floob') }
   qr/DBICTest::Exception is handling this: floob/;
 
+# test antipatterns
+for my $ap (qw(
+  DBICTest::AntiPattern::TrueZeroLen
+  DBICTest::AntiPattern::NullObject
+)) {
+  eval "require $ap";
+  my $exp_warn = qr/\QObjects of external exception class '$ap' stringify to '' (the empty string)/;
+
+  # make sure an exception_action can replace $@ with an antipattern
+  $schema->exception_action(sub { die $ap->new });
+  warnings_like {
+    eval { $throw->() };
+    isa_ok $@, $ap;
+  } $exp_warn, 'proper warning on antipattern encountered within exception_action';
+
+  # and make sure that the rethrow works
+  $schema->exception_action(sub { die @_ });
+  warnings_like {
+    eval {
+      $schema->txn_do (sub { die $ap->new });
+    };
+
+    isa_ok $@, $ap;
+  } $exp_warn, 'Proper warning on encountered antipattern';
+}
+
 done_testing;
diff --git a/t/35exception_inaction.t b/t/35exception_inaction.t
new file mode 100644 (file)
index 0000000..0d8597f
--- /dev/null
@@ -0,0 +1,102 @@
+use strict;
+use warnings;
+
+use lib 't/lib';
+use DBICTest::RunMode;
+BEGIN {
+  if( DBICTest::RunMode->is_plain ) {
+    print "1..0 # SKIP not running dangerous segfault-prone test on plain install\n";
+    exit 0;
+  }
+}
+
+use File::Temp ();
+use DBIx::Class::_Util 'scope_guard';
+use DBIx::Class::Schema;
+
+# Do not use T::B - the test is hard enough not to segfault as it is
+my $test_count = 0;
+
+# start with one failure, and decrement it at the end
+my $failed = 1;
+
+sub ok {
+  printf STDOUT ("%s %u - %s\n",
+    ( $_[0] ? 'ok' : 'not ok' ),
+    ++$test_count,
+    $_[1] || '',
+  );
+
+  unless( $_[0] ) {
+    $failed++;
+    printf STDERR ("# Failed test #%d at %s line %d\n",
+      $test_count,
+      (caller(0))[1,2]
+    );
+  }
+
+  return !!$_[0];
+}
+
+# yes, make it even dirtier
+my $schema = 'DBIx::Class::Schema';
+
+$schema->connection('dbi:SQLite::memory:');
+
+# this is incredibly horrible...
+# demonstrate utter breakage of the reconnection/retry logic
+#
+open(my $stderr_copy, '>&', *STDERR) or die "Unable to dup STDERR: $!";
+my $tf = File::Temp->new( UNLINK => 1 );
+
+my $output;
+
+ESCAPE:
+{
+  my $guard = scope_guard {
+    close STDERR;
+    open(STDERR, '>&', $stderr_copy);
+    $output = do { local (@ARGV, $/) = $tf; <> };
+    close $tf;
+    unlink $tf;
+    undef $tf;
+    close $stderr_copy;
+  };
+
+  close STDERR;
+  open(STDERR, '>&', $tf) or die "Unable to reopen STDERR: $!";
+
+  $schema->storage->ensure_connected;
+  $schema->storage->_dbh->disconnect;
+
+  local $SIG{__WARN__} = sub {};
+
+  $schema->exception_action(sub {
+    ok(1, 'exception_action invoked');
+    # essentially what Dancer2's redirect() does after https://github.com/PerlDancer/Dancer2/pull/485
+    # which "nicely" combines with: https://metacpan.org/source/MARKOV/Log-Report-1.12/lib/Dancer2/Plugin/LogReport.pm#L143
+    # as encouraged by: https://metacpan.org/pod/release/MARKOV/Log-Report-1.12/lib/Dancer2/Plugin/LogReport.pod#Logging-DBIC-database-queries-and-errors
+    last ESCAPE;
+  });
+
+  # this *DOES* throw, but the exception will *NEVER SHOW UP*
+  $schema->storage->dbh_do(sub { $_[1]->selectall_arrayref("SELECT * FROM wfwqfdqefqef") } );
+
+  # NEITHER will this
+  ok(0, "Nope");
+}
+
+ok(1, "Post-escape reached");
+
+ok(
+  !!( $output =~ /DBIx::Class INTERNAL PANIC.+FIX YOUR ERROR HANDLING/s ),
+  'Proper warning emitted on STDERR'
+) or print STDERR "Instead found:\n\n$output\n";
+
+print "1..$test_count\n";
+
+# this is our "done_testing"
+$failed--;
+
+# avoid tasty segfaults on 5.8.x
+exit( $failed );
index 6cd62ff..051ab9b 100644 (file)
@@ -16,15 +16,4 @@ warnings_exist { DBICTest->init_schema( compose_connection => 1, sqlite_use_file
 
 cmp_ok(DBICTest->resultset('Artist')->count, '>', 0, 'count is valid');
 
-# cleanup globals so we do not trigger the leaktest
-for ( map { DBICTest->schema->class($_) } DBICTest->schema->sources ) {
-  $_->class_resolver(undef);
-  $_->resultset_instance(undef);
-  $_->result_source_instance(undef);
-}
-{
-  no warnings qw/redefine once/;
-  *DBICTest::schema = sub {};
-}
-
 done_testing;
index a54adb6..f798ace 100644 (file)
@@ -6,12 +6,13 @@ use lib qw(t/lib);
 use DBICTest;
 my $schema = DBICTest->init_schema();
 
-plan tests => 19;
-
 # select from a class with resultset_attributes
 my $resultset = $schema->resultset('BooksInLibrary');
 is($resultset, 3, "select from a class with resultset_attributes okay");
 
+$resultset = $resultset->search({}, { where => undef });
+is($resultset, 3, "where condition not obliterated");
+
 # now test out selects through a resultset
 my $owner = $schema->resultset('Owners')->find({name => "Newton"});
 my $programming_perl = $owner->books->find_or_create({ title => "Programming Perl" });
@@ -82,3 +83,5 @@ if ($@) { print $@ }
 ok( !$@, 'many_to_many set_$rel(\@objects) did not throw');
 is($pointy_objects->count, $pointy_count, 'many_to_many set_$rel($hash) count correct');
 is($round_objects->count, $round_count, 'many_to_many set_$rel($hash) other rel count correct');
+
+done_testing;
index af61dca..a9fbdec 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_pg';
+
 use strict;
 use warnings;
 use Test::More;
@@ -5,18 +7,9 @@ use Test::Exception;
 
 use lib qw(t/lib);
 use DBICTest;
-use DBIx::Class::Optional::Dependencies ();
 
 my $main_pid = $$;
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('rdbms_pg')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('rdbms_pg');
-
-my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
-
-plan skip_all => 'Set $ENV{DBICTEST_PG_DSN}, _USER and _PASS to run this test'
-      . ' (note: creates and drops a table named artist!)' unless ($dsn && $user);
-
 # README: If you set the env var to a number greater than 10,
 #   we will use that many children
 my $num_children = $ENV{DBICTEST_FORK_STRESS} || 1;
@@ -24,6 +17,7 @@ if($num_children !~ /^[0-9]+$/ || $num_children < 10) {
    $num_children = 10;
 }
 
+my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass, { AutoCommit => 1 });
 
 my $parent_rs;
index 95c9aaf..3af78d5 100644 (file)
@@ -15,15 +15,16 @@ use threads;
 use strict;
 use warnings;
 use Test::More;
+use DBIx::Class::_Util 'sigwarn_silencer';
 
 use lib qw(t/lib);
 use DBICTest;
 
 plan skip_all => 'DBIC does not actively support threads before perl 5.8.5'
-  if $] < '5.008005';
+  if "$]" < 5.008005;
 
 plan skip_all => 'Potential problems on Win32 Perl < 5.14 and Variable::Magic - investigation pending'
-  if $^O eq 'MSWin32' && $] < 5.014 && DBICTest::RunMode->is_plain;
+  if $^O eq 'MSWin32' && "$]" < 5.014 && DBICTest::RunMode->is_plain;
 
 # README: If you set the env var to a number greater than 10,
 #   we will use that many children
@@ -36,14 +37,27 @@ my $schema = DBICTest->init_schema(no_deploy => 1);
 isa_ok ($schema, 'DBICTest::Schema');
 
 my @threads;
-push @threads, threads->create(sub {
-  my $rsrc = $schema->source('Artist');
-  undef $schema;
-  isa_ok ($rsrc->schema, 'DBICTest::Schema');
-  my $s2 = $rsrc->schema->clone;
-
-  sleep 1;  # without this many tasty crashes
-}) for (1.. $num_children);
+SKIP: {
+
+  local $SIG{__WARN__} = sigwarn_silencer( qr/Thread creation failed/i );
+
+  for (1.. $num_children) {
+    push @threads, threads->create(sub {
+      my $rsrc = $schema->source('Artist');
+      undef $schema;
+      isa_ok ($rsrc->schema, 'DBICTest::Schema');
+      my $s2 = $rsrc->schema->clone;
+
+      sleep 1;  # without this many tasty crashes
+    }) || do {
+      skip "EAGAIN encountered, your system is likely bogged down: skipping rest of test", 1
+        if $! == Errno::EAGAIN();
+
+      die "Unable to start thread: $!";
+    };
+  }
+}
+
 ok(1, "past spawning");
 
 $_->join for @threads;
index 382458d..ae3addc 100644 (file)
@@ -12,6 +12,8 @@ BEGIN {
 }
 use threads;
 
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_pg';
+
 use strict;
 use warnings;
 
@@ -19,19 +21,11 @@ use Test::More;
 use Test::Exception;
 
 plan skip_all => 'DBIC does not actively support threads before perl 5.8.5'
-  if $] < '5.008005';
+  if "$]" < 5.008005;
 
-use DBIx::Class::Optional::Dependencies ();
 use lib qw(t/lib);
 use DBICTest;
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('rdbms_pg')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('rdbms_pg');
-
-my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
-plan skip_all => 'Set $ENV{DBICTEST_PG_DSN}, _USER and _PASS to run this test'
-      . ' (note: creates and drops a table named artist!)' unless ($dsn && $user);
-
 # README: If you set the env var to a number greater than 10,
 #   we will use that many children
 my $num_children = $ENV{DBICTEST_THREAD_STRESS} || 1;
@@ -39,7 +33,7 @@ if($num_children !~ /^[0-9]+$/ || $num_children < 10) {
    $num_children = 10;
 }
 
-use_ok('DBICTest::Schema');
+my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
 
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass, { AutoCommit => 1, RaiseError => 1, PrintError => 0 });
 
index a0e07bd..e74c7c1 100644 (file)
@@ -15,32 +15,26 @@ BEGIN {
 }
 use threads;
 
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_pg';
+
 use strict;
 use warnings;
 
 use Test::More;
 
 plan skip_all => 'DBIC does not actively support threads before perl 5.8.5'
-  if $] < '5.008005';
+  if "$]" < 5.008005;
 
-use DBIx::Class::Optional::Dependencies ();
 use Scalar::Util 'weaken';
 use lib qw(t/lib);
 use DBICTest;
 
-my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
-plan skip_all => 'Set $ENV{DBICTEST_PG_DSN}, _USER and _PASS to run this test'
-      . ' (note: creates and drops a table named artist!)' unless ($dsn && $user);
-
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('rdbms_pg')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('rdbms_pg');
-
 my $num_children = $ENV{DBICTEST_THREAD_STRESS} || 1;
 if($num_children !~ /^[0-9]+$/ || $num_children < 10) {
    $num_children = 10;
 }
 
-use_ok('DBICTest::Schema');
+my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
 
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass, { AutoCommit => 1, RaiseError => 1, PrintError => 0 });
 
index c566a9a..c6b64c2 100644 (file)
@@ -21,23 +21,26 @@ use strict;
 use warnings;
 use Test::More;
 
+use lib qw(t/lib);
+use DBICTest::RunMode;
+use DBICTest::Util::LeakTracer qw(populate_weakregistry assert_empty_weakregistry visit_refs);
+use Scalar::Util qw(weaken blessed reftype);
+use DBIx::Class::_Util qw(hrefaddr sigwarn_silencer modver_gt_or_eq modver_gt_or_eq_and_lt);
+BEGIN {
+  plan skip_all => "Your perl version $] appears to leak like a sieve - skipping test"
+    if DBIx::Class::_ENV_::PEEPEENESS;
+}
+
+
 my $TB = Test::More->builder;
 if ($ENV{DBICTEST_IN_PERSISTENT_ENV}) {
-  # without this explicit close older TBs warn in END after a ->reset
-  if ($TB->VERSION < 1.005) {
-    close ($TB->$_) for (qw/output failure_output todo_output/);
-  }
+  # without this explicit close TB warns in END after a ->reset
+  close ($TB->$_) for qw(output failure_output todo_output);
 
-  # if I do not do this, I get happy sigpipes on new TB, no idea why
-  # (the above close-and-forget doesn't work - new TB does *not* reopen
-  # its handles automatically anymore)
-  else {
-    for (qw/failure_output todo_output/) {
-      close $TB->$_;
-      open ($TB->$_, '>&', *STDERR);
-    }
-
-    close $TB->output;
+  # newer TB does not auto-reopen handles
+  if ( modver_gt_or_eq( 'Test::More', '1.200' ) ) {
+    open ($TB->$_, '>&', *STDERR)
+      for qw( failure_output todo_output );
     open ($TB->output, '>&', *STDOUT);
   }
 
@@ -45,17 +48,6 @@ if ($ENV{DBICTEST_IN_PERSISTENT_ENV}) {
   $TB->reset;
 }
 
-use lib qw(t/lib);
-use DBICTest::RunMode;
-use DBICTest::Util::LeakTracer qw(populate_weakregistry assert_empty_weakregistry visit_refs);
-use Scalar::Util qw(weaken blessed reftype);
-use DBIx::Class;
-use DBIx::Class::_Util qw(hrefaddr sigwarn_silencer);
-BEGIN {
-  plan skip_all => "Your perl version $] appears to leak like a sieve - skipping test"
-    if DBIx::Class::_ENV_::PEEPEENESS;
-}
-
 # this is what holds all weakened refs to be checked for leakage
 my $weak_registry = {};
 
@@ -63,7 +55,8 @@ my $weak_registry = {};
 my $has_dt;
 
 # Skip the heavy-duty leak tracing when just doing an install
-unless (DBICTest::RunMode->is_plain) {
+# or when having Moose crap all over everything
+if ( !$ENV{DBICTEST_VIA_REPLICATED} and !DBICTest::RunMode->is_plain ) {
 
   # redefine the bless override so that we can catch each and every object created
   no warnings qw/redefine once/;
@@ -87,7 +80,7 @@ unless (DBICTest::RunMode->is_plain) {
     # Test Builder is now making a new object for every pass/fail (que bloat?)
     # and as such we can't really store any of its objects (since it will
     # re-populate the registry while checking it, ewwww!)
-    return $obj if (ref $obj) =~ /^TB2::/;
+    return $obj if (ref $obj) =~ /^TB2::|^Test::Stream/;
 
     # populate immediately to avoid weird side effects
     return populate_weakregistry ($weak_registry, $obj );
@@ -106,7 +99,7 @@ unless (DBICTest::RunMode->is_plain) {
   # Load them and empty the registry
 
   # this loads the DT armada
-  $has_dt = DBIx::Class::Optional::Dependencies->req_ok_for('test_dt_sqlite');
+  $has_dt = DBIx::Class::Optional::Dependencies->req_ok_for([qw( test_rdbms_sqlite ic_dt )]);
 
   require Errno;
   require DBI;
@@ -124,8 +117,6 @@ unless (DBICTest::RunMode->is_plain) {
   my $rs = $schema->resultset ('Artist');
   my $storage = $schema->storage;
 
-  ok ($storage->connected, 'we are connected');
-
   my $row_obj = $rs->search({}, { rows => 1})->next;  # so that commits/rollbacks work
   ok ($row_obj, 'row from db');
 
@@ -328,9 +319,6 @@ unless (DBICTest::RunMode->is_plain) {
       ! DBICTest::RunMode->is_plain
         and
       ! $ENV{DBICTEST_IN_PERSISTENT_ENV}
-        and
-      # FIXME - investigate wtf is going on with 5.18
-      ! ( $] > 5.017 and $ENV{DBIC_TRACE_PROFILE} )
     ) {
 
       # FIXME - ideally we should be able to just populate an alternative
@@ -458,6 +446,18 @@ for my $addr (keys %$weak_registry) {
     delete $weak_registry->{$addr}
       unless $cleared->{hash_merge_singleton}{$weak_registry->{$addr}{weakref}{behavior}}++;
   }
+  elsif ($names =~ /^B::Hooks::EndOfScope::PP::_TieHintHashFieldHash/m) {
+    # there is one tied lexical which stays alive until GC time
+    # https://metacpan.org/source/ETHER/B-Hooks-EndOfScope-0.15/lib/B/Hooks/EndOfScope/PP/FieldHash.pm#L24
+    # simply ignore it here, instead of teaching the leaktracer to examine ties
+    # the latter is possible yet terrible: https://github.com/dbsrgits/dbix-class/blob/v0.082820/t/lib/DBICTest/Util/LeakTracer.pm#L113-L117
+    delete $weak_registry->{$addr}
+      unless $cleared->{bheos_pptiehinthashfieldhash}++;
+  }
+  elsif ($names =~ /^DateTime::TimeZone::UTC/m) {
+    # DT is going through a refactor it seems - let it leak zones for now
+    delete $weak_registry->{$addr};
+  }
   elsif (
 #    # if we can look at closed over pieces - we will register it as a global
 #    !DBICTest::Util::LeakTracer::CV_TRACING
@@ -519,29 +519,7 @@ assert_empty_weakregistry ($weak_registry);
 # this is ugly and dirty but we do not yet have a Test::Embedded or
 # similar
 
-# set up -I
-require Config;
-$ENV{PERL5LIB} = join ($Config::Config{path_sep}, @INC);
-($ENV{PATH}) = $ENV{PATH} =~ /(.+)/;
-
-
-my $persistence_tests = {
-  PPerl => {
-    cmd => [qw/pperl --prefork=1/, __FILE__],
-  },
-  'CGI::SpeedyCGI' => {
-    cmd => [qw/speedy -- -t5/, __FILE__],
-  },
-};
-
-# scgi is smart and will auto-reap after -t amount of seconds
-# pperl needs an actual killer :(
-$persistence_tests->{PPerl}{termcmd} = [
-  $persistence_tests->{PPerl}{cmd}[0],
-  '--kill',
-  @{$persistence_tests->{PPerl}{cmd}}[ 1 .. $#{$persistence_tests->{PPerl}{cmd}} ],
-];
-
+my $persistence_tests;
 SKIP: {
   skip 'Test already in a persistent loop', 1
     if $ENV{DBICTEST_IN_PERSISTENT_ENV};
@@ -549,9 +527,39 @@ SKIP: {
   skip 'Main test failed - skipping persistent env tests', 1
     unless $TB->is_passing;
 
+  skip "Test::Builder\@@{[ Test::Builder->VERSION ]} known to break persistence tests", 1
+    if modver_gt_or_eq_and_lt( 'Test::More', '1.200', '1.301001_099' );
+
   local $ENV{DBICTEST_IN_PERSISTENT_ENV} = 1;
 
-  require IPC::Open2;
+  $persistence_tests = {
+    PPerl => {
+      cmd => [qw/pperl --prefork=1/, __FILE__],
+    },
+    'CGI::SpeedyCGI' => {
+      cmd => [qw/speedy -- -t5/, __FILE__],
+    },
+  };
+
+  # scgi is smart and will auto-reap after -t amount of seconds
+  # pperl needs an actual killer :(
+  $persistence_tests->{PPerl}{termcmd} = [
+    $persistence_tests->{PPerl}{cmd}[0],
+    '--kill',
+    @{$persistence_tests->{PPerl}{cmd}}[ 1 .. $#{$persistence_tests->{PPerl}{cmd}} ],
+  ];
+
+  # set up -I
+  require Config;
+  $ENV{PERL5LIB} = join ($Config::Config{path_sep}, @INC);
+
+  # adjust PATH for -T
+  if (length $ENV{PATH}) {
+    ( $ENV{PATH} ) = join ( $Config::Config{path_sep},
+      map { length($_) ? File::Spec->rel2abs($_) : () }
+        split /\Q$Config::Config{path_sep}/, $ENV{PATH}
+    ) =~ /\A(.+)\z/;
+  }
 
   for my $type (keys %$persistence_tests) { SKIP: {
     unless (eval "require $type") {
@@ -573,6 +581,8 @@ SKIP: {
         if system(@cmd);
     }
 
+    require IPC::Open2;
+
     for (1,2,3) {
       note ("Starting run in persistent env ($type pass $_)");
       IPC::Open2::open2(my $out, undef, @cmd);
@@ -599,10 +609,13 @@ done_testing;
 # just an extra precaution in case we blew away from the SKIP - since there are no
 # PID files to go by (man does pperl really suck :(
 END {
-  unless ($ENV{DBICTEST_IN_PERSISTENT_ENV}) {
-    close $_ for (*STDIN, *STDOUT, *STDERR);
+  if ($persistence_tests->{PPerl}{termcmd}) {
     local $?; # otherwise test will inherit $? of the system()
-    system (@{$persistence_tests->{PPerl}{termcmd}})
-      if $persistence_tests->{PPerl}{termcmd};
+    require IPC::Open3;
+    open my $null, ">", File::Spec->devnull;
+    waitpid(
+      IPC::Open3::open3(undef, $null, $null, @{$persistence_tests->{PPerl}{termcmd}}),
+      0,
+    );
   }
 }
index 7f0db9a..6b866e6 100644 (file)
@@ -6,6 +6,11 @@ use Config;
 # doesn't work. We don't want to have the user deal with that.
 BEGIN { unless ($INC{'t/lib/DBICTest/WithTaint.pm'}) {
 
+  if ( $^O eq 'MSWin32' and $^X =~ /\x20/ ) {
+    print "1..0 # SKIP Running this test on Windows with spaces within the perl executable path (\$^X) is not possible due to https://rt.perl.org/Ticket/Display.html?id=123907\n";
+    exit 0;
+  }
+
   # it is possible the test itself is initially invoked in taint mode
   # and with relative paths *and* with a relative $^X and some other
   # craziness... in short: just be proactive
index f2e363a..595df62 100644 (file)
@@ -5,8 +5,7 @@ use Test::More;
 use Test::Exception;
 use Test::Warn;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
@@ -131,6 +130,13 @@ throws_ok {
 
 is($schema->resultset("Artist")->count, 4, 'count ok');
 
+# test find on an unresolvable condition
+is(
+  $schema->resultset('Artist')->find({ artistid => [ -and => 1, 2 ]}),
+  undef
+);
+
+
 # test find_or_new
 {
   my $existing_obj = $schema->resultset('Artist')->find_or_new({
@@ -200,41 +206,6 @@ $new = $schema->resultset("Track")->new( {
 $new->update_or_insert;
 ok($new->in_storage, 'update_or_insert insert ok');
 
-# test in update mode
-$new->title('Insert or Update - updated');
-$new->update_or_insert;
-is( $schema->resultset("Track")->find(100)->title, 'Insert or Update - updated', 'update_or_insert update ok');
-
-SKIP: {
-    skip "Tests require " . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt_sqlite'), 13
-      unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt_sqlite');
-
-    # test get_inflated_columns with objects
-    my $event = $schema->resultset('Event')->search->first;
-    my %edata = $event->get_inflated_columns;
-    is($edata{'id'}, $event->id, 'got id');
-    isa_ok($edata{'starts_at'}, 'DateTime', 'start_at is DateTime object');
-    isa_ok($edata{'created_on'}, 'DateTime', 'create_on DateTime object');
-    is($edata{'starts_at'}, $event->starts_at, 'got start date');
-    is($edata{'created_on'}, $event->created_on, 'got created date');
-
-
-    # get_inflated_columns w/relation and accessor alias
-    isa_ok($new->updated_date, 'DateTime', 'have inflated object via accessor');
-    my %tdata = $new->get_inflated_columns;
-    is($tdata{'trackid'}, 100, 'got id');
-    isa_ok($tdata{'cd'}, 'DBICTest::CD', 'cd is CD object');
-    is($tdata{'cd'}->id, 1, 'cd object is id 1');
-    is(
-        $tdata{'position'},
-        $schema->resultset ('Track')->search ({cd => 1})->count,
-        'Ordered assigned proper position',
-    );
-    is($tdata{'title'}, 'Insert or Update - updated');
-    is($tdata{'last_updated_on'}, '1973-07-19T12:01:02');
-    isa_ok($tdata{'last_updated_on'}, 'DateTime', 'inflated accessored column');
-}
-
 throws_ok (sub {
   $schema->class("Track")->load_components('DoesNotExist');
 }, qr!Can't locate DBIx/Class/DoesNotExist.pm!, 'exception on nonexisting component');
@@ -381,24 +352,29 @@ lives_ok (sub { my $newlink = $newbook->link}, "stringify to false value doesn't
   $schema->source("Artist")->column_info_from_storage(1);
   $schema->source("Artist")->{_columns_info_loaded} = 0;
 
+  my @undef_default = DBIx::Class::_ENV_::STRESSTEST_COLUMN_INFO_UNAWARE_STORAGE
+    ? ()
+    : ( default_value => undef )
+  ;
+
   is_deeply (
     $schema->source('Artist')->columns_info,
     {
       artistid => {
         data_type => "INTEGER",
-        default_value => undef,
+        @undef_default,
         is_nullable => 0,
         size => undef
       },
       charfield => {
         data_type => "char",
-        default_value => undef,
+        @undef_default,
         is_nullable => 1,
         size => 10
       },
       name => {
         data_type => "varchar",
-        default_value => undef,
+        @undef_default,
         is_nullable => 1,
         is_numeric => 0,
         size => 100
@@ -420,7 +396,7 @@ lives_ok (sub { my $newlink = $newbook->link}, "stringify to false value doesn't
     {
       artistid => {
         data_type => "INTEGER",
-        default_value => undef,
+        @undef_default,
         is_nullable => 0,
         size => undef
       },
@@ -630,4 +606,6 @@ SKIP: {
 
 throws_ok { $schema->resultset} qr/resultset\(\) expects a source name/, 'resultset with no argument throws exception';
 
+throws_ok { $schema->source('Artist')->result_class->new( 'bugger' ) } qr/must be a hashref/;
+
 done_testing;
index b8b0d31..ab709e3 100644 (file)
@@ -47,11 +47,17 @@ ok(@cd && !defined($cd[0]), 'Array contains an undef as only element');
 
 $cd = $schema->resultset("CD")->first;
 my $artist_rs = $schema->resultset("Artist")->search({ artistid => $cd->artist->artistid });
-$art = $artist_rs->find({ name => 'some other name' }, { key => 'primary' });
-ok($art, 'Artist found by key in the resultset');
+for my $key ('', 'primary') {
+  my $art = $artist_rs->find({ name => 'some other name' }, { $key ? (key => $key) : () });
+  is($art->artistid, $cd->get_column('artist'), "Artist found through @{[ $key ? 'explicit' : 'implicit' ]} key locked in the resultset");
+}
 
 # collapsing and non-collapsing are separate codepaths, thus the separate tests
-
+my $ea_count = 0;
+$schema->exception_action(sub {
+  $ea_count++;
+  die @_;
+});
 
 $artist_rs = $schema->resultset("Artist");
 
@@ -77,6 +83,10 @@ for (1, 0) {
   ;
 }
 
+is( $ea_count, 1, "exception action invoked the expected amount of times (just the exception)" );
+
+$schema->exception_action(undef);
+
 
 $artist_rs = $schema->resultset("Artist")->search({}, { prefetch => 'cds' });
 
index d1284f8..1a0046d 100644 (file)
--- a/t/64db.t
+++ b/t/64db.t
@@ -62,7 +62,7 @@ is_deeply (
       'rank' => {
           'data_type' => 'integer',
           'is_nullable' => 0,
-          'default_value' => '13',
+          DBIx::Class::_ENV_::STRESSTEST_COLUMN_INFO_UNAWARE_STORAGE ? () : ( 'default_value' => '13' ),
       },
       'charfield' => {
           'data_type' => 'char',
index f768549..fa7c93a 100644 (file)
@@ -4,7 +4,6 @@ use warnings;
 use Test::More;
 use lib qw(t/lib);
 use DBICTest;
-use Storable qw/dclone/;
 
 my $schema = DBICTest->init_schema();
 
@@ -70,12 +69,14 @@ $it = $rs->search(
   { order_by => 'title',
     rows => 3 }
 );
-my $page = $it->page(2);
 
-is( $page->count, 2, "standard resultset paged rs count ok" );
+{
+  my $page = $it->page(2);
 
-is( $page->next->title, "Generic Manufactured Singles", "second page of standard resultset ok" );
+  is( $page->count, 2, "standard resultset paged rs count ok" );
 
+  is( $page->next->title, "Generic Manufactured Singles", "second page of standard resultset ok" );
+}
 
 # test software-based limit paging
 $it = $rs->search(
@@ -197,7 +198,11 @@ $it = $rs->search(
 $pager = $it->pager;
 is ($qcnt, 0, 'No queries on rs/pager creation');
 
-$it = do { local $DBIx::Class::ResultSourceHandle::thaw_schema = $schema; dclone ($it) };
+# test *requires* it to be Storable
+$it = do {
+  local $DBIx::Class::ResultSourceHandle::thaw_schema = $schema;
+  Storable::dclone ($it);
+};
 is ($qcnt, 0, 'No queries on rs/pager freeze/thaw');
 
 is( $it->pager->entries_on_this_page, 1, "entries_on_this_page ok for page 2" );
@@ -207,7 +212,11 @@ is ($qcnt, 1, 'Count fired to get pager page entries');
 $rs->create({ title => 'bah', artist => 1, year => 2011 });
 
 $qcnt = 0;
-$it = do { local $DBIx::Class::ResultSourceHandle::thaw_schema = $schema; dclone ($it) };
+# test *requires* it to be Storable
+$it = do {
+  local $DBIx::Class::ResultSourceHandle::thaw_schema = $schema;
+  Storable::dclone ($it);
+};
 is ($qcnt, 0, 'No queries on rs/pager freeze/thaw');
 
 is( $it->pager->entries_on_this_page, 1, "entries_on_this_page ok for page 2, even though underlying count changed" );
index 242989e..1b967de 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_mysql';
+
 use strict;
 use warnings;
 
@@ -5,22 +7,15 @@ use Test::More;
 use Test::Exception;
 use Test::Warn;
 
+use B::Deparse;
 use DBI::Const::GetInfoType;
 use Scalar::Util qw/weaken/;
-use DBIx::Class::Optional::Dependencies ();
 
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::SqlMakerTest;
-
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_mysql')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_mysql');
 
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MYSQL_${_}" } qw/DSN USER PASS/};
 
-plan skip_all => 'Set $ENV{DBICTEST_MYSQL_DSN}, _USER and _PASS to run this test'
-  unless ($dsn && $user);
-
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass, { quote_names => 1 });
 
 my $dbh = $schema->storage->dbh;
@@ -101,30 +96,35 @@ lives_ok {
   });
 } 'LOCK IN SHARE MODE select works';
 
+my ($int_type_name, @undef_default) = DBIx::Class::_ENV_::STRESSTEST_COLUMN_INFO_UNAWARE_STORAGE
+  ? ('integer')
+  : ( 'INT', default_value => undef )
+;
+
 my $test_type_info = {
     'artistid' => {
-        'data_type' => 'INT',
+        'data_type' => $int_type_name,
         'is_nullable' => 0,
         'size' => 11,
-        'default_value' => undef,
+        @undef_default,
     },
     'name' => {
         'data_type' => 'VARCHAR',
         'is_nullable' => 1,
         'size' => 100,
-        'default_value' => undef,
+        @undef_default,
     },
     'rank' => {
-        'data_type' => 'INT',
+        'data_type' => $int_type_name,
         'is_nullable' => 0,
         'size' => 11,
-        'default_value' => 13,
+        DBIx::Class::_ENV_::STRESSTEST_COLUMN_INFO_UNAWARE_STORAGE ? () : ( 'default_value' => '13' ),
     },
     'charfield' => {
         'data_type' => 'CHAR',
         'is_nullable' => 1,
         'size' => 10,
-        'default_value' => undef,
+        @undef_default,
     },
 };
 
@@ -179,6 +179,10 @@ SKIP: {
         $test_type_info->{charfield}->{data_type} = 'VARCHAR';
     }
 
+    if (DBIx::Class::_ENV_::STRESSTEST_COLUMN_INFO_UNAWARE_STORAGE) {
+      $_->{data_type} = lc $_->{data_type} for values %$test_type_info;
+    }
+
     my $type_info = $schema->storage->columns_info_for('artist');
     is_deeply($type_info, $test_type_info, 'columns_info_for - column data types');
 }
@@ -199,20 +203,6 @@ lives_ok { $cd->set_producers ([ $producer ]) } 'set_relationship doesnt die';
     my $cd = $rs->next;
     is ($cd->artist->name, $artist->name, 'Prefetched artist');
   }, 'join does not throw (mysql 3 test)';
-
-  # induce a jointype override, make sure it works even if we don't have mysql3
-  local $schema->storage->sql_maker->{_default_jointype} = 'inner';
-  is_same_sql_bind (
-    $rs->as_query,
-    '(
-      SELECT `me`.`cdid`, `me`.`artist`, `me`.`title`, `me`.`year`, `me`.`genreid`, `me`.`single_track`,
-             `artist`.`artistid`, `artist`.`name`, `artist`.`rank`, `artist`.`charfield`
-        FROM cd `me`
-        INNER JOIN `artist` `artist` ON `artist`.`artistid` = `me`.`artist`
-    )',
-    [],
-    'overridden default join type works',
-  );
 }
 
 ## Can we properly deal with the null search problem?
@@ -299,15 +289,9 @@ NULLINSEARCH: {
 
   is ($rs->count, 10, '10 artists present');
 
-  my $orig_debug = $schema->storage->debug;
-  $schema->storage->debug(1);
-  my $query_count;
-  $schema->storage->debugcb(sub { $query_count++ });
-
-  $query_count = 0;
-  $complex_rs->delete;
-
-  is ($query_count, 1, 'One delete query fired');
+  $schema->is_executed_querycount( sub {
+    $complex_rs->delete;
+  }, 1, 'One delete query fired' );
   is ($rs->count, 0, '10 Artists correctly deleted');
 
   $rs->create({
@@ -316,15 +300,13 @@ NULLINSEARCH: {
   });
   is ($rs->count, 1, 'Artist with cd created');
 
-  $query_count = 0;
-  $schema->resultset('CD')->search_related('artist',
-    { 'artist.name' => { -like => 'baby_with_%' } }
-  )->delete;
-  is ($query_count, 1, 'And one more delete query fired');
-  is ($rs->count, 0, 'Artist with cd deleted');
 
-  $schema->storage->debugcb(undef);
-  $schema->storage->debug($orig_debug);
+  $schema->is_executed_querycount( sub {
+    $schema->resultset('CD')->search_related('artist',
+      { 'artist.name' => { -like => 'baby_with_%' } }
+    )->delete;
+  }, 1, 'And one more delete query fired');
+  is ($rs->count, 0, 'Artist with cd deleted');
 }
 
 ZEROINSEARCH: {
@@ -375,8 +357,8 @@ ZEROINSEARCH: {
   ]});
 
   warnings_exist { is_deeply (
-    [ $restrict_rs->get_column('y')->all ],
-    [ $y_rs->all ],
+    [ sort $restrict_rs->get_column('y')->all ],
+    [ sort $y_rs->all ],
     'Zero year was correctly excluded from resultset',
   ) } qr/
     \QUse of distinct => 1 while selecting anything other than a column \E
@@ -470,4 +452,59 @@ ZEROINSEARCH: {
   ok ($rs->find({ name => "Hardcore Forker $pid" }), 'Expected row created');
 }
 
+# Ensure disappearing RDBMS does not leave the storage in an inconsistent state
+# Unlike the test in storage/reconnect.t we test live RDBMS-side disconnection
+SKIP:
+for my $cref (
+  sub {
+    my $schema = shift;
+
+    my $g = $schema->txn_scope_guard;
+
+    is( $schema->storage->transaction_depth, 1, "Expected txn depth" );
+
+    $schema->storage->_dbh->do("SELECT SLEEP(2)");
+  },
+  sub {
+    my $schema = shift;
+    $schema->txn_do(sub {
+      is( $schema->storage->transaction_depth, 1, "Expected txn depth" );
+      $schema->storage->_dbh->do("SELECT SLEEP(2)")
+    } );
+  },
+  sub {
+    my $schema = shift;
+
+    my $g = $schema->txn_scope_guard;
+
+    $schema->txn_do(sub {
+      is( $schema->storage->transaction_depth, 2, "Expected txn depth" );
+      $schema->storage->_dbh->do("SELECT SLEEP(2)")
+    } );
+  },
+) {
+  # version needed for the "read_timeout" feature
+  DBIx::Class::Optional::Dependencies->skip_without( 'DBD::mysql>=4.023' );
+
+  note( "Testing with " . B::Deparse->new->coderef2text($cref) );
+
+  my $schema = DBICTest::Schema->connect($dsn, $user, $pass, {
+    mysql_read_timeout => 1,
+  });
+
+  ok( !$schema->storage->connected, 'Not connected' );
+
+  is( $schema->storage->transaction_depth, undef, "Start with unknown txn depth" );
+
+  throws_ok {
+    $cref->($schema)
+  } qr/Rollback failed/;
+
+  ok( !$schema->storage->connected, 'Not connected as a result of failed rollback' );
+
+  is( $schema->storage->transaction_depth, undef, "Depth expectedly unknown after failed rollbacks" );
+
+  ok( $schema->resultset('Artist')->count, 'query works after the fact' );
+}
+
 done_testing;
index 44b723c..71213e8 100644 (file)
--- a/t/72pg.t
+++ b/t/72pg.t
@@ -1,57 +1,28 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_pg';
+
 use strict;
 use warnings;
 
 use Test::More;
 use Test::Exception;
+use Test::Warn;
 use Sub::Name;
+use Config;
 use DBIx::Class::Optional::Dependencies ();
 use lib qw(t/lib);
 use DBICTest;
-
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_pg')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_pg');
+use SQL::Abstract 'is_literal_value';
+use DBIx::Class::_Util 'is_exception';
 
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
 
-plan skip_all => <<'EOM' unless $dsn && $user;
-Set $ENV{DBICTEST_PG_DSN}, _USER and _PASS to run this test
-( NOTE: This test drops and creates tables called 'artist', 'cd',
-'timestamp_primary_key_test', 'track', 'casecheck', 'array_test' and
-'sequence_test' as well as following sequences: 'pkid1_seq', 'pkid2_seq' and
-'nonpkid_seq'. as well as following schemas: 'dbic_t_schema',
-'dbic_t_schema_2', 'dbic_t_schema_3', 'dbic_t_schema_4', and 'dbic_t_schema_5')
-EOM
-
 ### load any test classes that are defined further down in the file via BEGIN blocks
-
 our @test_classes; #< array that will be pushed into by test classes defined in this file
 DBICTest::Schema->load_classes( map {s/.+:://;$_} @test_classes ) if @test_classes;
 
 ###  pre-connect tests (keep each test separate as to make sure rebless() runs)
   {
     my $s = DBICTest::Schema->connect($dsn, $user, $pass);
-
-    ok (!$s->storage->_dbh, 'definitely not connected');
-
-    # Check that datetime_parser returns correctly before we explicitly connect.
-    SKIP: {
-        skip (
-          "Pg parser detection test needs " . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt_pg'),
-          2
-        ) unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt_pg');
-
-        my $store = ref $s->storage;
-        is($store, 'DBIx::Class::Storage::DBI', 'Started with generic storage');
-
-        my $parser = $s->storage->datetime_parser;
-        is( $parser, 'DateTime::Format::Pg', 'datetime_parser is as expected');
-    }
-
-    ok (!$s->storage->_dbh, 'still not connected');
-  }
-
-  {
-    my $s = DBICTest::Schema->connect($dsn, $user, $pass);
     # make sure sqlt_type overrides work (::Storage::DBI::Pg does this)
     ok (!$s->storage->_dbh, 'definitely not connected');
     is ($s->storage->sqlt_type, 'PostgreSQL', 'sqlt_type correct pre-connection');
@@ -146,6 +117,16 @@ for my $use_insert_returning ($test_server_supports_insert_returning
   run_apk_tests($schema); #< older set of auto-pk tests
   run_extended_apk_tests($schema); #< new extended set of auto-pk tests
 
+
+######## test the pg-specific syntax from https://rt.cpan.org/Ticket/Display.html?id=99503
+  lives_ok {
+    is(
+      $schema->resultset('Artist')->search({ artistid => { -in => \ '(select 4) union (select 5)' } })->count,
+      2,
+      'Two expected artists found on subselect union within IN',
+    );
+  };
+
 ### type_info tests
 
   my $test_type_info = {
@@ -183,14 +164,19 @@ for my $use_insert_returning ($test_server_supports_insert_returning
 
   my $type_info = $schema->storage->columns_info_for('dbic_t_schema.artist');
   my $artistid_defval = delete $type_info->{artistid}->{default_value};
-  like($artistid_defval,
-       qr/^nextval\('([^\.]*\.){0,1}artist_artistid_seq'::(?:text|regclass)\)/,
-       'columns_info_for - sequence matches Pg get_autoinc_seq expectations');
-  is_deeply($type_info, $test_type_info,
-            'columns_info_for - column data types');
-
 
+  # The curor info is too radically different from what is in the column_info
+  # call - just punt it (DBD::SQLite tests the codepath plenty enough)
+  unless (DBIx::Class::_ENV_::STRESSTEST_COLUMN_INFO_UNAWARE_STORAGE) {
+    like(
+      $artistid_defval,
+      qr/^nextval\('([^\.]*\.){0,1}artist_artistid_seq'::(?:text|regclass)\)/,
+      'columns_info_for - sequence matches Pg get_autoinc_seq expectations'
+    );
 
+    is_deeply($type_info, $test_type_info,
+            'columns_info_for - column data types');
+  }
 
 ####### Array tests
 
@@ -248,7 +234,7 @@ for my $use_insert_returning ($test_server_supports_insert_returning
     lives_ok {
       is_deeply (
         $arr_rs->search({ arrayfield => { '=' => { -value => [3,4] }} })->first->arrayfield,
-        [3,4],,
+        [3,4],
         'Array value matches explicit equal'
       );
     } 'searching by arrayref (explicit equal sign)';
@@ -289,9 +275,12 @@ for my $use_insert_returning ($test_server_supports_insert_returning
     # test inferred condition for creation
     for my $cond (
       { -value => [3,4] },
-      \[ '= ?' => [arrayfield => [3, 4]] ],
+      \[ '= ?' => [3, 4] ],
     ) {
-      local $TODO = 'No introspection of complex conditions :(';
+      local $TODO = 'No introspection of complex literal conditions :('
+        if is_literal_value $cond;
+
+
       my $arr_rs_cond = $arr_rs->search({ arrayfield => $cond });
 
       my $row = $arr_rs_cond->create({});
@@ -299,6 +288,49 @@ for my $use_insert_returning ($test_server_supports_insert_returning
       $row->discard_changes;
       is_deeply ($row->arrayfield, [3,4], 'Array value made it to storage');
     }
+
+    my $arr = [ 1..10 ];
+    # exercise the creation-logic even more (akin to t/100populate.t)
+    for my $insert_value (
+      $arr,
+      { -value => $arr },
+      \[ '?', $arr ],
+    ) {
+      $arr_rs->delete;
+
+      my @objs = (
+        $arr_rs->create({ arrayfield => $insert_value }),
+        $arr_rs->populate([ { arrayfield => $insert_value } ]),
+        $arr_rs->populate([ ['arrayfield'], [ $insert_value ] ]),
+      );
+
+      my $loose_obj = $arr_rs->new({ arrayfield => $insert_value });
+
+      unless (is_literal_value $insert_value) {
+        is_deeply( $_->arrayfield, $arr, 'array value preserved during set_columns' )
+          for ($loose_obj, @objs)
+      }
+
+      push @objs, $loose_obj->insert;
+
+      $_->discard_changes for @objs;
+      is_deeply( $_->arrayfield, $arr, 'array value correct after discard_changes' )
+        for (@objs);
+
+      # insert couple more in void ctx
+      $arr_rs->populate([ { arrayfield => $insert_value } ]);
+      $arr_rs->populate([ ['arrayfield'], [ $insert_value ] ]);
+
+      # should have a total of 6 now, all pristine
+      my @retrieved_objs = $arr_rs->search({
+        arrayfield => ref $insert_value eq 'ARRAY'
+          ? { -value => $insert_value }
+          : { '=' => $insert_value }
+      })->all;
+      is scalar @retrieved_objs, 6, 'Correct count of inserted rows';
+      is_deeply( $_->arrayfield, $arr, 'array value correct after storage retrieval' )
+        for (@retrieved_objs);
+    }
   }
 
 ########## Case check
@@ -335,14 +367,9 @@ my $cds = $artist->cds_unordered->search({
 lives_ok { $cds->update({ year => '2010' }) } 'Update on prefetched rs';
 
 ## Test SELECT ... FOR UPDATE
-
   SKIP: {
-      if(eval { require Sys::SigAction }) {
-          Sys::SigAction->import( 'set_sig_handler' );
-      }
-      else {
-        skip "Sys::SigAction is not available", 6;
-      }
+      skip "Your system does not support unsafe signals (d_sigaction) - unable to run deadlock test", 1
+        unless eval { $Config{d_sigaction} and require POSIX };
 
       my ($timed_out, $artist2);
 
@@ -381,15 +408,34 @@ lives_ok { $cds->update({ year => '2010' }) } 'Update on prefetched rs';
           is($artist->artistid, 1, "select returns artistid = 1");
 
           $timed_out = 0;
+
           eval {
-              my $h = set_sig_handler( 'ALRM', sub { die "DBICTestTimeout" } );
-              alarm(2);
+              # can not use %SIG assignment directly - we need sigaction below
+              # localization to a block still works however
+              local $SIG{ALRM};
+
+              POSIX::sigaction( POSIX::SIGALRM() => POSIX::SigAction->new(
+                sub { die "DBICTestTimeout" },
+              ));
+
               $artist2 = $schema2->resultset('Artist')->find(1);
               $artist2->name('fooey');
+
+              # FIXME - this needs to go away in lieu of a non-retrying runner
+              # ( i.e. after solving RT#47005 )
+              local *DBIx::Class::Storage::DBI::_ping = sub { 1 }, DBIx::Class::_ENV_::OLD_MRO && Class::C3->reinitialize()
+                if DBIx::Class::_Util::modver_gt_or_eq( 'DBD::Pg' => '3.5.0' );
+
+              alarm(1);
               $artist2->update;
-              alarm(0);
           };
-          $timed_out = $@ =~ /DBICTestTimeout/;
+
+          alarm(0);
+
+          if (is_exception($@)) {
+            $timed_out = $@ =~ /DBICTestTimeout/
+              or die $@;
+          }
         });
 
         $t->{test_sub}->();
@@ -440,7 +486,24 @@ lives_ok { $cds->update({ year => '2010' }) } 'Update on prefetched rs';
     $schema->resultset('Track')->create({
       trackid => 1, cd => 9999, position => 1, title => 'Track1'
     });
-  } qr/constraint/i, 'with_deferred_fk_checks is off';
+  } qr/violates foreign key constraint/i, 'with_deferred_fk_checks is off outside of TXN';
+
+  # rerun the same under with_deferred_fk_checks
+  # it is expected to fail, hence the eval
+  # but it also should not warn
+  warnings_like {
+    eval {
+      $schema->storage->with_deferred_fk_checks(sub {
+        $schema->resultset('Track')->create({
+          trackid => 1, cd => 9999, position => 1, title => 'Track1'
+        });
+      } )
+    };
+
+    like $@, qr/violates foreign key constraint/i,
+      "Still expected exception on deferred failure at commit time";
+
+  } [], 'No warnings on deferred rollback';
 }
 
 done_testing;
index ac5b9c4..7049b31 100644 (file)
@@ -1,32 +1,23 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw(test_rdbms_pg binary_data);
+
 use strict;
 use warnings;
 
 use Test::More;
-use DBIx::Class::Optional::Dependencies ();
-use Try::Tiny;
+use DBIx::Class::_Util 'modver_gt_or_eq';
+
 use lib qw(t/lib);
 use DBICTest;
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('rdbms_pg')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('rdbms_pg');
-
 my ($dsn, $dbuser, $dbpass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
 
-plan skip_all => 'Set $ENV{DBICTEST_PG_DSN}, _USER and _PASS to run this test'
-  unless ($dsn && $dbuser);
-
 my $schema = DBICTest::Schema->connect($dsn, $dbuser, $dbpass, { AutoCommit => 1 });
 
-if ($schema->storage->_server_info->{normalized_dbms_version} >= 9.0) {
-  if (not try { DBD::Pg->VERSION('2.17.2') }) {
-    plan skip_all =>
-      'DBD::Pg < 2.17.2 does not work with Pg >= 9.0 BYTEA columns';
-  }
-}
-elsif (not try { DBD::Pg->VERSION('2.9.2') }) {
-  plan skip_all =>
-    'DBD::Pg < 2.9.2 does not work with BYTEA columns';
-}
+plan skip_all => 'DBD::Pg < 2.17.2 does not work with Pg >= 9.0 BYTEA columns' if (
+  ! modver_gt_or_eq('DBD::Pg', '2.17.2')
+    and
+  $schema->storage->_server_info->{normalized_dbms_version} >= 9.0
+);
 
 my $dbh = $schema->storage->dbh;
 
index 40dcaac..c6211e2 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_oracle';
+
 use strict;
 use warnings;
 
@@ -5,27 +7,19 @@ use Test::Exception;
 use Test::More;
 use Sub::Name;
 use Try::Tiny;
-use DBIx::Class::Optional::Dependencies ();
 
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::SqlMakerTest;
+
+$ENV{NLS_SORT} = "BINARY";
+$ENV{NLS_COMP} = "BINARY";
+$ENV{NLS_LANG} = "AMERICAN";
 
 my ($dsn,  $user,  $pass)  = @ENV{map { "DBICTEST_ORA_${_}" }  qw/DSN USER PASS/};
 
 # optional:
 my ($dsn2, $user2, $pass2) = @ENV{map { "DBICTEST_ORA_EXTRAUSER_${_}" } qw/DSN USER PASS/};
 
-plan skip_all => 'Set $ENV{DBICTEST_ORA_DSN}, _USER and _PASS to run this test.'
-  unless ($dsn && $user && $pass);
-
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_oracle')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_oracle');
-
-$ENV{NLS_SORT} = "BINARY";
-$ENV{NLS_COMP} = "BINARY";
-$ENV{NLS_LANG} = "AMERICAN";
-
 {
   package    # hide from PAUSE
     DBICTest::Schema::ArtistFQN;
index ae5a359..0391d4b 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_oracle';
+
 use strict;
 use warnings;
 
@@ -5,24 +7,31 @@ use Test::Exception;
 use Test::More;
 use Sub::Name;
 use Try::Tiny;
-use DBIx::Class::Optional::Dependencies ();
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
-
-my ($dsn,  $user,  $pass)  = @ENV{map { "DBICTEST_ORA_${_}" }  qw/DSN USER PASS/};
 
-plan skip_all => 'Set $ENV{DBICTEST_ORA_DSN}, _USER and _PASS to run this test.'
-  unless ($dsn && $user && $pass);
+use DBICTest::Schema::BindType;
+BEGIN {
+  DBICTest::Schema::BindType->add_columns(
+    'blb2' => {
+      data_type => 'blob',
+      is_nullable => 1,
+    },
+    'clb2' => {
+      data_type => 'clob',
+      is_nullable => 1,
+    }
+  );
+}
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_oracle')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_oracle');
+use DBICTest;
 
 $ENV{NLS_SORT} = "BINARY";
 $ENV{NLS_COMP} = "BINARY";
 $ENV{NLS_LANG} = "AMERICAN";
 
+my ($dsn,  $user,  $pass)  = @ENV{map { "DBICTEST_ORA_${_}" }  qw/DSN USER PASS/};
+
 my $v = do {
   my $si = DBICTest::Schema->connect($dsn, $user, $pass)->storage->_server_info;
   $si->{normalized_dbms_version}
@@ -78,21 +87,16 @@ SKIP: {
               . ': https://rt.cpan.org/Ticket/Display.html?id=64206'
     if $q;
 
-  # so we can disable BLOB mega-output
-  my $orig_debug = $schema->storage->debug;
-
   my $id;
   foreach my $size (qw( small large )) {
     $id++;
 
-    local $schema->storage->{debug} = $size eq 'large'
-      ? 0
-      : $orig_debug
-    ;
+    local $schema->storage->{debug} = 0
+      if $size eq 'large';
 
     my $str = $binstr{$size};
     lives_ok {
-      $rs->create( { 'id' => $id, blob => "blob:$str", clob => "clob:$str" } )
+      $rs->create( { 'id' => $id, blob => "blob:$str", clob => "clob:$str", blb2 => "blb2:$str", clb2 => "clb2:$str" } )
     } "inserted $size without dying";
 
     my %kids = %{$schema->storage->_dbh->{CachedKids}};
@@ -105,6 +109,8 @@ SKIP: {
     is @objs, 1, 'One row found matching on both LOBs';
     ok (try { $objs[0]->blob }||'' eq "blob:$str", 'blob inserted/retrieved correctly');
     ok (try { $objs[0]->clob }||'' eq "clob:$str", 'clob inserted/retrieved correctly');
+    ok (try { $objs[0]->clb2 }||'' eq "clb2:$str", "clb2 inserted correctly");
+    ok (try { $objs[0]->blb2 }||'' eq "blb2:$str", "blb2 inserted correctly");
 
     {
       local $TODO = '-like comparison on blobs not tested before ora 10 (fails on 8i)'
@@ -120,7 +126,7 @@ SKIP: {
       { blob => "blob:$str", clob => "clob:$str" },
       {
         from => \ "(SELECT * FROM ${q}bindtype_test${q} WHERE ${q}id${q} != ?) ${q}me${q}",
-        bind => [ [ undef => 12345678 ] ],
+        bind => [ [ {} => 12345678 ] ],
       }
     )->get_column('id')->as_query);
 
@@ -129,13 +135,15 @@ SKIP: {
 
     lives_ok {
       $rs->search({ id => $id, blob => "blob:$str", clob => "clob:$str" })
-        ->update({ blob => 'updated blob', clob => 'updated clob' });
+        ->update({ blob => 'updated blob', clob => 'updated clob', clb2 => 'updated clb2', blb2 => 'updated blb2' });
     } 'blob UPDATE with blobs in WHERE clause survived';
 
     @objs = $rs->search({ blob => "updated blob", clob => 'updated clob' })->all;
     is @objs, 1, 'found updated row';
     ok (try { $objs[0]->blob }||'' eq "updated blob", 'blob updated/retrieved correctly');
     ok (try { $objs[0]->clob }||'' eq "updated clob", 'clob updated/retrieved correctly');
+    ok (try { $objs[0]->clb2 }||'' eq "updated clb2", "clb2 updated correctly");
+    ok (try { $objs[0]->blb2 }||'' eq "updated blb2", "blb2 updated correctly");
 
     lives_ok {
       $rs->search({ id => $id  })
@@ -154,8 +162,6 @@ SKIP: {
     @objs = $rs->search({ blob => "re-updated blob", clob => 're-updated clob' })->all;
     is @objs, 0, 'row deleted successfully';
   }
-
-  $schema->storage->debug ($orig_debug);
 }
 
   do_clean ($dbh);
@@ -168,7 +174,7 @@ sub do_creates {
 
   do_clean($dbh);
 
-  $dbh->do("CREATE TABLE ${q}bindtype_test${q} (${q}id${q} integer NOT NULL PRIMARY KEY, ${q}bytea${q} integer NULL, ${q}blob${q} blob NULL, ${q}blob2${q} blob NULL, ${q}clob${q} clob NULL, ${q}clob2${q} clob NULL, ${q}a_memo${q} integer NULL)");
+  $dbh->do("CREATE TABLE ${q}bindtype_test${q} (${q}id${q} integer NOT NULL PRIMARY KEY, ${q}bytea${q} integer NULL, ${q}blob${q} blob NULL, ${q}blb2${q} blob NULL, ${q}clob${q} clob NULL, ${q}clb2${q} clob NULL, ${q}a_memo${q} integer NULL)");
 }
 
 # clean up our mess
index 0f887fa..57bdc2b 100644 (file)
@@ -1,23 +1,16 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_oracle';
+
 use strict;
 use warnings;
 
 use Test::Exception;
 use Test::More;
-use DBIx::Class::Optional::Dependencies ();
-use lib qw(t/lib);
-use DBICTest::RunMode;
-
-$ENV{NLS_SORT} = "BINARY";
-$ENV{NLS_COMP} = "BINARY";
-$ENV{NLS_LANG} = "AMERICAN";
-
-my ($dsn,  $user,  $pass)  = @ENV{map { "DBICTEST_ORA_${_}" }  qw/DSN USER PASS/};
 
-plan skip_all => 'Set $ENV{DBICTEST_ORA_DSN}, _USER and _PASS to run this test.'
- unless ($dsn && $user && $pass);
+# I *strongly* suspect Oracle has an implicit stable output order when
+# dealing with HQs. So just punt on the entire shuffle thing.
+BEGIN { $ENV{DBIC_SHUFFLE_UNORDERED_RESULTSETS} = 0 }
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('rdbms_oracle')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('rdbms_oracle');
+use lib qw(t/lib);
 
 use DBICTest::Schema::Artist;
 BEGIN {
@@ -37,6 +30,11 @@ BEGIN {
 use DBICTest;
 use DBICTest::Schema;
 
+$ENV{NLS_SORT} = "BINARY";
+$ENV{NLS_COMP} = "BINARY";
+$ENV{NLS_LANG} = "AMERICAN";
+
+my ($dsn,  $user,  $pass)  = @ENV{map { "DBICTEST_ORA_${_}" }  qw/DSN USER PASS/};
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
 
 note "Oracle Version: " . $schema->storage->_server_info->{dbms_version};
index 9123330..17a6343 100644 (file)
@@ -1,23 +1,15 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_db2';
+
 use strict;
 use warnings;
 
 use Test::More;
 use Test::Exception;
 use Try::Tiny;
-use DBIx::Class::Optional::Dependencies ();
 use lib qw(t/lib);
 use DBICTest;
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_db2')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_db2');
-
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_DB2_${_}" } qw/DSN USER PASS/};
-
-#warn "$dsn $user $pass";
-
-plan skip_all => 'Set $ENV{DBICTEST_DB2_DSN}, _USER and _PASS to run this test'
-  unless ($dsn && $user);
-
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
 
 my $name_sep = $schema->storage->_dbh_get_info('SQL_QUALIFIER_NAME_SEPARATOR');
index 3a5d902..b6c4350 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_db2_400';
+
 use strict;
 use warnings;
 
@@ -6,18 +8,10 @@ use DBIx::Class::Optional::Dependencies ();
 use lib qw(t/lib);
 use DBICTest;
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_db2_400')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_db2_400');
-
-my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_DB2_400_${_}" } qw/DSN USER PASS/};
-
-#warn "$dsn $user $pass";
-
 # Probably best to pass the DBQ option in the DSN to specify a specific
 # libray.  Something like:
 # DBICTEST_DB2_400_DSN='dbi:ODBC:dsn=MyAS400;DBQ=MYLIB'
-plan skip_all => 'Set $ENV{DBICTEST_DB2_400_DSN}, _USER and _PASS to run this test'
-  unless ($dsn && $user);
+my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_DB2_400_${_}" } qw/DSN USER PASS/};
 
 plan tests => 6;
 
index 2cc0281..23778a4 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_mssql_odbc';
+
 use strict;
 use warnings;
 
@@ -5,18 +7,11 @@ use Test::More;
 use Test::Exception;
 use Try::Tiny;
 
-use DBIx::Class::Optional::Dependencies ();
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_mssql_odbc')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_mssql_odbc');
-
 use lib qw(t/lib);
 use DBICTest;
 
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MSSQL_ODBC_${_}" } qw/DSN USER PASS/};
 
-plan skip_all => 'Set $ENV{DBICTEST_MSSQL_ODBC_DSN}, _USER and _PASS to run this test'
-  unless ($dsn && $user);
-
 {
   my $srv_ver = DBICTest::Schema->connect($dsn, $user, $pass)->storage->_server_info->{dbms_version};
   ok ($srv_ver, 'Got a test server version on fresh schema: ' . ($srv_ver||'???') );
@@ -280,8 +275,8 @@ SQL
           my $sealed_owners = $owners->as_subselect_rs;
 
           is_deeply (
-            [ map { $_->name } ($sealed_owners->all) ],
-            [ map { $_->name } ($owners->all) ],
+            [ sort map { $_->name } ($sealed_owners->all) ],
+            [ sort map { $_->name } ($owners->all) ],
             "$test_type: Sort preserved from within a subquery",
           );
         }
@@ -331,20 +326,13 @@ SQL
           is ($limited_rs->count, 6, "$test_type: Correct count of limited right-sorted joined resultset");
           is ($limited_rs->count_rs->next, 6, "$test_type: Correct count_rs of limited right-sorted joined resultset");
 
-          my $queries;
-          my $orig_debug = $schema->storage->debug;
-          $schema->storage->debugcb(sub { $queries++; });
-          $schema->storage->debug(1);
-
-          is_deeply (
-            [map { $_->owner->name } ($limited_rs->all) ],
-            [@owner_names[2 .. 7]],
-            "$test_type: Prefetch-limited rows were properly ordered"
-          );
-          is ($queries, 1, "$test_type: Only one query with prefetch");
-
-          $schema->storage->debugcb(undef);
-          $schema->storage->debug($orig_debug);
+          $schema->is_executed_querycount( sub {
+            is_deeply (
+              [map { $_->owner->name } ($limited_rs->all) ],
+              [@owner_names[2 .. 7]],
+              "$test_type: Prefetch-limited rows were properly ordered"
+            );
+          }, 1, "$test_type: Only one query with prefetch" );
 
           is_deeply (
             [map { $_->name } ($limited_rs->search_related ('owner')->all) ],
index cb6849a..0b8406c 100644 (file)
@@ -1,33 +1,24 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_ase';
+
 use strict;
 use warnings;
 no warnings 'uninitialized';
 
 use Test::More;
 use Test::Exception;
-use DBIx::Class::Optional::Dependencies ();
+use DBIx::Class::_Util 'sigwarn_silencer';
+
 use lib qw(t/lib);
 use DBICTest;
 
-my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_SYBASE_${_}" } qw/DSN USER PASS/};
-if (not ($dsn && $user)) {
-  plan skip_all => join ' ',
-    'Set $ENV{DBICTEST_SYBASE_DSN}, _USER and _PASS to run this test.',
-    'Warning: This test drops and creates the tables:',
-    "'artist', 'money_test' and 'bindtype_test'",
-  ;
-};
-
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_ase')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_ase');
-
 my @storage_types = (
   'DBI::Sybase::ASE',
   'DBI::Sybase::ASE::NoBindVars',
 );
-eval "require DBIx::Class::Storage::$_;" for @storage_types;
 
 my $schema;
-my $storage_idx = -1;
+
+my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_SYBASE_${_}" } qw/DSN USER PASS/};
 
 sub get_schema {
   DBICTest::Schema->connect($dsn, $user, $pass, {
@@ -39,6 +30,7 @@ sub get_schema {
 
 my $ping_count = 0;
 {
+  require DBIx::Class::Storage::DBI::Sybase::ASE;
   my $ping = DBIx::Class::Storage::DBI::Sybase::ASE->can('_ping');
   *DBIx::Class::Storage::DBI::Sybase::ASE::_ping = sub {
     $ping_count++;
@@ -47,7 +39,6 @@ my $ping_count = 0;
 }
 
 for my $storage_type (@storage_types) {
-  $storage_idx++;
 
   unless ($storage_type eq 'DBI::Sybase::ASE') { # autodetect
     DBICTest::Schema->storage_type("::$storage_type");
@@ -57,12 +48,12 @@ for my $storage_type (@storage_types) {
 
   $schema->storage->ensure_connected;
 
-  if ($storage_idx == 0 &&
-      $schema->storage->isa('DBIx::Class::Storage::DBI::Sybase::ASE::NoBindVars')) {
-      # no placeholders in this version of Sybase or DBD::Sybase (or using FreeTDS)
-      skip "Skipping entire test for $storage_type - no placeholder support", 1;
-      next;
-  }
+  # we are going to explicitly test this anyway, just loop through
+  next if
+    $storage_type ne 'DBI::Sybase::ASE::NoBindVars'
+      and
+    $schema->storage->isa('DBIx::Class::Storage::DBI::Sybase::ASE::NoBindVars')
+  ;
 
   isa_ok( $schema->storage, "DBIx::Class::Storage::$storage_type" );
 
@@ -207,12 +198,16 @@ SQL
     name => { -like => 'bulk artist %' }
   });
 
-# test insert_bulk using populate.
+# test _insert_bulk using populate.
   SKIP: {
-    skip 'insert_bulk not supported', 4
+    skip '_insert_bulk not supported', 4
       unless $storage_type !~ /NoBindVars/i;
 
     lives_ok {
+
+      local $SIG{__WARN__} = sigwarn_silencer(qr/Sybase bulk API operation failed due to character set incompatibility/)
+        unless $ENV{DBICTEST_SYBASE_SUBTEST_RERUN};
+
       $schema->resultset('Artist')->populate([
         {
           name => 'bulk artist 1',
@@ -227,25 +222,25 @@ SQL
           charfield => 'foo',
         },
       ]);
-    } 'insert_bulk via populate';
+    } '_insert_bulk via populate';
 
-    is $bulk_rs->count, 3, 'correct number inserted via insert_bulk';
+    is $bulk_rs->count, 3, 'correct number inserted via _insert_bulk';
 
     is ((grep $_->charfield eq 'foo', $bulk_rs->all), 3,
-      'column set correctly via insert_bulk');
+      'column set correctly via _insert_bulk');
 
     my %bulk_ids;
     @bulk_ids{map $_->artistid, $bulk_rs->all} = ();
 
     is ((scalar keys %bulk_ids), 3,
-      'identities generated correctly in insert_bulk');
+      'identities generated correctly in _insert_bulk');
 
     $bulk_rs->delete;
   }
 
-# make sure insert_bulk works a second time on the same connection
+# make sure _insert_bulk works a second time on the same connection
   SKIP: {
-    skip 'insert_bulk not supported', 3
+    skip '_insert_bulk not supported', 3
       unless $storage_type !~ /NoBindVars/i;
 
     lives_ok {
@@ -263,36 +258,44 @@ SQL
           charfield => 'bar',
         },
       ]);
-    } 'insert_bulk via populate called a second time';
+    } '_insert_bulk via populate called a second time';
 
     is $bulk_rs->count, 3,
-      'correct number inserted via insert_bulk';
+      'correct number inserted via _insert_bulk';
 
     is ((grep $_->charfield eq 'bar', $bulk_rs->all), 3,
-      'column set correctly via insert_bulk');
+      'column set correctly via _insert_bulk');
 
     $bulk_rs->delete;
   }
 
-# test invalid insert_bulk (missing required column)
+# test invalid _insert_bulk (missing required column)
 #
-# There should be a rollback, reconnect and the next valid insert_bulk should
-# succeed.
   throws_ok {
+    local $SIG{__WARN__} = sigwarn_silencer(qr/Sybase bulk API operation failed due to character set incompatibility/)
+      unless $ENV{DBICTEST_SYBASE_SUBTEST_RERUN};
+
     $schema->resultset('Artist')->populate([
       {
         charfield => 'foo',
       }
     ]);
-  } qr/no value or default|does not allow null|placeholders/i,
+  }
 # The second pattern is the error from fallback to regular array insert on
 # incompatible charset.
 # The third is for ::NoBindVars with no syb_has_blk.
-  'insert_bulk with missing required column throws error';
-
-# now test insert_bulk with IDENTITY_INSERT
+  qr/
+    \Qno value or default\E
+      |
+    \Qdoes not allow null\E
+      |
+    \QUnable to invoke fast-path insert without storage placeholder support\E
+  /xi,
+  '_insert_bulk with missing required column throws error';
+
+# now test _insert_bulk with IDENTITY_INSERT
   SKIP: {
-    skip 'insert_bulk not supported', 3
+    skip '_insert_bulk not supported', 3
       unless $storage_type !~ /NoBindVars/i;
 
     lives_ok {
@@ -313,13 +316,13 @@ SQL
           charfield => 'foo',
         },
       ]);
-    } 'insert_bulk with IDENTITY_INSERT via populate';
+    } '_insert_bulk with IDENTITY_INSERT via populate';
 
     is $bulk_rs->count, 3,
-      'correct number inserted via insert_bulk with IDENTITY_INSERT';
+      'correct number inserted via _insert_bulk with IDENTITY_INSERT';
 
     is ((grep $_->charfield eq 'foo', $bulk_rs->all), 3,
-      'column set correctly via insert_bulk with IDENTITY_INSERT');
+      'column set correctly via _insert_bulk with IDENTITY_INSERT');
 
     $bulk_rs->delete;
   }
@@ -434,7 +437,7 @@ SQL
 
     $rs->delete;
 
-    # now try insert_bulk with blobs and only blobs
+    # now try _insert_bulk with blobs and only blobs
     $new_str = $binstr{large} . 'bar';
     lives_ok {
       $rs->populate([
@@ -447,18 +450,18 @@ SQL
           clob => $new_str,
         },
       ]);
-    } 'insert_bulk with blobs does not die';
+    } '_insert_bulk with blobs does not die';
 
     is((grep $_->blob eq $binstr{large}, $rs->all), 2,
-      'IMAGE column set correctly via insert_bulk');
+      'IMAGE column set correctly via _insert_bulk');
 
     is((grep $_->clob eq $new_str, $rs->all), 2,
-      'TEXT column set correctly via insert_bulk');
+      'TEXT column set correctly via _insert_bulk');
 
-    # now try insert_bulk with blobs and a non-blob which also happens to be an
+    # now try _insert_bulk with blobs and a non-blob which also happens to be an
     # identity column
     SKIP: {
-      skip 'no insert_bulk without placeholders', 4
+      skip 'no _insert_bulk without placeholders', 4
         if $storage_type =~ /NoBindVars/i;
 
       $rs->delete;
@@ -480,16 +483,16 @@ SQL
             a_memo => 2,
           },
         ]);
-      } 'insert_bulk with blobs and explicit identity does NOT die';
+      } '_insert_bulk with blobs and explicit identity does NOT die';
 
       is((grep $_->blob eq $binstr{large}, $rs->all), 2,
-        'IMAGE column set correctly via insert_bulk with identity');
+        'IMAGE column set correctly via _insert_bulk with identity');
 
       is((grep $_->clob eq $new_str, $rs->all), 2,
-        'TEXT column set correctly via insert_bulk with identity');
+        'TEXT column set correctly via _insert_bulk with identity');
 
       is_deeply [ map $_->id, $rs->all ], [ 1,2 ],
-        'explicit identities set correctly via insert_bulk with blobs';
+        'explicit identities set correctly via _insert_bulk with blobs';
     }
 
     lives_and {
@@ -498,6 +501,13 @@ SQL
       $rs->update({ blob => undef });
       is((grep !defined($_->blob), $rs->all), 2);
     } 'blob update to NULL';
+
+    lives_ok {
+      $schema->txn_do(sub {
+        my $created = $rs->create( { clob => "some text" } );
+      });
+    } 'insert blob field in transaction';
+    $ping_count-- if $@; # failure retry triggers a ping
   }
 
 # test MONEY column support (and some other misc. stuff)
@@ -608,12 +618,13 @@ SQL
 
 is $ping_count, 0, 'no pings';
 
-# if tests passed and did so under a non-C lang - let's rerun the test
-if (Test::Builder->new->is_passing and $ENV{LANG} and $ENV{LANG} ne 'C') {
-  my $oldlang = $ENV{LANG};
-  local $ENV{LANG} = 'C';
+# if tests passed and did so under a non-C LC_ALL - let's rerun the test
+if (Test::Builder->new->is_passing and $ENV{LC_ALL} and $ENV{LC_ALL} ne 'C') {
+
+  pass ("Your LC_ALL is set to $ENV{LC_ALL} - retesting with C");
 
-  pass ("Your lang is set to $oldlang - retesting with C");
+  local $ENV{LC_ALL} = 'C';
+  local $ENV{DBICTEST_SYBASE_SUBTEST_RERUN} = 1;
 
   local $ENV{PATH};
   my @cmd = map { $_ =~ /(.+)/ } ($^X, __FILE__);
index 77a88dc..9c1d084 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_mssql_ado';
+
 use strict;
 use warnings;
 
@@ -8,17 +10,10 @@ use DBIx::Class::Optional::Dependencies ();
 use lib qw(t/lib);
 use DBICTest;
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_mssql_ado')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_mssql_ado');
-
 # Example DSN (from frew):
 # dbi:ADO:PROVIDER=sqlncli10;SERVER=tcp:172.24.2.10;MARS Connection=True;Initial Catalog=CIS;UID=cis_web;PWD=...;DataTypeCompatibility=80;
-
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MSSQL_ADO_${_}" } qw/DSN USER PASS/};
 
-plan skip_all => 'Set $ENV{DBICTEST_MSSQL_ADO_DSN}, _USER and _PASS to run this test'
-  unless ($dsn && $user);
-
 DBICTest::Schema->load_classes(qw/VaryingMAX ArtistGUID/);
 
 my %binstr = ( 'small' => join('', map { chr($_) } ( 1 .. 127 )) );
@@ -223,9 +218,7 @@ is $row->artistid, $current_artistid+1,
 my $rs = $schema->resultset('VaryingMAX');
 
 foreach my $size (qw/small large/) {
-  my $orig_debug = $schema->storage->debug;
-
-  $schema->storage->debug(0) if $size eq 'large';
+  local $schema->storage->{debug} = 0 if $size eq 'large';
 
   my $str = $binstr{$size};
   my $row;
@@ -242,8 +235,6 @@ foreach my $size (qw/small large/) {
   cmp_ok try { $row->varchar_max },   'eq', $str, 'VARCHAR(MAX) matches';
   cmp_ok try { $row->nvarchar_max },  'eq', $str, 'NVARCHAR(MAX) matches';
   cmp_ok try { $row->varbinary_max }, 'eq', $str, 'VARBINARY(MAX) matches';
-
-  $schema->storage->debug($orig_debug);
 }
 
 # test regular blobs
index 42bdac8..08fc4b5 100644 (file)
@@ -1,22 +1,15 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_informix';
+
 use strict;
 use warnings;
 
 use Test::More;
 use Test::Exception;
-use DBIx::Class::Optional::Dependencies ();
 use lib qw(t/lib);
 use DBICTest;
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_informix')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_informix');
-
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_INFORMIX_${_}" } qw/DSN USER PASS/};
 
-#warn "$dsn $user $pass";
-
-plan skip_all => 'Set $ENV{DBICTEST_INFORMIX_DSN}, _USER and _PASS to run this test'
-  unless $dsn;
-
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass, {
   auto_savepoint => 1
 });
index 396e103..a52b5bd 100644 (file)
@@ -3,9 +3,9 @@ use warnings;
 
 use Test::More;
 use Test::Exception;
-use Scope::Guard ();
 use Try::Tiny;
 use DBIx::Class::Optional::Dependencies ();
+use DBIx::Class::_Util 'scope_guard';
 use lib qw(t/lib);
 use DBICTest;
 
@@ -48,7 +48,7 @@ foreach my $info (@info) {
     auto_savepoint => 1
   });
 
-  my $guard = Scope::Guard->new(sub{ cleanup($schema) });
+  my $guard = scope_guard { cleanup($schema) };
 
   my $dbh = $schema->storage->dbh;
 
index 263fecb..4f72cc4 100644 (file)
@@ -1,22 +1,15 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_mssql_sybase';
+
 use strict;
 use warnings;
 
 use Test::More;
 use Test::Exception;
 use Scalar::Util 'weaken';
-use DBIx::Class::Optional::Dependencies ();
 use lib qw(t/lib);
 use DBICTest;
 
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MSSQL_${_}" } qw/DSN USER PASS/};
-
-plan skip_all => 'Set $ENV{DBICTEST_MSSQL_DSN}, _USER and _PASS to run this test'
-  unless ($dsn);
-
-
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_mssql_sybase')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_mssql_sybase');
-
 {
   my $srv_ver = DBICTest::Schema->connect($dsn, $user, $pass)->storage->_server_info->{dbms_version};
   ok ($srv_ver, 'Got a test server version on fresh schema: ' . ($srv_ver||'???') );
index d092379..45dd895 100644 (file)
@@ -4,7 +4,8 @@ use warnings;
 use Test::More;
 use Test::Exception;
 use DBIx::Class::Optional::Dependencies ();
-use Scope::Guard ();
+use DBIx::Class::_Util 'scope_guard';
+use List::Util 'shuffle';
 use Try::Tiny;
 use lib qw(t/lib);
 use DBICTest;
@@ -36,17 +37,15 @@ plan skip_all => join (' ',
 
 my $schema;
 
-for my $prefix (keys %$env2optdep) { SKIP: {
+for my $prefix (shuffle keys %$env2optdep) { SKIP: {
 
-  my ($dsn, $user, $pass) = map { $ENV{"${prefix}_$_"} } qw/DSN USER PASS/;
+  skip ("Testing with ${prefix}_DSN needs " . DBIx::Class::Optional::Dependencies->req_missing_for( $env2optdep->{$prefix} ), 1)
+    unless  DBIx::Class::Optional::Dependencies->req_ok_for($env2optdep->{$prefix});
 
-  next unless $dsn;
+  my ($dsn, $user, $pass) = map { $ENV{"${prefix}_$_"} } qw/DSN USER PASS/;
 
   note "Testing with ${prefix}_DSN";
 
-  skip ("Testing with ${prefix}_DSN needs " . DBIx::Class::Optional::Dependencies->req_missing_for( $env2optdep->{$prefix} ), 1)
-    unless  DBIx::Class::Optional::Dependencies->req_ok_for($env2optdep->{$prefix});
-
   $schema = DBICTest::Schema->connect($dsn, $user, $pass, {
     auto_savepoint  => 1,
     quote_names     => 1,
@@ -54,7 +53,7 @@ for my $prefix (keys %$env2optdep) { SKIP: {
   });
   my $dbh = $schema->storage->dbh;
 
-  my $sg = Scope::Guard->new(sub { cleanup($schema) });
+  my $sg = scope_guard { cleanup($schema) };
 
   eval { $dbh->do(q[DROP TABLE "artist"]) };
   $dbh->do(<<EOF);
index 8d8aa7e..dfd5816 100644 (file)
@@ -3,13 +3,11 @@ use warnings;
 
 use Test::More;
 use Test::Exception;
-use Scope::Guard ();
 use Try::Tiny;
 use DBIx::Class::Optional::Dependencies ();
+use DBIx::Class::_Util 'scope_guard';
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::DebugObj ();
-use DBIC::SqlMakerTest;
 
 my ($dsn,  $user,  $pass)  = @ENV{map { "DBICTEST_MSACCESS_ODBC_${_}" } qw/DSN USER PASS/};
 my ($dsn2, $user2, $pass2) = @ENV{map { "DBICTEST_MSACCESS_ADO_${_}" }  qw/DSN USER PASS/};
@@ -64,7 +62,7 @@ foreach my $info (@info) {
     LongReadLen => $maxloblen,
   });
 
-  my $guard = Scope::Guard->new(sub { cleanup($schema) });
+  my $guard = scope_guard { cleanup($schema) };
 
   my $dbh = $schema->storage->dbh;
 
@@ -144,12 +142,7 @@ EOF
     title => 'my track',
   });
 
-  my ($sql, @bind);
-
   my $joined_track = try {
-    local $schema->storage->{debug} = 1;
-    local $schema->storage->{debugobj} = DBIC::DebugObj->new(\$sql, \@bind);
-
     $schema->resultset('Artist')->search({
       artistid => $first_artistid,
     }, {
@@ -162,27 +155,10 @@ EOF
     diag "Could not execute two-step left join: $_";
   };
 
-  s/^'//, s/'\z// for @bind;
-
-  # test is duplicated in t/sqlmaker/msaccess.t, keep a duplicate here anyway, just to be safe
-  # -- ribasushi
-  is_same_sql_bind(
-    $sql,
-    \@bind,
-    'SELECT [me].[artistid], [me].[name], [me].[rank], [me].[charfield], [tracks].[title] FROM ( ( [artist] [me] LEFT JOIN cd [cds] ON [cds].[artist] = [me].[artistid] ) LEFT JOIN [track] [tracks] ON [tracks].[cd] = [cds].[cdid] ) WHERE ( [artistid] = ? )',
-    [1],
-    'correct SQL for two-step left join',
-  );
-
   is try { $joined_track->get_column('track_title') }, 'my track',
     'two-step left join works';
 
-  ($sql, @bind) = ();
-
   $joined_artist = try {
-    local $schema->storage->{debug} = 1;
-    local $schema->storage->{debugobj} = DBIC::DebugObj->new(\$sql, \@bind);
-
     $schema->resultset('Track')->search({
       trackid => $track->trackid,
     }, {
@@ -195,18 +171,6 @@ EOF
     diag "Could not execute two-step inner join: $_";
   };
 
-  s/^'//, s/'\z// for @bind;
-
-  # test is duplicated in t/sqlmaker/msaccess.t, keep a duplicate here anyway, just to be safe
-  # -- ribasushi
-  is_same_sql_bind(
-    $sql,
-    \@bind,
-    'SELECT [me].[trackid], [me].[cd], [me].[position], [me].[title], [me].[last_updated_on], [me].[last_updated_at], [artist].[name] FROM ( ( [track] [me] INNER JOIN cd [cd] ON [cd].[cdid] = [me].[cd] ) INNER JOIN [artist] [artist] ON [artist].[artistid] = [cd].[artist] ) WHERE ( [trackid] = ? )',
-    [$track->trackid],
-    'correct SQL for two-step inner join',
-  );
-
   is try { $joined_artist->get_column('artist_name') }, 'foo',
     'two-step inner join works';
 
index fd7e8d7..f61f07e 100644 (file)
@@ -9,41 +9,11 @@ use Math::BigInt;
 
 use lib qw(t/lib);
 use DBICTest;
-use DBIx::Class::_Util qw(sigwarn_silencer modver_gt_or_eq);
+use DBIx::Class::_Util qw( sigwarn_silencer modver_gt_or_eq modver_gt_or_eq_and_lt );
 
-# savepoints test
-{
-  my $schema = DBICTest->init_schema(auto_savepoint => 1);
-
-  my $ars = $schema->resultset('Artist');
-
-  # test two-phase commit and inner transaction rollback from nested transactions
-  $schema->txn_do(sub {
-    $ars->create({ name => 'in_outer_transaction' });
-    $schema->txn_do(sub {
-      $ars->create({ name => 'in_inner_transaction' });
-    });
-    ok($ars->search({ name => 'in_inner_transaction' })->first,
-      'commit from inner transaction visible in outer transaction');
-    throws_ok {
-      $schema->txn_do(sub {
-        $ars->create({ name => 'in_inner_transaction_rolling_back' });
-        die 'rolling back inner transaction';
-      });
-    } qr/rolling back inner transaction/, 'inner transaction rollback executed';
-    $ars->create({ name => 'in_outer_transaction2' });
-  });
-
-  ok($ars->search({ name => 'in_outer_transaction' })->first,
-    'commit from outer transaction');
-  ok($ars->search({ name => 'in_outer_transaction2' })->first,
-    'second commit from outer transaction');
-  ok($ars->search({ name => 'in_inner_transaction' })->first,
-    'commit from inner transaction');
-  is $ars->search({ name => 'in_inner_transaction_rolling_back' })->first,
-    undef,
-    'rollback from inner transaction';
-}
+# make one deploy() round before we load anything else - need this in order
+# to prime SQLT if we are using it (deep depchain is deep)
+DBICTest->init_schema( no_populate => 1 );
 
 # check that we work somewhat OK with braindead SQLite transaction handling
 #
@@ -52,7 +22,6 @@ use DBIx::Class::_Util qw(sigwarn_silencer modver_gt_or_eq);
 #
 # However DBD::SQLite 1.38_02 seems to fix this, with an accompanying test:
 # https://metacpan.org/source/ADAMK/DBD-SQLite-1.38_02/t/54_literal_txn.t
-
 my $lit_txn_todo = modver_gt_or_eq('DBD::SQLite', '1.38_02')
   ? undef
   : "DBD::SQLite before 1.38_02 is retarded wrt detecting literal BEGIN/COMMIT statements"
@@ -124,6 +93,46 @@ DDL
   }
 }
 
+# test blank begin/svp/commit/begin cycle
+warnings_are {
+  my $schema = DBICTest->init_schema( no_populate => 1 );
+  my $rs = $schema->resultset('Artist');
+  is ($rs->count, 0, 'Start with empty table');
+
+  for my $do_commit (1, 0) {
+    $schema->txn_begin;
+    $schema->svp_begin;
+    $schema->svp_rollback;
+
+    $schema->svp_begin;
+    $schema->svp_rollback;
+
+    $schema->svp_release;
+
+    $schema->svp_begin;
+
+    $schema->txn_rollback;
+
+    $schema->txn_begin;
+    $schema->svp_begin;
+    $schema->svp_rollback;
+
+    $schema->svp_begin;
+    $schema->svp_rollback;
+
+    $schema->svp_release;
+
+    $schema->svp_begin;
+
+    $do_commit ? $schema->txn_commit : $schema->txn_rollback;
+
+    is_deeply $schema->storage->savepoints, [], 'Savepoint names cleared away'
+  }
+
+  $schema->txn_do(sub {
+    ok (1, 'all seems fine');
+  });
+} [], 'No warnings emitted';
 
 my $schema = DBICTest->init_schema();
 
@@ -137,6 +146,11 @@ $row->discard_changes;
 is ($row->rank, 'abc', 'proper rank inserted into database');
 
 # and make sure we do not lose actual bigints
+SKIP: {
+
+skip "Not testing bigint handling on known broken DBD::SQLite trial versions", 1
+  if modver_gt_or_eq_and_lt( 'DBD::SQLite', '1.45', '1.45_03' );
+
 {
   package DBICTest::BigIntArtist;
   use base 'DBICTest::Schema::Artist';
@@ -148,9 +162,7 @@ $schema->storage->dbh_do(sub {
   $_[1]->do('ALTER TABLE artist ADD COLUMN bigint BIGINT');
 });
 
-my $sqlite_broken_bigint = (
-  modver_gt_or_eq('DBD::SQLite', '1.34') and ! modver_gt_or_eq('DBD::SQLite', '1.37')
-);
+my $sqlite_broken_bigint = modver_gt_or_eq_and_lt( 'DBD::SQLite', '1.34', '1.37' );
 
 # 63 bit integer
 my $many_bits = (Math::BigInt->new(2) ** 62);
@@ -169,7 +181,6 @@ for my $bi ( qw(
   1
   2
 
-  -9223372036854775808
   -9223372036854775807
   -8694837494948124658
   -6848440844435891639
@@ -204,6 +215,15 @@ for my $bi ( qw(
   $sqlite_broken_bigint
     ? ()
     : ( '2147483648', '2147483649' )
+  ,
+
+  # with newer compilers ( gcc 4.9+ ) older DBD::SQLite does not
+  # play well with the "Most Negative Number"
+  modver_gt_or_eq( 'DBD::SQLite', '1.33' )
+    ? ( '-9223372036854775808' )
+    : ()
+  ,
+
 ) {
   # unsigned 32 bit ints have a range of −2,147,483,648 to 2,147,483,647
   # alternatively expressed as the hexadecimal numbers below
@@ -215,14 +235,27 @@ for my $bi ( qw(
   my $v_desc = sprintf '%s (%d bit signed int)', $bi, $v_bits;
 
   my @w;
-  local $SIG{__WARN__} = sub { $_[0] =~ /datatype mismatch/ ? push @w, @_ : warn @_ };
+  local $SIG{__WARN__} = sub {
+    if ($_[0] =~ /datatype mismatch/) {
+      push @w, @_;
+    }
+    elsif ($_[0] =~ /An integer value occupying more than 32 bits was supplied .+ can not bind properly so DBIC will treat it as a string instead/ ) {
+      # do nothing, this warning will pop up here and there depending on
+      # DBD/bitness combination
+      # we don't want to test for it explicitly, we are just interested
+      # in the results matching at the end
+    }
+    else {
+      warn @_;
+    }
+  };
 
   # some combinations of SQLite 1.35 and older 5.8 faimly is wonky
   # instead of a warning we get a full exception. Sod it
   eval {
     $row = $schema->resultset('BigIntArtist')->create({ bigint => $bi });
   } or do {
-    fail("Exception on inserting $v_desc") unless $sqlite_broken_bigint;
+    fail("Exception on inserting $v_desc: $@") unless $sqlite_broken_bigint;
     next;
   };
 
@@ -248,11 +281,6 @@ for my $bi ( qw(
     "value in database correct ($v_desc)"
   );
 
-# FIXME - temporary smoke-only escape
-SKIP: {
-  skip 'Potential for false negatives - investigation pending', 1
-    if DBICTest::RunMode->is_plain;
-
   # check if math works
   # start by adding/subtracting a 50 bit integer, and then divide by 2 for good measure
   my ($sqlop, $expect) = $bi < 0
@@ -286,11 +314,10 @@ SKIP: {
     , "simple integer math with@{[ $dtype ? '' : 'out' ]} bindtype in database correct (base $v_desc)")
       or diag sprintf '%s != %s', $row->bigint, $expect;
   }
-# end of fixme
-}
 
   is_deeply (\@w, [], "No mismatch warnings on bigint operations ($v_desc)" );
-}
+
+}}
 
 done_testing;
 
index 0fd511f..d20faec 100644 (file)
@@ -3,110 +3,19 @@ use warnings;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
-my $orig_debug = $schema->storage->debug;
-
-# test the abstract join => SQL generator
-my $sa = new DBIx::Class::SQLMaker;
-
-my @j = (
-    { child => 'person' },
-    [ { father => 'person' }, { 'father.person_id' => 'child.father_id' }, ],
-    [ { mother => 'person' }, { 'mother.person_id' => 'child.mother_id' } ],
-);
-my $match = 'person child JOIN person father ON ( father.person_id = '
-          . 'child.father_id ) JOIN person mother ON ( mother.person_id '
-          . '= child.mother_id )'
-          ;
-is_same_sql(
-  $sa->_recurse_from(@j),
-  $match,
-  'join 1 ok'
-);
-
-my @j2 = (
-    { mother => 'person' },
-    [   [   { child => 'person' },
-            [   { father             => 'person' },
-                { 'father.person_id' => 'child.father_id' }
-            ]
-        ],
-        { 'mother.person_id' => 'child.mother_id' }
-    ],
-);
-$match = 'person mother JOIN (person child JOIN person father ON ('
-       . ' father.person_id = child.father_id )) ON ( mother.person_id = '
-       . 'child.mother_id )'
-       ;
-is_same_sql(
-  $sa->_recurse_from(@j2),
-  $match,
-  'join 2 ok'
-);
-
-
-my @j3 = (
-    { child => 'person' },
-    [ { father => 'person', -join_type => 'inner' }, { 'father.person_id' => 'child.father_id' }, ],
-    [ { mother => 'person', -join_type => 'inner'  }, { 'mother.person_id' => 'child.mother_id' } ],
-);
-$match = 'person child INNER JOIN person father ON ( father.person_id = '
-          . 'child.father_id ) INNER JOIN person mother ON ( mother.person_id '
-          . '= child.mother_id )'
-          ;
-
-is_same_sql(
-  $sa->_recurse_from(@j3),
-  $match,
-  'join 3 (inner join) ok'
-);
-
-my @j4 = (
-    { mother => 'person' },
-    [   [   { child => 'person', -join_type => 'left' },
-            [   { father             => 'person', -join_type => 'right' },
-                { 'father.person_id' => 'child.father_id' }
-            ]
-        ],
-        { 'mother.person_id' => 'child.mother_id' }
-    ],
-);
-$match = 'person mother LEFT JOIN (person child RIGHT JOIN person father ON ('
-       . ' father.person_id = child.father_id )) ON ( mother.person_id = '
-       . 'child.mother_id )'
-       ;
-is_same_sql(
-  $sa->_recurse_from(@j4),
-  $match,
-  'join 4 (nested joins + join types) ok'
-);
-
-my @j5 = (
-    { child => 'person' },
-    [ { father => 'person' }, { 'father.person_id' => \'!= child.father_id' }, ],
-    [ { mother => 'person' }, { 'mother.person_id' => 'child.mother_id' } ],
-);
-$match = 'person child JOIN person father ON ( father.person_id != '
-          . 'child.father_id ) JOIN person mother ON ( mother.person_id '
-          . '= child.mother_id )'
-          ;
-is_same_sql(
-  $sa->_recurse_from(@j5),
-  $match,
-  'join 5 (SCALAR reference for ON statement) ok'
-);
-
 my $rs = $schema->resultset("CD")->search(
            { 'year' => 2001, 'artist.name' => 'Caterwauler McCrae' },
-           { from => [ { 'me' => 'cd' },
-                         [
-                           { artist => 'artist' },
-                           { 'me.artist' => 'artist.artistid' }
-                         ] ] }
+           { from => [
+              { 'me' => 'cd' },
+              [
+                { artist => 'artist' },
+                { 'me.artist' => { -ident => 'artist.artistid' } },
+              ],
+           ] }
          );
 
 is( $rs + 0, 1, "Single record in resultset");
index dca5654..9d09380 100644 (file)
@@ -4,8 +4,7 @@ use warnings;
 use Test::More;
 use Test::Exception;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
@@ -61,6 +60,7 @@ my $subsel = $cds->search ({}, {
 is ($subsel->count, 2, 'Subselect correctly limited the rs to 2 cds');
 is ($subsel->next->title, $cds->next->title, 'First CD title match');
 is ($subsel->next->title, $cds->next->title, 'Second CD title match');
+$cds->reset;
 
 is($schema->resultset('CD')->current_source_alias, "me", '$rs->current_source_alias returns "me"');
 
index ba5a181..b380225 100644 (file)
@@ -6,8 +6,6 @@ use Test::Exception;
 use Test::Warn;
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::SqlMakerTest;
-use DBIC::DebugObj;
 
 my $schema = DBICTest->init_schema();
 
@@ -228,23 +226,12 @@ is($row->baz, 3, 'baz is correct');
 {
   my $artist = $schema->resultset('Artist')->find(1);
 
-  my ($sql, @bind);
-  my $old_debugobj = $schema->storage->debugobj;
-  my $old_debug = $schema->storage->debug;
-  $schema->storage->debugobj(DBIC::DebugObj->new(\$sql, \@bind)),
-  $schema->storage->debug(1);
-
-  $artist->discard_changes;
-
-  is_same_sql_bind (
-    $sql,
-    \@bind,
-    'SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me WHERE me.artistid = ?',
-    [qw/'1'/],
-  );
-
-  $schema->storage->debug($old_debug);
-  $schema->storage->debugobj($old_debugobj);
+  $schema->is_executed_sql_bind( sub { $artist->discard_changes }, [
+    [
+      'SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me WHERE me.artistid = ?',
+      [ { dbic_colname => "me.artistid", sqlt_datatype => "integer" } => 1 ],
+    ]
+  ], 'Expected query on discard_changes');
 }
 
 {
index 14c4762..ec3ba92 100644 (file)
@@ -17,10 +17,24 @@ cmp_ok($cover_band->id, '!=', $artist->id, 'ok got new column id...');
 is($cover_cds->count, $artist_cds->count, 'duplicated rows count ok');
 
 #check multi-keyed
-cmp_ok($cover_band->search_related('twokeys')->count, '>', 0, 'duplicated multiPK ok');
+is(
+  $cover_band->search_related('twokeys')->count,
+  $artist->search_related('twokeys')->count,
+  'duplicated multiPK ok'
+);
 
 #and check copying a few relations away
 cmp_ok($cover_cds->search_related('tags')->count, '==',
    $artist_cds->search_related('tags')->count , 'duplicated count ok');
 
+
+# check from the other side
+my $cd = $schema->resultset('CD')->find(1);
+my $dup_cd = $cd->copy ({ title => 'ha!' });
+is(
+  $dup_cd->search_related('twokeys')->count,
+  $cd->search_related('twokeys')->count,
+  'duplicated multiPK ok'
+);
+
 done_testing;
index 9edfe71..5812083 100644 (file)
@@ -7,12 +7,6 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-my $queries;
-my $debugcb = sub{ $queries++ };
-my $sdebug = $schema->storage->debug;
-
-plan tests => 23;
-
 my $rs = $schema->resultset("Artist")->search(
   { artistid => 1 }
 );
@@ -43,18 +37,12 @@ my $cd = $schema->resultset('CD')->find(1);
 
 $rs->clear_cache;
 
-$queries = 0;
-$schema->storage->debug(1);
-$schema->storage->debugcb ($debugcb);
-
-$rs = $schema->resultset('Artist')->search( undef, { cache => 1 } );
-while( $artist = $rs->next ) {}
-$artist = $rs->first();
-
-is( $queries, 1, 'revisiting a row does not issue a query when cache => 1' );
+$schema->is_executed_querycount( sub {
 
-$schema->storage->debug($sdebug);
-$schema->storage->debugcb (undef);
+  $rs = $schema->resultset('Artist')->search( undef, { cache => 1 } );
+  while( $artist = $rs->next ) {}
+  $artist = $rs->first();
+}, 1, 'revisiting a row does not issue a query when cache => 1' );
 
 my @a = $schema->resultset("Artist")->search(
   { },
@@ -74,33 +62,28 @@ $rs = $schema->resultset("Artist")->search(
   }
 );
 
-# start test for prefetch SELECT count
-$queries = 0;
-$schema->storage->debug(1);
-$schema->storage->debugcb ($debugcb);
-
-$artist = $rs->first;
-$rs->reset();
+# prefetch SELECT count
+$schema->is_executed_querycount( sub {
+  $artist = $rs->first;
+  $rs->reset();
 
-# make sure artist contains a related resultset for cds
-isa_ok( $artist->{related_resultsets}{cds}, 'DBIx::Class::ResultSet', 'artist has a related_resultset for cds' );
+  # make sure artist contains a related resultset for cds
+  isa_ok( $artist->{related_resultsets}{cds}, 'DBIx::Class::ResultSet', 'artist has a related_resultset for cds' );
 
-# check if $artist->cds->get_cache is populated
-is( scalar @{$artist->cds->get_cache}, 3, 'cache for artist->cds contains correct number of records');
+  # check if $artist->cds->get_cache is populated
+  is( scalar @{$artist->cds->get_cache}, 3, 'cache for artist->cds contains correct number of records');
 
-# ensure that $artist->cds returns correct number of objects
-is( scalar ($artist->cds), 3, 'artist->cds returns correct number of objects' );
+  # ensure that $artist->cds returns correct number of objects
+  is( scalar ($artist->cds), 3, 'artist->cds returns correct number of objects' );
 
-# ensure that $artist->cds->count returns correct value
-is( $artist->cds->count, 3, 'artist->cds->count returns correct value' );
+  # ensure that $artist->cds->count returns correct value
+  is( $artist->cds->count, 3, 'artist->cds->count returns correct value' );
 
-# ensure that $artist->count_related('cds') returns correct value
-is( $artist->count_related('cds'), 3, 'artist->count_related returns correct value' );
+  # ensure that $artist->count_related('cds') returns correct value
+  is( $artist->count_related('cds'), 3, 'artist->count_related returns correct value' );
 
-is($queries, 1, 'only one SQL statement executed');
+}, 1, 'only one SQL statement executed');
 
-$schema->storage->debug($sdebug);
-$schema->storage->debugcb (undef);
 
 # make sure related_resultset is deleted after object is updated
 $artist->set_column('name', 'New Name');
@@ -131,57 +114,44 @@ is($artist->cds, 0, 'No cds for this artist');
 }
 
 # SELECT count for nested has_many prefetch
-$queries = 0;
-$schema->storage->debug(1);
-$schema->storage->debugcb ($debugcb);
-
-$artist = ($rs->all)[0];
-
-is($queries, 1, 'only one SQL statement executed');
-
-$queries = 0;
-
-my @objs;
-my $cds = $artist->cds;
-my $tags = $cds->next->tags;
-while( my $tag = $tags->next ) {
-  push @objs, $tag->tagid; #warn "tag:", $tag->ID, " => ", $tag->tag;
-}
-
-is_deeply( \@objs, [ 3 ], 'first cd has correct tags' );
-
-$tags = $cds->next->tags;
-@objs = ();
-while( my $tag = $tags->next ) {
-  push @objs, $tag->id; #warn "tag: ", $tag->ID;
-}
-
-is_deeply( [ sort @objs] , [ 2, 5, 8 ], 'third cd has correct tags' );
-
-$tags = $cds->next->tags;
-@objs = ();
-while( my $tag = $tags->next ) {
-  push @objs, $tag->id; #warn "tag: ", $tag->ID;
-}
-
-is_deeply( \@objs, [ 1 ], 'second cd has correct tags' );
+$schema->is_executed_querycount( sub {
+  $artist = ($rs->all)[0];
+}, 1, 'only one SQL statement executed');
+
+$schema->is_executed_querycount( sub {
+  my @objs;
+  my $cds = $artist->cds;
+  my $tags = $cds->next->tags;
+  while( my $tag = $tags->next ) {
+    push @objs, $tag->tagid; #warn "tag:", $tag->ID, " => ", $tag->tag;
+  }
 
-is( $queries, 0, 'no additional SQL statements while checking nested data' );
+  is_deeply( \@objs, [ 3 ], 'first cd has correct tags' );
 
-# start test for prefetch SELECT count
-$queries = 0;
+  $tags = $cds->next->tags;
+  @objs = ();
+  while( my $tag = $tags->next ) {
+    push @objs, $tag->id; #warn "tag: ", $tag->ID;
+  }
 
-$artist = $schema->resultset('Artist')->find(1, { prefetch => [qw/cds/] });
+  is_deeply( [ sort @objs] , [ 2, 5, 8 ], 'third cd has correct tags' );
 
-is( $queries, 1, 'only one select statement on find with inline has_many prefetch' );
+  $tags = $cds->next->tags;
+  @objs = ();
+  while( my $tag = $tags->next ) {
+    push @objs, $tag->id; #warn "tag: ", $tag->ID;
+  }
 
-# start test for prefetch SELECT count
-$queries = 0;
+  is_deeply( \@objs, [ 1 ], 'second cd has correct tags' );
+}, 0, 'no additional SQL statements while checking nested data' );
 
-$rs = $schema->resultset('Artist')->search(undef, { prefetch => [qw/cds/] });
-$artist = $rs->find(1);
+$schema->is_executed_querycount( sub {
+  $artist = $schema->resultset('Artist')->find(1, { prefetch => [qw/cds/] });
+}, 1, 'only one select statement on find with inline has_many prefetch' );
 
-is( $queries, 1, 'only one select statement on find with has_many prefetch on resultset' );
+$schema->is_executed_querycount( sub {
+  $rs = $schema->resultset('Artist')->search(undef, { prefetch => [qw/cds/] });
+  $artist = $rs->find(1);
+}, 1, 'only one select statement on find with has_many prefetch on resultset' );
 
-$schema->storage->debug($sdebug);
-$schema->storage->debugcb (undef);
+done_testing;
index ffe0368..ffa63fa 100644 (file)
@@ -138,17 +138,12 @@ for my $name (keys %stores) {
 
 
     # Test resultsource with cached rows
-    my $query_count;
-    $cd_rs = $cd_rs->search ({}, { cache => 1 });
+    $schema->is_executed_querycount( sub {
+      $cd_rs = $cd_rs->search ({}, { cache => 1 });
 
-    my $orig_debug = $schema->storage->debug;
-    $schema->storage->debug(1);
-    $schema->storage->debugcb(sub { $query_count++ } );
+      # this will hit the database once and prime the cache
+      my @cds = $cd_rs->all;
 
-    # this will hit the database once and prime the cache
-    my @cds = $cd_rs->all;
-
-    lives_ok {
       $copy = $store->($cd_rs);
       ref_ne($copy, $cd_rs, 'Cached resultset cloned');
       is_deeply (
@@ -158,12 +153,7 @@ for my $name (keys %stores) {
       );
 
       is ($copy->count, $cd_rs->count, 'Cached count identical');
-    } "serialize cached resultset lives: $name";
-
-    is ($query_count, 1, 'Only one db query fired');
-
-    $schema->storage->debug($orig_debug);
-    $schema->storage->debugcb(undef);
+    }, 1, 'Only one db query fired');
 }
 
 # test schema-less detached thaw
index a07e42a..e1f2cae 100644 (file)
@@ -5,7 +5,6 @@ use Test::More;
 use Test::Warn;
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::DebugObj;
 
 {
   package A::Comp;
@@ -88,7 +87,7 @@ warnings_like (
 my $schema = DBICTest->init_schema();
 DBICTest::Schema::CD->load_components('UTF8Columns');
 DBICTest::Schema::CD->utf8_columns('title');
-Class::C3->reinitialize();
+Class::C3->reinitialize() if DBIx::Class::_ENV_::OLD_MRO;
 
 # as per http://search.cpan.org/dist/Test-Simple/lib/Test/More.pm#utf8
 binmode (Test::More->builder->$_, ':utf8') for qw/output failure_output todo_output/;
@@ -97,24 +96,22 @@ my $bytestream_title = my $utf8_title = "weird \x{466} stuff";
 utf8::encode($bytestream_title);
 cmp_ok ($bytestream_title, 'ne', $utf8_title, 'unicode/raw differ (sanity check)');
 
-my $storage = $schema->storage;
-my ($sql, @bind);
-my $debugobj = DBIC::DebugObj->new (\$sql, \@bind);
-my ($orig_debug, $orig_debugobj) = ($storage->debug, $storage->debugobj);
-$storage->debugobj ($debugobj);
-$storage->debug (1);
-
-my $cd = $schema->resultset('CD')->create( { artist => 1, title => $utf8_title, year => '2048' } );
-
-$storage->debugobj ($orig_debugobj);
-$storage->debug ($orig_debug);
-
-# bind values are always alphabetically ordered by column, thus [1]
-# the single quotes are an artefact of the debug-system
+my $cd;
 {
   local $TODO = "This has been broken since rev 1191, Mar 2006";
-  is ($bind[1], "'$bytestream_title'", 'INSERT: raw bytes sent to the database');
-}
+
+  $schema->is_executed_sql_bind( sub {
+    $cd = $schema->resultset('CD')->create( { artist => 1, title => $utf8_title, year => '2048' } )
+  }, [[
+    'INSERT INTO cd ( artist, title, year) VALUES ( ?, ?, ? )',
+     [ { dbic_colname => "artist", sqlt_datatype => "integer" }
+        => 1 ],
+     [ { dbic_colname => "title", sqlt_datatype => "varchar", sqlt_size => 100 }
+        => $bytestream_title ],
+     [ { dbic_colname => "year", sqlt_datatype => "varchar", sqlt_size => 100 }
+        => 2048 ],
+  ]], 'INSERT: raw bytes sent to the database' );
+};
 
 # this should be using the cursor directly, no inflation/processing of any sort
 my ($raw_db_title) = $schema->resultset('CD')
@@ -149,16 +146,20 @@ ok(! utf8::is_utf8( $cd->{_column_data}{title} ), 'reloaded utf8-less title' );
 $bytestream_title = $utf8_title = "something \x{219} else";
 utf8::encode($bytestream_title);
 
+$schema->is_executed_sql_bind( sub {
+  $cd->update ({ title => $utf8_title });
+}, [
+  [ 'BEGIN' ],
+  [
+    'UPDATE cd SET title = ? WHERE cdid = ?',
+    [ { dbic_colname => "title", sqlt_datatype => "varchar", sqlt_size => 100 }
+      => $bytestream_title ],
+    [ { dbic_colname => "cdid", sqlt_datatype => "integer" }
+      => 6 ],
+  ],
+  [ 'COMMIT' ],
+], 'UPDATE: raw bytes sent to the database');
 
-$storage->debugobj ($debugobj);
-$storage->debug (1);
-
-$cd->update ({ title => $utf8_title });
-
-$storage->debugobj ($orig_debugobj);
-$storage->debug ($orig_debug);
-
-is ($bind[0], "'$bytestream_title'", 'UPDATE: raw bytes sent to the database');
 ($raw_db_title) = $schema->resultset('CD')
                              ->search ($cd->ident_condition)
                                ->get_column('title')
index 0ca9a06..05ba539 100644 (file)
@@ -8,36 +8,27 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-my $queries;
-$schema->storage->debugcb( sub{ $queries++ } );
-my $sdebug = $schema->storage->debug;
-
 my $cd = $schema->resultset("CD")->find(1);
 $cd->title('test');
 
-# SELECT count
-$queries = 0;
-$schema->storage->debug(1);
-
-$cd->update;
-
-is($queries, 1, 'liner_notes (might_have) not prefetched - do not load
-liner_notes on update');
-
-$schema->storage->debug($sdebug);
-
+$schema->is_executed_querycount( sub {
+  $cd->update;
+}, {
+  BEGIN => 1,
+  UPDATE => 1,
+  COMMIT => 1,
+}, 'liner_notes (might_have) not prefetched - do not load liner_notes on update' );
 
 my $cd2 = $schema->resultset("CD")->find(2, {prefetch => 'liner_notes'});
 $cd2->title('test2');
 
-# SELECT count
-$queries = 0;
-$schema->storage->debug(1);
-
-$cd2->update;
-
-is($queries, 1, 'liner_notes (might_have) prefetched - do not load
-liner_notes on update');
+$schema->is_executed_querycount( sub {
+  $cd2->update;
+}, {
+  BEGIN => 1,
+  UPDATE => 1,
+  COMMIT => 1,
+}, 'liner_notes (might_have) prefetched - do not load liner_notes on update');
 
 warning_like {
   local $ENV{DBIC_DONT_VALIDATE_RELS};
@@ -62,5 +53,4 @@ warning_like {
   'Setting DBIC_DONT_VALIDATE_RELS suppresses nullable relation warnings';
 }
 
-$schema->storage->debug($sdebug);
 done_testing();
index 87b90a5..a6b17ec 100644 (file)
@@ -1,19 +1,14 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'deploy';
+
 use strict;
 use warnings;
 
 use Test::More;
 use Test::Warn;
-use lib qw(t/lib);
-use DBICTest;
-
 use Scalar::Util 'blessed';
 
-BEGIN {
-  require DBIx::Class;
-  plan skip_all =>
-      'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('deploy')
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('deploy')
-}
+use lib qw(t/lib);
+use DBICTest;
 
 my $custom_deployment_statements_called = 0;
 
@@ -23,10 +18,9 @@ sub DBICTest::Schema::deployment_statements {
   return $self->next::method(@_);
 }
 
-
 # Check deployment statements ctx sensitivity
 {
-  my $schema = DBICTest->init_schema (no_deploy => 1);
+  my $schema = DBICTest->init_schema (no_deploy => 1, quote_names => 1);
   my $not_first_table_creation_re = qr/CREATE TABLE "fourkeys_to_twokeys"/;
 
   my $statements = $schema->deployment_statements;
index 324be84..1eb079b 100644 (file)
@@ -6,7 +6,7 @@ use Test::More;
 use lib qw(t/lib);
 use DBICTest;
 
-use POSIX qw(ceil);
+use POSIX ();
 
 my $schema = DBICTest->init_schema();
 
@@ -156,7 +156,9 @@ $to_pos = undef;
     $to_pos++;
     $to_group = ($to_group % 3) + 1;
     $to_group_2_base++;
-    $to_group_2 = (ceil($to_group_2_base/3.0) %3) +1
+    $to_group_2 = (
+      POSIX::ceil( $to_group_2_base / 3.0 ) % 3
+    ) + 1;
   }
 }
 foreach my $group_id_2 (1..4) {
index b1c1e96..e1b73a3 100644 (file)
@@ -4,9 +4,14 @@ use warnings;
 use Test::More;
 use Test::Warn;
 use Test::Exception;
+
+# MASSIVE FIXME - there is a hole in ::RSC / as_subselect_rs
+# losing the order. Needs a rework/extract of the realiaser,
+# and that's a whole another bag of dicks
+BEGIN { $ENV{DBIC_SHUFFLE_UNORDERED_RESULTSETS} = 0 }
+
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
@@ -41,14 +46,16 @@ is($rs_title->min, 'Caterwaulin\' Blues', "min okay for title");
 
 cmp_ok($rs_year->sum, '==', 9996, "three artists returned");
 
-my $rso_year = $rs->search({}, { order_by => 'cdid' })->get_column('year');
-is($rso_year->next, 1999, "reset okay");
+{
+  my $rso_year = $rs->search({}, { order_by => 'cdid' })->get_column('year');
+  is($rso_year->next, 1999, "reset okay");
 
-is($rso_year->first, 1999, "first okay");
+  is($rso_year->first, 1999, "first okay");
 
-warnings_exist (sub {
-  is($rso_year->single, 1999, "single okay");
-}, qr/Query returned more than one row/, 'single warned');
+  warnings_exist (sub {
+    is($rso_year->single, 1999, "single okay");
+  }, qr/Query returned more than one row/, 'single warned');
+}
 
 
 # test distinct propagation
@@ -248,17 +255,23 @@ is_same_sql_bind (
 
   $schema->resultset('CD')->create({ artist => 1, title => 'dealbroker no tracks', year => 2001 });
 
+  my $yp1 = \[ 'year + ?', 1 ];
+
   my $rs = $schema->resultset ('CD')->search (
     { 'artist.name' => { '!=', 'evancarrol' }, 'tracks.trackid' => { '!=', undef } },
     {
       order_by => 'me.year',
       join => [qw(artist tracks)],
-      columns => [ 'year', { cnt => { count => 'me.cdid' }} ],
+      columns => [
+        'year',
+        { cnt => { count => 'me.cdid' } },
+        {  year_plus_one => $yp1 },
+      ],
     },
   );
 
   my $rstypes = {
-    'explicitly grouped' => $rs->search_rs({}, { group_by => 'year' }),
+    'explicitly grouped' => $rs->search_rs({}, { group_by => [ 'year', $yp1 ] } ),
     'implicitly grouped' => $rs->search_rs({}, { distinct => 1 }),
   };
 
@@ -277,27 +290,37 @@ is_same_sql_bind (
   # would silently drop the group_by entirely, likely ending up with nonsensival results
   # With the current behavior the user will at least get a nice fat exception from the
   # RDBMS (or maybe the RDBMS will even decide to handle the situation sensibly...)
-  warnings_exist { is_same_sql_bind(
-    $rstypes->{'implicitly grouped'}->get_column('cnt')->as_query,
-    '(
-      SELECT COUNT( me.cdid )
-        FROM cd me
-        JOIN artist artist
-          ON artist.artistid = me.artist
-        LEFT JOIN track tracks
-          ON tracks.cd = me.cdid
-      WHERE artist.name != ? AND tracks.trackid IS NOT NULL
-      GROUP BY COUNT( me.cdid )
-      ORDER BY MIN(me.year)
-    )',
-    [ [ { dbic_colname => 'artist.name', sqlt_datatype => 'varchar', sqlt_size => 100 }
-        => 'evancarrol'
-    ] ],
-    'Expected (though nonsensical) SQL generated on rscol-with-distinct-over-function',
-  ) } qr/
-    \QUse of distinct => 1 while selecting anything other than a column \E
-    \Qdeclared on the primary ResultSource is deprecated\E
-  /x, 'deprecation warning';
+  for (
+    [ cnt => 'COUNT( me.cdid )' ],
+    [ year_plus_one => 'year + ?' => [ {} => 1 ] ],
+  ) {
+    my ($col, $sel_grp_sql, @sel_grp_bind) = @$_;
+
+    warnings_exist { is_same_sql_bind(
+      $rstypes->{'implicitly grouped'}->get_column($col)->as_query,
+      "(
+        SELECT $sel_grp_sql
+          FROM cd me
+          JOIN artist artist
+            ON artist.artistid = me.artist
+          LEFT JOIN track tracks
+            ON tracks.cd = me.cdid
+        WHERE artist.name != ? AND tracks.trackid IS NOT NULL
+        GROUP BY $sel_grp_sql
+        ORDER BY MIN(me.year)
+      )",
+      [
+        @sel_grp_bind,
+        [ { dbic_colname => 'artist.name', sqlt_datatype => 'varchar', sqlt_size => 100 }
+          => 'evancarrol' ],
+        @sel_grp_bind,
+      ],
+      'Expected (though nonsensical) SQL generated on rscol-with-distinct-over-function',
+    ) } qr/
+      \QUse of distinct => 1 while selecting anything other than a column \E
+      \Qdeclared on the primary ResultSource is deprecated (you selected '$col')\E
+    /x, 'deprecation warning';
+  }
 
   {
     local $TODO = 'multiplying join leaks through to the count aggregate... this may never actually work';
index 2bc86b2..27111e4 100644 (file)
@@ -5,8 +5,7 @@ use Test::More;
 use Test::Exception;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 my $schema = DBICTest->init_schema();
 
 lives_ok (sub {
index 95d2b92..49c1f57 100644 (file)
@@ -4,8 +4,6 @@ use warnings;
 use Test::More;
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::SqlMakerTest;
-use DBIC::DebugObj;
 
 { # Fake storage driver for sqlite with autocast
     package DBICTest::SQLite::AutoCast;
@@ -37,22 +35,18 @@ my $rs = $schema->resultset ('CD')->search ({
   'me.single_track' => \[ '= ?', [ single_track => 1 ] ],
 }, { join => 'tracks' });
 
-my ($sql, @bind);
-my $debugobj = DBIC::DebugObj->new (\$sql, \@bind);
-my $storage = $schema->storage;
-my ($orig_debug, $orig_debugobj) = ($storage->debug, $storage->debugobj);
-$storage->debugobj ($debugobj);
-$storage->debug (1);
-
-# the quoting is a debugobj thing, not dbic-internals
-my $bind = [ map { "'$_'" } qw/
-  5 1 2009 4
-/];
+my @bind = (
+  [ { dbic_colname => "cdid", sqlt_datatype => "integer" }
+      => 5 ],
+  [ { dbic_colname => "single_track", sqlt_datatype => "integer" }
+      => 1 ],
+  [ { dbic_colname => "tracks.last_updated_on", sqlt_datatype => "datetime" }
+      => 2009 ],
+  [ { dbic_colname => "tracks.position", sqlt_datatype => "int" }
+      => 4 ],
+);
 
-$rs->all;
-is_same_sql_bind (
-  $sql,
-  \@bind,
+$schema->is_executed_sql_bind( sub { $rs->all }, [[
   '
     SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
       FROM cd me
@@ -64,16 +58,12 @@ is_same_sql_bind (
       AND tracks.last_updated_on < ?
       AND tracks.position = ?
   ',
-  $bind,
-  'expected sql with casting off',
-);
+  @bind,
+]], 'expected sql with casting off' );
 
 $schema->storage->auto_cast (1);
 
-$rs->all;
-is_same_sql_bind (
-  $sql,
-  \@bind,
+$schema->is_executed_sql_bind( sub { $rs->all }, [[
   '
     SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
       FROM cd me
@@ -85,11 +75,7 @@ is_same_sql_bind (
       AND tracks.last_updated_on < CAST (? AS DateTime)
       AND tracks.position = ?
   ',
-  $bind,
-  'expected sql with casting on',
-);
-
-$storage->debugobj ($orig_debugobj);
-$storage->debug ($orig_debug);
+  @bind,
+]], 'expected sql with casting on' );
 
 done_testing;
index 93fcca7..9dcdcf1 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw(deploy test_rdbms_mysql);
+
 use strict;
 use warnings;
 
@@ -13,23 +15,7 @@ use lib qw(t/lib);
 use DBICTest;
 use DBIx::Class::_Util 'sigwarn_silencer';
 
-my ($dsn, $user, $pass);
-
-BEGIN {
-  ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MYSQL_${_}" } qw/DSN USER PASS/};
-
-  plan skip_all => 'Set $ENV{DBICTEST_MYSQL_DSN}, _USER and _PASS to run this test'
-    unless ($dsn);
-
-  require DBIx::Class;
-  plan skip_all =>
-      'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('deploy')
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('deploy');
-
-  plan skip_all =>
-      'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_mysql')
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_mysql');
-}
+my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MYSQL_${_}" } qw/DSN USER PASS/};
 
 # this is just to grab a lock
 {
@@ -285,6 +271,17 @@ my $schema_v3 = DBICVersion::Schema->connect($dsn, $user, $pass, { ignore_versio
   ok($get_db_version_run == 0, "attributes pulled from list connect_info");
 }
 
+# at this point we have v1, v2 and v3 still connected
+# make sure they are the only connections and everything else is gone
+is
+  scalar( grep
+    { defined $_ and $_->{Active} }
+    map
+      { @{$_->{ChildHandles}} }
+      values %{ { DBI->installed_drivers } }
+  ), 3, "Expected number of connections at end of script"
+;
+
 END {
   unless ($ENV{DBICTEST_KEEP_VERSIONING_DDL}) {
     $ddl_dir->rmtree;
diff --git a/t/98savepoints.t b/t/98savepoints.t
deleted file mode 100644 (file)
index a195b85..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-use strict;
-use warnings;
-
-use Test::More;
-use DBIx::Class::Optional::Dependencies ();
-
-my $env2optdep = {
-  DBICTEST_PG => 'rdbms_pg',
-  DBICTEST_MYSQL => 'test_rdbms_mysql',
-};
-
-plan skip_all => join (' ',
-  'Set $ENV{DBICTEST_PG_DSN} and/or $ENV{DBICTEST_MYSQL_DSN} _USER and _PASS to run these tests.',
-) unless grep { $ENV{"${_}_DSN"} } keys %$env2optdep;
-
-use lib qw(t/lib);
-use DBICTest;
-use DBICTest::Stats;
-
-my $schema;
-
-for my $prefix (keys %$env2optdep) { SKIP: {
-  my ($dsn, $user, $pass) = map { $ENV{"${prefix}_$_"} } qw/DSN USER PASS/;
-
-  skip ("Skipping tests with $prefix: set \$ENV{${prefix}_DSN} _USER and _PASS", 1)
-    unless $dsn;
-
-  skip ("Testing with ${prefix}_DSN needs " . DBIx::Class::Optional::Dependencies->req_missing_for( $env2optdep->{$prefix} ), 1)
-    unless  DBIx::Class::Optional::Dependencies->req_ok_for($env2optdep->{$prefix});
-
-  $schema = DBICTest::Schema->connect ($dsn,$user,$pass,{ auto_savepoint => 1 });
-
-  my $create_sql;
-  $schema->storage->ensure_connected;
-  if ($schema->storage->isa('DBIx::Class::Storage::DBI::Pg')) {
-    $create_sql = "CREATE TABLE artist (artistid serial PRIMARY KEY, name VARCHAR(100), rank INTEGER NOT NULL DEFAULT '13', charfield CHAR(10))";
-    $schema->storage->dbh->do('SET client_min_messages=WARNING');
-  }
-  elsif ($schema->storage->isa('DBIx::Class::Storage::DBI::mysql')) {
-    $create_sql = "CREATE TABLE artist (artistid INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), rank INTEGER NOT NULL DEFAULT '13', charfield CHAR(10)) ENGINE=InnoDB";
-  }
-  else {
-    skip( 'Untested driver ' . $schema->storage, 1 );
-  }
-
-  note "Testing $prefix";
-
-  my $stats = DBICTest::Stats->new;
-  $schema->storage->debugobj($stats);
-  $schema->storage->debug(1);
-
-  $schema->storage->dbh->do ('DROP TABLE IF EXISTS artist');
-  $schema->storage->dbh->do ($create_sql);
-
-  $schema->resultset('Artist')->create({ name => 'foo' });
-
-  $schema->txn_begin;
-
-  my $arty = $schema->resultset('Artist')->find(1);
-
-  my $name = $arty->name;
-
-  # First off, test a generated savepoint name
-  $schema->svp_begin;
-
-  cmp_ok($stats->{'SVP_BEGIN'}, '==', 1, 'Statistics svp_begin tickled');
-
-  $arty->update({ name => 'Jheephizzy' });
-
-  $arty->discard_changes;
-
-  cmp_ok($arty->name, 'eq', 'Jheephizzy', 'Name changed');
-
-  # Rollback the generated name
-  # Active: 0
-  $schema->svp_rollback;
-
-  cmp_ok($stats->{'SVP_ROLLBACK'}, '==', 1, 'Statistics svp_rollback tickled');
-
-  $arty->discard_changes;
-
-  cmp_ok($arty->name, 'eq', $name, 'Name rolled back');
-
-  $arty->update({ name => 'Jheephizzy'});
-
-  # Active: 0 1
-  $schema->svp_begin('testing1');
-
-  $arty->update({ name => 'yourmom' });
-
-  # Active: 0 1 2
-  $schema->svp_begin('testing2');
-
-  $arty->update({ name => 'gphat' });
-  $arty->discard_changes;
-  cmp_ok($arty->name, 'eq', 'gphat', 'name changed');
-  # Active: 0 1 2
-  # Rollback doesn't DESTROY the savepoint, it just rolls back to the value
-  # at its conception
-  $schema->svp_rollback('testing2');
-  $arty->discard_changes;
-  cmp_ok($arty->name, 'eq', 'yourmom', 'testing2 reverted');
-
-  # Active: 0 1 2 3
-  $schema->svp_begin('testing3');
-  $arty->update({ name => 'coryg' });
-  # Active: 0 1 2 3 4
-  $schema->svp_begin('testing4');
-  $arty->update({ name => 'watson' });
-
-  # Release 3, which implicitly releases 4
-  # Active: 0 1 2
-  $schema->svp_release('testing3');
-  $arty->discard_changes;
-  cmp_ok($arty->name, 'eq', 'watson', 'release left data');
-  # This rolls back savepoint 2
-  # Active: 0 1 2
-  $schema->svp_rollback;
-  $arty->discard_changes;
-  cmp_ok($arty->name, 'eq', 'yourmom', 'rolled back to 2');
-
-  # Rollback the original savepoint, taking us back to the beginning, implicitly
-  # rolling back savepoint 1 and 2
-  $schema->svp_rollback('savepoint_0');
-  $arty->discard_changes;
-  cmp_ok($arty->name, 'eq', 'foo', 'rolled back to start');
-
-  $schema->txn_commit;
-
-  # And now to see if txn_do will behave correctly
-  $schema->txn_do (sub {
-    my $artycp = $arty;
-
-    $schema->txn_do (sub {
-      $artycp->name ('Muff');
-      $artycp->update;
-    });
-
-    eval {
-      $schema->txn_do (sub {
-        $artycp->name ('Moff');
-        $artycp->update;
-        $artycp->discard_changes;
-        is($artycp->name,'Moff','Value updated in nested transaction');
-        $schema->storage->dbh->do ("GUARANTEED TO PHAIL");
-      });
-    };
-
-    ok ($@,'Nested transaction failed (good)');
-
-    $arty->discard_changes;
-
-    is($arty->name,'Muff','auto_savepoint rollback worked');
-
-    $arty->name ('Miff');
-
-    $arty->update;
-  });
-
-  $arty->discard_changes;
-
-  is($arty->name,'Miff','auto_savepoint worked');
-
-  cmp_ok($stats->{'SVP_BEGIN'},'==',7,'Correct number of savepoints created');
-
-  cmp_ok($stats->{'SVP_RELEASE'},'==',3,'Correct number of savepoints released');
-
-  cmp_ok($stats->{'SVP_ROLLBACK'},'==',5,'Correct number of savepoint rollbacks');
-
-  $schema->storage->dbh->do ("DROP TABLE artist");
-}}
-
-done_testing;
-
-END {
-  eval { $schema->storage->dbh->do ("DROP TABLE artist") } if defined $schema;
-  undef $schema;
-}
index b8b57cf..a9e708f 100644 (file)
@@ -1,6 +1,10 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'deploy';
+
 use strict;
 use warnings;
 
+BEGIN { $ENV{DBICTEST_VIA_REPLICATED} = 0 }
+
 use Test::More;
 use Test::Warn;
 use Test::Exception;
@@ -10,13 +14,6 @@ use lib qw(t/lib);
 use DBICTest;
 use DBIx::Class::_Util 'sigwarn_silencer';
 
-BEGIN {
-  require DBIx::Class;
-  plan skip_all =>
-      'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('deploy')
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('deploy')
-}
-
 # Test for SQLT-related leaks
 {
   my $s = DBICTest::Schema->clone;
@@ -53,8 +50,7 @@ lives_ok { isa_ok (create_schema ({ schema => 'DBICTest::Schema' }), 'SQL::Trans
 # make sure a connected instance passed via $args does not get the $dbh improperly serialized
 SKIP: {
 
-  # YAML is a build_requires dep of SQLT - it may or may not be here
-  eval { require YAML } or skip "Test requires YAML.pm", 1;
+  DBIx::Class::Optional::Dependencies->skip_without( 'YAML>=0' );
 
   lives_ok {
 
@@ -259,6 +255,28 @@ lives_ok (sub {
   }, 'partial schema tests successful');
 }
 
+{
+  my $cd_rsrc = $schema->source('CD');
+  $cd_rsrc->name(\'main.cd');
+
+  my $sqlt_schema = create_schema(
+    { schema => $schema },
+    args => { ignore_constraint_names => 0, ignore_index_names => 0 }
+  );
+
+  foreach my $source_name (qw(CD)) {
+    my $table = get_table($sqlt_schema, $schema, $source_name);
+    ok(
+      !(grep {$_->name =~ m/main\./} $table->get_indices),
+      'indices have periods stripped out'
+    );
+    ok(
+      !(grep {$_->name =~ m/main\./} $table->get_constraints),
+      'constraints have periods stripped out'
+    );
+  }
+}
+
 done_testing;
 
 sub create_schema {
diff --git a/t/admin/01load.t b/t/admin/01load.t
deleted file mode 100644 (file)
index 3bdaeb6..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-use strict;
-use warnings;
-
-use Test::More;
-
-use lib 't/lib';
-use DBICTest;
-
-BEGIN {
-    require DBIx::Class;
-    plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for('admin')
-      unless DBIx::Class::Optional::Dependencies->req_ok_for('admin');
-}
-
-use_ok 'DBIx::Class::Admin';
-
-
-done_testing;
index 1d9ce88..b2414c3 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( admin deploy );
+
 use strict;
 use warnings;
 
@@ -11,16 +13,7 @@ use lib qw(t/lib);
 use DBICTest;
 use DBIx::Class::_Util 'sigwarn_silencer';
 
-BEGIN {
-    require DBIx::Class;
-    plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for('admin')
-      unless DBIx::Class::Optional::Dependencies->req_ok_for('admin');
-
-    plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for('deploy')
-      unless DBIx::Class::Optional::Dependencies->req_ok_for('deploy');
-}
-
-use_ok 'DBIx::Class::Admin';
+use DBIx::Class::Admin;
 
 # lock early
 DBICTest->init_schema(no_deploy => 1, no_populate => 1);
@@ -108,13 +101,15 @@ my $admin = DBIx::Class::Admin->new(
 );
 
 $admin->version("3.0");
-lives_ok { $admin->install(); } 'install schema version 3.0';
+$admin->install;
 is($admin->schema->get_db_version, "3.0", 'db thinks its version 3.0');
-dies_ok { $admin->install("4.0"); } 'cannot install to allready existing version';
+throws_ok {
+  $admin->install("4.0")
+} qr/Schema already has a version. Try upgrade instead/, 'cannot install to allready existing version';
 
 $admin->force(1);
 warnings_exist ( sub {
-  lives_ok { $admin->install("4.0") } 'can force install to allready existing version'
+  $admin->install("4.0")
 }, qr/Forcing install may not be a good idea/, 'Force warning emitted' );
 is($admin->schema->get_db_version, "4.0", 'db thinks its version 4.0');
 }
index ee35001..d73f619 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'admin';
+
 use strict;
 use warnings;
 
@@ -7,14 +9,7 @@ use Test::Exception;
 use lib 't/lib';
 use DBICTest;
 
-BEGIN {
-    require DBIx::Class;
-    plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for('admin')
-      unless DBIx::Class::Optional::Dependencies->req_ok_for('admin');
-}
-
-use_ok 'DBIx::Class::Admin';
-
+use DBIx::Class::Admin;
 
 { # test data maniplulation functions
 
index 275ed4a..76bce52 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
 
@@ -97,8 +99,8 @@ ok(!State->find_column('HGLAGAGlAG'), '!find_column HGLAGAGlAG');
 }
 
 {
-  SKIP: {
-    skip "No column objects", 1;
+  {
+    local $TODO = "No column objects";
 
     eval { my @grps = State->__grouper->groups_for("Huh"); };
     ok $@, "Huh not in groups";
index fe4a691..7a6f9e9 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
 
@@ -6,10 +8,8 @@ use Scalar::Util 'refaddr';
 use namespace::clean;
 $| = 1;
 
-INIT {
-  use lib 't/cdbi/testlib';
-  use Film;
-}
+use lib 't/cdbi/testlib';
+use Film;
 
 ok(Film->can('db_Main'), 'set_db()');
 is(Film->__driver, "SQLite", "Driver set correctly");
@@ -32,7 +32,7 @@ is(Film->__driver, "SQLite", "Driver set correctly");
 }
 
 eval { my $duh = Film->insert; };
-like $@, qr/create needs a hashref/, "needs a hashref";
+like $@, qr/Result object instantiation requires a hashref as argument/, "needs a hashref";
 
 ok +Film->create_test_film;
 
@@ -371,9 +371,7 @@ if (0) {
   ok !$film, "It destroys itself";
 }
 
-SKIP: {
-    skip "Caching has been removed", 5
-        if Film->isa("DBIx::Class::CDBICompat::NoObjectIndex");
+{
 
   # my bad taste is your bad taste
   my $btaste  = Film->retrieve('Bad Taste');
index 767b341..8a73a09 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
 
index d89d1b4..2e37827 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 use Test::Warn;
 
@@ -7,10 +10,8 @@ use Test::Warn;
 # Test lazy loading
 #----------------------------------------------------------------------
 
-INIT {
-  use lib 't/cdbi/testlib';
-  use Lazy;
-}
+use lib 't/cdbi/testlib';
+use Lazy;
 
 is_deeply [ Lazy->columns('Primary') ],        [qw/this/],      "Pri";
 is_deeply [ sort Lazy->columns('Essential') ], [qw/opop this/], "Essential";
@@ -109,7 +110,7 @@ warning_like {
 
 # Now again for inflated values
 SKIP: {
-    skip "Requires Date::Simple 3.03", 5 unless eval "use Date::Simple 3.03; 1; ";
+    DBIx::Class::Optional::Dependencies->skip_without( 'Date::Simple>=3.03' );
     Lazy->has_a(
         orp     => 'Date::Simple',
         inflate => sub { Date::Simple->new($_[0] . '-01-01') },
index 255383b..d191b65 100644 (file)
@@ -1,16 +1,17 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
+use Test::Exception;
+use DBIx::Class::_Util 'sigwarn_silencer';
 
 @YA::Film::ISA = 'Film';
 
-#local $SIG{__WARN__} = sub { };
-
-INIT {
-  use lib 't/cdbi/testlib';
-  use Film;
-  use Director;
-}
+use lib 't/cdbi/testlib';
+use Film;
+use Director;
 
 Film->create_test_film;
 ok(my $btaste = Film->retrieve('Bad Taste'), "We have Bad Taste");
@@ -105,7 +106,8 @@ sub taste_bad {
 
 sub fail_with_bad_object {
   my ($dir, $codir) = @_;
-  eval {
+  throws_ok {
+    local $SIG{__WARN__} = sigwarn_silencer( qr/\Qusually should inherit from the related ResultClass ('Director')/ );
     YA::Film->create(
       {
         Title             => 'Tastes Bad',
@@ -115,8 +117,7 @@ sub fail_with_bad_object {
         NumExplodingSheep => 23
       }
     );
-  };
-  ok $@, $@;
+  } qr/isn't a Director/;
 }
 
 package Foo;
index 5550e59..bc9b90a 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
index 89a59a5..a19500a 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
@@ -46,7 +49,7 @@ eval { my $pj = Film->add_to_actors(\%pj_data) };
 like $@, qr/class/, "add_to_actors must be object method";
 
 eval { my $pj = $btaste->add_to_actors(%pj_data) };
-like $@, qr/expects a hashref/, "add_to_actors takes hash";
+like $@, qr/Result object instantiation requires a hashref as argument/, "add_to_actors takes hash";
 
 ok(
   my $pj = $btaste->add_to_actors(
index c944248..cd322e5 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
index 9a715ed..de68fa1 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
index 4191fe5..ba9f654 100644 (file)
@@ -1,6 +1,10 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
+use Test::Exception;
 
 use lib 't/cdbi/testlib';
 use Film;
@@ -58,10 +62,10 @@ ok $fred, "Got fred";
     like $@, qr/fails.*constraint/, "Fails listref constraint";
     my $ok = eval { Film->create({ Rating => 'U' }) };
     is $@, '', "Can create with rating U";
-    SKIP: {
-        skip "No column objects", 2;
-    ok +Film->find_column('rating')->is_constrained, "Rating is constrained";
-    ok +Film->find_column('director')->is_constrained, "Director is not";
+    {
+      local $TODO = "No column objects";
+      lives_ok { Film->find_column('rating')->is_constrained || die } "Rating is constrained";
+      lives_ok { Film->find_column('director')->is_constrained || die } "Director is not";
     }
 }
 
index 73318ac..52a2abd 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
index 267916d..85f8464 100644 (file)
@@ -1,19 +1,20 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
+use lib 't/cdbi/testlib';
 INIT {
-    #local $SIG{__WARN__} =
-        #sub { like $_[0], qr/clashes with built-in method/, $_[0] };
-    use lib 't/cdbi/testlib';
-    require Film;
-    require Actor;
-    require Director;
+  require Film;
+  require Actor;
+  require Director;
 
-    Actor->has_a(film => 'Film');
-    Film->has_a(director => 'Director');
+  Actor->has_a(film => 'Film');
+  Film->has_a(director => 'Director');
 
-    sub Class::DBI::sheep { ok 0; }
+  sub Class::DBI::sheep { ok 0; }
 }
 
 # Install the deprecation warning intercept here for the rest of the 08 dev cycle
index e54d0ae..ce8a4b3 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
index 1dacd6c..dfb5819 100644 (file)
@@ -1,6 +1,11 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
+use Test::Exception;
+use DBIx::Class::_Util 'sigwarn_silencer';
 
 use lib 't/cdbi/testlib';
 use Film;
@@ -45,8 +50,8 @@ my $sj = Director->create({
   });
 
 {
-  eval { $btaste->Director($btaste) };
-  like $@, qr/Director/, "Can't set film as director";
+  throws_ok { $btaste->Director($btaste) }
+    qr/isn't a Director/, "Can't set film as director";
   is $btaste->Director->id, $pj->id, "PJ still the director";
 
   # drop from cache so that next retrieve() is from db
@@ -69,8 +74,7 @@ my $sj = Director->create({
 is $sj->id, 'Skippy Jackson', 'Create new director - Skippy';
 Film->has_a('CoDirector' => 'Director');
 {
-  eval { $btaste->CoDirector("Skippy Jackson") };
-  is $@, "", "Auto inflates";
+  lives_ok { $btaste->CoDirector("Skippy Jackson") };
   isa_ok $btaste->CoDirector, "Director";
   is $btaste->CoDirector->id, $sj->id, "To skippy";
 }
@@ -96,7 +100,8 @@ is(
   $pj = Director->retrieve('Peter Jackson');
 
   my $fail;
-  eval {
+  throws_ok {
+    local $SIG{__WARN__} = sigwarn_silencer( qr/\Qusually should inherit from the related ResultClass ('Director')/ );
     $fail = YA::Film->create({
         Title             => 'Tastes Bad',
         Director          => $sj,
@@ -104,8 +109,7 @@ is(
         Rating            => 'R',
         NumExplodingSheep => 23
       });
-  };
-  ok $@,    "Can't have film as codirector: $@";
+  } qr/isn't a Director/, "Can't have film as codirector";
   is $fail, undef, "We didn't get anything";
 
   my $tastes_bad = YA::Film->create({
@@ -226,8 +230,10 @@ SKIP: {
 }
 
 { # Broken has_a declaration
-  eval { Film->has_a(driector => "Director") };
-  like $@, qr/driector/, "Sensible error from has_a with incorrect column: $@";
+  throws_ok{ Film->has_a(driector => "Director") }
+    qr/No such column driector/,
+    "Sensible error from has_a with incorrect column"
+  ;
 }
 
 done_testing;
index 41040af..a981810 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
index f49bf68..14a1b30 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
index d4e9935..71d8d7d 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( cdbicompat test_rdbms_mysql Time::Piece::MySQL>=0 );
+
 $| = 1;
 use warnings;
 use strict;
@@ -5,10 +7,6 @@ use strict;
 use Test::More;
 
 use lib 't/cdbi/testlib';
-use DBIC::Test::SQLite (); # this will issue the necessary SKIPs on missing reqs
-
-eval { require Time::Piece::MySQL }
-  or plan skip_all => 'Time::Piece::MySQL required for this test';
 
 use_ok ('Log');
 
index 1538ef4..43ad050 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
index eb9c3f5..809f458 100644 (file)
@@ -1,13 +1,14 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 use Data::Dumper;
 
-INIT {
-    use lib 't/cdbi/testlib';
-    use Film;
-    use Director;
-}
+use lib 't/cdbi/testlib';
+use Film;
+use Director;
 
 { # Cascade on delete
     Director->has_many(nasties => 'Film');
index f2fc57f..703e3fd 100644 (file)
@@ -1,14 +1,11 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( cdbicompat Time::Piece>=0 );
+
 use strict;
 use warnings;
+
 use Test::More;
 use Test::Warn;
 
-use lib 't/cdbi/testlib';
-use DBIC::Test::SQLite (); # this will issue the necessary SKIPs on missing reqs
-
-eval { require Time::Piece }
-  or plan skip_all => 'Time::Piece required for this test';
-
 package Temp::DBI;
 use base qw(DBIx::Class::CDBICompat);
 Temp::DBI->columns(All => qw(id date));
index a203059..54a4229 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
index 9ee838a..d192d97 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
index 04abccb..37eac4b 100644 (file)
@@ -1,14 +1,11 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt cdbicompat );
+
 use strict;
 use warnings;
-use Test::More;
 
-use lib qw(t/cdbi/testlib);
-use DBIC::Test::SQLite (); # this will issue the necessary SKIPs on missing reqs
-
-BEGIN {
-  eval { require DateTime; DateTime->VERSION(0.55) }
-    or plan skip_all => 'DateTime 0.55 required for this test';
-}
+use Test::More;
+use lib 't/lib';
+use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
@@ -18,7 +15,7 @@ DBICTest::Schema::CD->has_a( 'year', 'DateTime',
       inflate => sub { DateTime->new( year => shift ) },
       deflate => sub { shift->year }
 );
-Class::C3->reinitialize;
+Class::C3->reinitialize if DBIx::Class::_ENV_::OLD_MRO;
 
 # inflation test
 my $cd = $schema->resultset("CD")->find(3);
diff --git a/t/cdbi/70_implicit_inflate.t b/t/cdbi/70_implicit_inflate.t
new file mode 100644 (file)
index 0000000..fa53816
--- /dev/null
@@ -0,0 +1,32 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( cdbicompat rdbms_sqlite ic_dt );
+
+use strict;
+use warnings;
+
+# Class::DBI in its infinate wisdom allows implicit inflation
+# and deflation of foriegn clas looups in has_a relationships.
+# for inflate it would call ->new on the foreign_class and for
+# deflate it would "" the column value and allow for overloading
+# of the "" operator.
+
+use Test::More;
+
+use lib 't/cdbi/testlib';
+use ImplicitInflate;
+
+ok(ImplicitInflate->can('db_Main'), 'set_db()');
+is(ImplicitInflate->__driver, "SQLite", 'Driver set correctly');
+
+my $now = DateTime->now;
+
+ImplicitInflate->create({
+  update_datetime => $now,
+  text            => "Test Data",
+});
+
+my $implicit_inflate = ImplicitInflate->retrieve(text => 'Test Data');
+
+ok($implicit_inflate->update_datetime->isa('DateTime'), 'Date column inflated correctly');
+is($implicit_inflate->update_datetime => $now, 'Date has correct year');
+
+done_testing;
diff --git a/t/cdbi/71_column_object.t b/t/cdbi/71_column_object.t
new file mode 100644 (file)
index 0000000..e00820b
--- /dev/null
@@ -0,0 +1,30 @@
+# Columns in CDBI could be defined as Class::DBI::Column objects rather than
+# or as well as with __PACKAGE__->columns();
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( cdbicompat Class::DBI>=3.000005 );
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib 't/cdbi/testlib';
+use ColumnObject;
+
+ok(ColumnObject->can('db_Main'), 'set_db()');
+is(ColumnObject->__driver, 'SQLite', 'Driver set correctly');
+
+ColumnObject->create({
+  columna => 'Test Data',
+  columnb => 'Test Data 2',
+});
+
+my $column_object = ColumnObject->retrieve(columna => 'Test Data');
+$column_object->columnb_as_write('Test Data Written');
+$column_object->update;
+$column_object = ColumnObject->retrieve(columna => 'Test Data');
+
+is($column_object->columna_as_read => 'Test Data', 'Read column via accessor');
+is($column_object->columna         => 'Test Data', 'Real column returns right data');
+is($column_object->columnb         => 'Test Data Written', 'ColumnB wrote via mutator');
+
+done_testing;
index 1a42e03..9a993c4 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 #----------------------------------------------------------------------
index 95ad021..f4911c7 100644 (file)
@@ -1,14 +1,12 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( cdbicompat Class::DBI::Plugin::DeepAbstractSearch>=0 );
+
 use strict;
 use warnings;
-use Test::More;
 
-use lib 't/cdbi/testlib';
-use DBIC::Test::SQLite (); # this will issue the necessary SKIPs on missing reqs
+use Test::More;
 
-BEGIN {
-  eval { require Class::DBI::Plugin::DeepAbstractSearch }
-    or plan skip_all => 'Class::DBI::Plugin::DeepAbstractSearch required for this test';
-}
+use lib 't/lib';
+use DBICTest;
 
 my $DB = DBICTest->_sqlite_dbname(sqlite_use_file => 1);;
 
index a14682f..2c15ecc 100644 (file)
@@ -1,13 +1,12 @@
-use Test::More;
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
 
 use strict;
 use warnings;
 
-INIT {
-  use lib 't/cdbi/testlib';
-  use Film;
-}
+use Test::More;
 
+use lib 't/cdbi/testlib';
+use Film;
 
 Film->create({ Title => $_, Rating => "PG" }) for ("Superman", "Super Fuzz");
 Film->create({ Title => "Batman", Rating => "PG13" });
index f10f522..9731ae3 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 use Test::Warn;
 
@@ -86,11 +89,8 @@ warning_is {
 
 # Emulate that Class::DBI inflates immediately
 SKIP: {
-    unless (eval { require MyFoo }) {
-      my ($err) = $@ =~ /([^\n]+)/;
-      skip $err, 3
-    }
-
+    DBIx::Class::Optional::Dependencies->skip_without([qw( Date::Simple>=3.03 test_rdbms_mysql )]);
+    require MyFoo;
     my $foo = MyFoo->insert({
         name    => 'Whatever',
         tdate   => '1949-02-01',
index 27a96fd..5748b6e 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 use lib 't/cdbi/testlib';
 
index 1ce8160..d10e6a1 100644 (file)
@@ -1,11 +1,12 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
-INIT {
-    use lib 't/cdbi/testlib';
-    use Film;
-}
+use lib 't/cdbi/testlib';
+use Film;
 
 {
     Film->insert({
index 6c079cc..f587ae0 100644 (file)
@@ -1,10 +1,11 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
-INIT {
-    use lib 't/cdbi/testlib';
-}
+use lib 't/cdbi/testlib';
 
 {
     package # hide from PAUSE
index 859d43d..e91b401 100644 (file)
@@ -1,11 +1,10 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
 
 use Test::More;
 
-use lib 't/cdbi/testlib';
-use DBIC::Test::SQLite (); # this will issue the necessary SKIPs on missing reqs
-
 {
     package Thing;
     use base qw(DBIx::Class::CDBICompat);
index bba66e8..5485972 100644 (file)
@@ -1,15 +1,17 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
-use Test::More;
-use Class::Inspector ();
 
+use Test::More;
 
 use lib 't/cdbi/testlib';
 use Director;
 
-# Test that has_many() will load the foreign class.
+# Test that has_many() will load the foreign class
+require Class::Inspector;
 ok !Class::Inspector->loaded( 'Film' );
-ok eval { Director->has_many( films => 'Film' ); 1; } || diag $@;
+ok eval { Director->has_many( films => 'Film' ); 1; } or diag $@;
 
 my $shan_hua = Director->create({
     Name    => "Shan Hua",
index 5f92df2..3b92c4d 100644 (file)
@@ -1,9 +1,11 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 use lib 't/cdbi/testlib';
-use DBIC::Test::SQLite (); # this will issue the necessary SKIPs on missing reqs
 
 package Foo;
 
index 5dc2f1a..aba3821 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
 #----------------------------------------------------------------------
index 08adeef..5fc1994 100644 (file)
@@ -1,11 +1,12 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
-INIT {
-    use lib 't/cdbi/testlib';
-    require Film;
-}
+use lib 't/cdbi/testlib';
+INIT { require Film }
 
 sub Film::get_test {
     my $self = shift;
index 8ca3bcf..1f1d1ac 100644 (file)
@@ -1,5 +1,8 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 use lib 't/cdbi/testlib';
 
index c349940..db0dc06 100644 (file)
@@ -1,19 +1,14 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 $| = 1;
 
 
-INIT {
-    use lib 't/cdbi/testlib';
-    use Film;
-}
-
-plan skip_all => "Object cache is turned off"
-    if Film->isa("DBIx::Class::CDBICompat::NoObjectIndex");
-
-plan tests => 5;
-
+use lib 't/cdbi/testlib';
+use Film;
 
 ok +Film->create({
     Title       => 'This Is Spinal Tap',
index a73358c..4045361 100644 (file)
@@ -1,11 +1,12 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
+
 use Test::More;
 
-INIT {
-    use lib 't/cdbi/testlib';
-    use Film;
-}
+use lib 't/cdbi/testlib';
+use Film;
 
 for my $title ("Bad Taste", "Braindead", "Forgotten Silver") {
     Film->insert({ Title => $title, Director => 'Peter Jackson' });
index 03fe0ca..5b642e0 100644 (file)
@@ -1,15 +1,10 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt cdbicompat );
+
 use strict;
 use warnings;
-use Test::More;
 
+use Test::More;
 use lib 't/cdbi/testlib';
-use DBIC::Test::SQLite (); # this will issue the necessary SKIPs on missing reqs
-
-BEGIN {
-  eval { require DateTime; DateTime->VERSION(0.55) }
-    or plan skip_all => 'DateTime 0.55 required for this test';
-}
-
 
 # Don't use Test::NoWarnings because of an unrelated DBD::SQLite warning.
 my @warnings;
index 08fe4c9..2fe0879 100644 (file)
@@ -1,15 +1,12 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt cdbicompat );
+
 use strict;
 use warnings;
+
 use Test::More;
 use Test::Exception;
 
 use lib 't/cdbi/testlib';
-use DBIC::Test::SQLite (); # this will issue the necessary SKIPs on missing reqs
-
-BEGIN {
-  eval { require DateTime; DateTime->VERSION(0.55) }
-    or plan skip_all => 'DateTime 0.55 required for this test';
-}
 
 {
     package Thing;
index f645276..7f94e51 100644 (file)
@@ -1,10 +1,12 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'cdbicompat';
+
 use strict;
 use warnings;
 
 use Test::More;
 
-use lib 't/cdbi/testlib';
-use DBIC::Test::SQLite;
+use lib 't/lib';
+use DBICTest;
 
 DBICTest::Schema::CD->load_components(qw/CDBICompat CDBICompat::Pager/);
 
@@ -61,15 +63,4 @@ is( $it->next, undef, "disable_sql_paging next past end of page ok" );
 );
 is( $it->count, 1, "complex abstract count ok" );
 
-# cleanup globals so we do not trigger the leaktest
-for ( map { DBICTest->schema->class($_) } DBICTest->schema->sources ) {
-  $_->class_resolver(undef);
-  $_->resultset_instance(undef);
-  $_->result_source_instance(undef);
-}
-{
-  no warnings qw/redefine once/;
-  *DBICTest::schema = sub {};
-}
-
 done_testing;
diff --git a/t/cdbi/testlib/ColumnObject.pm b/t/cdbi/testlib/ColumnObject.pm
new file mode 100644 (file)
index 0000000..11eeb89
--- /dev/null
@@ -0,0 +1,29 @@
+package # Hide from PAUSE
+    ColumnObject;
+
+use strict;
+use warnings;
+
+use base 'DBIC::Test::SQLite';
+use Class::DBI::Column;
+
+__PACKAGE__->set_table('column_object');
+
+__PACKAGE__->columns( Primary => 'id' );
+__PACKAGE__->columns( All => (
+  'id',
+  'columna',
+  'columnb',
+  Class::DBI::Column->new('columna' => {accessor => 'columna_as_read'}),
+  Class::DBI::Column->new('columnb' => {mutator  => 'columnb_as_write'}),
+));
+
+sub create_sql {
+  return qq{
+    id       INTEGER PRIMARY KEY,
+    columna  VARCHAR(20),
+    columnb  VARCHAR(20)
+  }
+}
+
+1;
index 5dc4a66..76822cd 100644 (file)
@@ -1,6 +1,9 @@
 package # hide from PAUSE
     DBIC::Test::SQLite;
 
+use strict;
+use warnings;
+
 =head1 NAME
 
 DBIx::Class::Test::SQLite - Base class for running Class::DBI tests against DBIx::Class compat layer, shamelessly ripped from Class::DBI::Test::SQLite
@@ -31,19 +34,14 @@ table, and tie it to the class.
 
 =cut
 
-use strict;
-use warnings;
-
-use Test::More;
+# adding implicit search criteria to the iterator will alter the test
+# mechanics - leave everything as-is instead, and hope SQLite won't
+# change too much
+BEGIN { $ENV{DBIC_SHUFFLE_UNORDERED_RESULTSETS} = 0 }
 
 use lib 't/lib';
 use DBICTest;
 
-BEGIN {
-  eval { require DBIx::Class::CDBICompat }
-    or plan skip_all => 'Class::DBI required for this test';
-}
-
 use base qw/DBIx::Class/;
 
 __PACKAGE__->load_components(qw/CDBICompat Core DB/);
diff --git a/t/cdbi/testlib/ImplicitInflate.pm b/t/cdbi/testlib/ImplicitInflate.pm
new file mode 100644 (file)
index 0000000..610e835
--- /dev/null
@@ -0,0 +1,42 @@
+package # Hide from PAUSE
+  ImplicitInflate;
+
+# Test class for the testing of Implicit inflation
+# in CDBI Classes using Compat layer
+# See t/cdbi/70-implicit_inflate.t
+
+use strict;
+use warnings;
+
+use base 'DBIC::Test::SQLite';
+
+__PACKAGE__->set_table('Date');
+
+__PACKAGE__->columns( Primary => 'id' );
+__PACKAGE__->columns( All => qw/ update_datetime text/);
+
+__PACKAGE__->has_a(
+  update_datetime => 'MyDateStamp',
+);
+
+sub create_sql {
+  # SQLite doesn't support Datetime datatypes.
+  return qq{
+    id              INTEGER PRIMARY KEY,
+    update_datetime TEXT,
+    text            VARCHAR(20)
+  }
+}
+
+{
+  package MyDateStamp;
+
+  use DateTime::Format::SQLite;
+
+  sub new {
+    my ($self, $value) = @_;
+    return DateTime::Format::SQLite->parse_datetime($value);
+  }
+}
+
+1;
index 4f90ed1..362b61e 100644 (file)
@@ -7,7 +7,7 @@ use strict;
 use base 'MyBase';
 
 use Time::Piece::MySQL;
-use POSIX;
+use POSIX ();
 
 __PACKAGE__->set_table();
 __PACKAGE__->columns(All => qw/id message datetime_stamp/);
index bf55635..1fe9317 100644 (file)
@@ -11,36 +11,19 @@ use DBICTest;
 
 use base qw(DBIx::Class::CDBICompat);
 
-our $dbh;
-
-my $err;
-if (! $ENV{DBICTEST_MYSQL_DSN} ) {
-  $err = 'Set $ENV{DBICTEST_MYSQL_DSN}, _USER and _PASS to run this test';
-}
-elsif ( ! DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_mysql') ) {
-  $err = 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_mysql')
-}
-
-if ($err) {
-  my $t = eval { Test::Builder->new };
-  if ($t and ! $t->current_test) {
-    $t->skip_all ($err);
-  }
-  else {
-    die "$err\n";
-  }
-}
-
 my @connect = (@ENV{map { "DBICTEST_MYSQL_${_}" } qw/DSN USER PASS/}, { PrintError => 0});
 # this is only so we grab a lock on mysql
 {
   my $x = DBICTest::Schema->connect(@connect);
 }
 
-$dbh = DBI->connect(@connect) or die DBI->errstr;
+our $dbh = DBI->connect(@connect) or die DBI->errstr;
 my @table;
 
-END { $dbh->do("DROP TABLE $_") foreach @table }
+END {
+  $dbh->do("DROP TABLE $_") for @table;
+  undef $dbh;
+}
 
 __PACKAGE__->connection(@connect);
 
index 11a4feb..7df9c6f 100644 (file)
@@ -6,8 +6,6 @@ use strict;
 
 use base 'MyBase';
 
-use Date::Simple 3.03;
-
 __PACKAGE__->set_table();
 __PACKAGE__->columns(All => qw/myid name val tdate/);
 __PACKAGE__->has_a(
index 83b6257..174f630 100644 (file)
@@ -4,9 +4,7 @@ use warnings;
 use lib qw(t/lib);
 
 use Test::More;
-use DBICTest;
-use DBIC::SqlMakerTest;
-use DBIC::DebugObj;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 
 my ($ROWS, $OFFSET) = (
@@ -23,27 +21,25 @@ my $schema = DBICTest->init_schema();
                 { position => [1,2] },
                 { prefetch => [qw/disc lyrics/], rows => 3, offset => 8 },
             );
-  is ($rs->all, 2, 'Correct number of objects');
-
-
-  my ($sql, @bind);
-  $schema->storage->debugobj(DBIC::DebugObj->new(\$sql, \@bind));
-  $schema->storage->debug(1);
+  my @wherebind = (
+    [ { sqlt_datatype => 'int', dbic_colname => 'position' }
+      => 1 ],
+    [ { sqlt_datatype => 'int', dbic_colname => 'position' }
+      => 2 ],
+  );
 
-  is ($rs->count, 2, 'Correct count via count()');
+  is ($rs->all, 2, 'Correct number of objects');
 
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    is ($rs->count, 2, 'Correct count via count()');
+  }, [[
     'SELECT COUNT( * )
       FROM cd me
       JOIN track tracks ON tracks.cd = me.cdid
       JOIN cd disc ON disc.cdid = tracks.cd
      WHERE ( ( position = ? OR position = ? ) )
-    ',
-    [ qw/'1' '2'/ ],
-    'count softlimit applied',
-  );
+    ', @wherebind
+  ]], 'count softlimit applied');
 
   my $crs = $rs->count_rs;
   is ($crs->next, 2, 'Correct count via count_rs()');
@@ -60,14 +56,7 @@ my $schema = DBICTest->init_schema();
         LIMIT ? OFFSET ?
        ) tracks
     )',
-    [
-      [ { sqlt_datatype => 'int', dbic_colname => 'position' }
-        => 1 ],
-      [ { sqlt_datatype => 'int', dbic_colname => 'position' }
-        => 2 ],
-      [$ROWS => 3],
-      [$OFFSET => 8],
-    ],
+    [ @wherebind, [$ROWS => 3], [$OFFSET => 8] ],
     'count_rs db-side limit applied',
   );
 }
@@ -79,17 +68,18 @@ my $schema = DBICTest->init_schema();
                 { 'tracks.position' => [1,2] },
                 { prefetch => [qw/tracks artist/], rows => 3, offset => 4 },
             );
-  is ($rs->all, 1, 'Correct number of objects');
-
-  my ($sql, @bind);
-  $schema->storage->debugobj(DBIC::DebugObj->new(\$sql, \@bind));
-  $schema->storage->debug(1);
+  my @wherebind = (
+    [ { sqlt_datatype => 'int', dbic_colname => 'tracks.position' }
+      => 1 ],
+    [ { sqlt_datatype => 'int', dbic_colname => 'tracks.position' }
+      => 2 ],
+  );
 
-  is ($rs->count, 1, 'Correct count via count()');
+  is ($rs->all, 1, 'Correct number of objects');
 
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    is ($rs->count, 1, 'Correct count via count()');
+  }, [ [
     'SELECT COUNT( * )
       FROM (
         SELECT cds.cdid
@@ -100,10 +90,8 @@ my $schema = DBICTest->init_schema();
         WHERE tracks.position = ? OR tracks.position = ?
         GROUP BY cds.cdid
       ) cds
-    ',
-    [ qw/'1' '2'/ ],
-    'count softlimit applied',
-  );
+    ', @wherebind
+  ]], 'count softlimit applied' );
 
   my $crs = $rs->count_rs;
   is ($crs->next, 1, 'Correct count via count_rs()');
@@ -122,14 +110,7 @@ my $schema = DBICTest->init_schema();
         LIMIT ? OFFSET ?
       ) cds
     )',
-    [
-      [ { sqlt_datatype => 'int', dbic_colname => 'tracks.position' }
-        => 1 ],
-      [ { sqlt_datatype => 'int', dbic_colname => 'tracks.position' }
-        => 2 ],
-      [ $ROWS => 3],
-      [$OFFSET => 4],
-    ],
+    [ @wherebind, [$ROWS => 3], [$OFFSET => 4], ],
     'count_rs db-side limit applied',
   );
 }
index 1b44b9a..e916ab9 100644 (file)
@@ -6,8 +6,7 @@ use Test::Exception;
 
 use lib qw(t/lib);
 
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
index 8f56d83..85f48d0 100644 (file)
@@ -5,7 +5,6 @@ use Test::More;
 
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::SqlMakerTest;
 
 my $schema = DBICTest->init_schema();
 
index 352eef9..bb8eb4c 100644 (file)
@@ -7,24 +7,37 @@ use lib qw(t/lib);
 
 use DBICTest;
 
-plan tests => 7;
-
 my $schema = DBICTest->init_schema();
 
 my $cds = $schema->resultset("CD")->search({ cdid => 1 }, { join => { cd_to_producer => 'producer' } });
 cmp_ok($cds->count, '>', 1, "extra joins explode entity count");
 
-is (
-  $cds->search({}, { prefetch => 'cd_to_producer' })->count,
-  1,
-  "Count correct with extra joins collapsed by prefetch"
-);
-
-is (
-  $cds->search({}, { distinct => 1 })->count,
-  1,
-  "Count correct with requested distinct collapse of main table"
-);
+for my $arg (
+  [ 'prefetch-collapsed has_many' => { prefetch => 'cd_to_producer' } ],
+  [ 'distict-collapsed result' => { distinct => 1 } ],
+  [ 'explicit collapse request' => { collapse => 1 } ],
+) {
+  for my $hri (0,1) {
+    my $diag = $arg->[0] . ($hri ? ' with HRI' : '');
+
+    my $rs = $cds->search({}, {
+      %{$arg->[1]},
+      $hri ? ( result_class => 'DBIx::Class::ResultClass::HashRefInflator' ) : (),
+    });
+
+    is
+      $rs->count,
+      1,
+      "Count correct on $diag",
+    ;
+
+    is
+      scalar $rs->all,
+      1,
+      "Amount of constructed objects matches count on $diag",
+    ;
+  }
+}
 
 # JOIN and LEFT JOIN issues mean that we've seen problems where counted rows and fetched rows are sometimes 1 higher than they should
 # be in the related resultset.
@@ -35,3 +48,5 @@ is(scalar($artist->related_resultset('cds')->all()), 0, "No CDs fetched for a sh
 my $artist_rs = $schema->resultset('Artist')->search({artistid => $artist->id});
 is($artist_rs->related_resultset('cds')->count(), 0, "No CDs counted for a shiny new artist using a resultset search");
 is(scalar($artist_rs->related_resultset('cds')->all), 0, "No CDs fetched for a shiny new artist using a resultset search");
+
+done_testing;
index 25ae856..eb18236 100644 (file)
@@ -4,8 +4,7 @@ use warnings;
 use lib qw(t/lib);
 
 use Test::More;
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
index 03de883..8bd8a76 100644 (file)
@@ -11,13 +11,13 @@ use DBICTest;
 my $schema = DBICTest->init_schema();
 $schema->_unregister_source('CD');
 
-warnings_like {
+warnings_exist {
   my $s = $schema;
   lives_ok {
     $_->delete for $s->resultset('Artist')->all;
   } 'delete on rows with dangling rels lives';
 } [
-  # 12 == 3 artists * failed cascades:
+  # 9 == 3 artists * failed cascades:
   #   cds
   #   cds_unordered
   #   cds_very_very_very_long_relationship_name
similarity index 95%
rename from t/inflate/core.t
rename to t/icdt/core.t
index aadc4af..8f0c83c 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( test_rdbms_sqlite ic_dt );
+
 use strict;
 use warnings;
 
@@ -8,9 +10,6 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-plan skip_all => 'Inflation tests need ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt');
-
 $schema->class('CD') ->inflate_column( 'year',
     { inflate => sub { DateTime->new( year => shift ) },
       deflate => sub { shift->year } }
similarity index 76%
rename from t/inflate/datetime_firebird.t
rename to t/icdt/engine_specific/firebird.t
index c958d6b..05ef381 100644 (file)
@@ -1,11 +1,12 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt _rdbms_firebird_common );
+
 use strict;
 use warnings;
 
 use Test::More;
-use DBIx::Class::Optional::Dependencies ();
+use DBIx::Class::_Util 'scope_guard';
 use lib qw(t/lib);
 use DBICTest;
-use Scope::Guard ();
 
 my $env2optdep = {
   DBICTEST_FIREBIRD => 'test_rdbms_firebird',
@@ -13,16 +14,14 @@ my $env2optdep = {
   DBICTEST_FIREBIRD_ODBC => 'test_rdbms_firebird_odbc',
 };
 
-plan skip_all => join (' ',
-  'Set $ENV{DBICTEST_FIREBIRD_DSN} and/or $ENV{DBICTEST_FIREBIRD_INTERBASE_DSN}',
-  'and/or $ENV{DBICTEST_FIREBIRD_ODBC_DSN},',
-  '_USER and _PASS to run these tests.',
-
-  "WARNING: This test drops and creates a table called 'event'",
-) unless grep { $ENV{"${_}_DSN"} } keys %$env2optdep;
-
-plan skip_all => ( 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for('test_dt') )
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt');
+my @tdeps = values %$env2optdep;
+plan skip_all => 'Test needs  ' . (join '  OR  ', map
+  { "[ @{[ DBIx::Class::Optional::Dependencies->req_missing_for( $_ ) ]} ]" }
+  @tdeps
+) unless scalar grep
+  { DBIx::Class::Optional::Dependencies->req_ok_for( $_ ) }
+  @tdeps
+;
 
 my $schema;
 
@@ -43,7 +42,7 @@ for my $prefix (keys %$env2optdep) { SKIP: {
     on_connect_call => [ 'datetime_setup' ],
   });
 
-  my $sg = Scope::Guard->new(sub { cleanup($schema) } );
+  my $sg = scope_guard { cleanup($schema) };
 
   eval { $schema->storage->dbh->do('DROP TABLE "event"') };
   $schema->storage->dbh->do(<<'SQL');
similarity index 68%
rename from t/inflate/datetime_informix.t
rename to t/icdt/engine_specific/informix.t
index 8bbd524..4a6231c 100644 (file)
@@ -1,27 +1,14 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt test_rdbms_informix );
+
 use strict;
 use warnings;
 
 use Test::More;
-use DBIx::Class::Optional::Dependencies ();
+use DBIx::Class::_Util 'scope_guard';
 use lib qw(t/lib);
 use DBICTest;
-use Scope::Guard ();
-
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt')
-. ' and ' .
-DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_informix')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt')
-    && DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_informix');
 
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_INFORMIX_${_}" } qw/DSN USER PASS/};
-
-if (not $dsn) {
-  plan skip_all => <<'EOF';
-Set $ENV{DBICTEST_INFORMIX_DSN} _USER and _PASS to run this test'.
-Warning: This test drops and creates a table called 'event'";
-EOF
-}
-
 my $schema;
 
 {
@@ -29,7 +16,7 @@ my $schema;
     on_connect_call => [ 'datetime_setup' ],
   });
 
-  my $sg = Scope::Guard->new(sub { cleanup($schema) } );
+  my $sg = scope_guard { cleanup($schema) };
 
   eval { $schema->storage->dbh->do('DROP TABLE event') };
   $schema->storage->dbh->do(<<'SQL');
similarity index 60%
rename from t/inflate/datetime_msaccess.t
rename to t/icdt/engine_specific/msaccess.t
index f012199..9e647fb 100644 (file)
@@ -1,34 +1,26 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt _rdbms_msaccess_common );
+
 use strict;
 use warnings;
 
 use Test::More;
-use Scope::Guard ();
 use Try::Tiny;
-use DBIx::Class::Optional::Dependencies ();
+use DBIx::Class::_Util 'scope_guard';
 use lib qw(t/lib);
 use DBICTest;
 
+my @tdeps = qw( test_rdbms_msaccess_odbc test_rdbms_msaccess_ado );
+plan skip_all => 'Test needs  ' . (join '  OR  ', map
+  { "[ @{[ DBIx::Class::Optional::Dependencies->req_missing_for( $_ ) ]} ]" }
+  @tdeps
+) unless scalar grep
+  { DBIx::Class::Optional::Dependencies->req_ok_for( $_ ) }
+  @tdeps
+;
+
 my ($dsn,  $user,  $pass)  = @ENV{map { "DBICTEST_MSACCESS_ODBC_${_}" } qw/DSN USER PASS/};
 my ($dsn2, $user2, $pass2) = @ENV{map { "DBICTEST_MSACCESS_ADO_${_}" }  qw/DSN USER PASS/};
 
-plan skip_all => 'Test needs ' .
-  (join ' and ', map { $_ ? $_ : () }
-    DBIx::Class::Optional::Dependencies->req_missing_for('test_dt'),
-    (join ' or ', map { $_ ? $_ : () }
-      DBIx::Class::Optional::Dependencies->req_missing_for('test_rdbms_msaccess_odbc'),
-      DBIx::Class::Optional::Dependencies->req_missing_for('test_rdbms_msaccess_ado')))
-  unless
-    DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt') && (
-    $dsn && DBIx::Class::Optional::Dependencies->req_ok_for('test_rdbms_msaccess_odbc')
-    or
-    $dsn2 && DBIx::Class::Optional::Dependencies->req_ok_for('test_rdbms_msaccess_ado'))
-      or (not $dsn || $dsn2);
-
-plan skip_all => <<'EOF' unless $dsn || $dsn2;
-Set $ENV{DBICTEST_MSACCESS_ODBC_DSN} and/or $ENV{DBICTEST_MSACCESS_ADO_DSN} (and optionally _USER and _PASS) to run these tests.
-Warning: this test drops and creates the table 'track'.
-EOF
-
 my @connect_info = (
   [ $dsn,  $user  || '', $pass  || '' ],
   [ $dsn2, $user2 || '', $pass2 || '' ],
@@ -44,7 +36,7 @@ for my $connect_info (@connect_info) {
     quote_names => 1,
   });
 
-  my $guard = Scope::Guard->new(sub { cleanup($schema) });
+  my $guard = scope_guard { cleanup($schema) };
 
   try { local $^W = 0; $schema->storage->dbh->do('DROP TABLE track') };
   $schema->storage->dbh->do(<<"SQL");
similarity index 75%
rename from t/inflate/datetime_mssql.t
rename to t/icdt/engine_specific/mssql.t
index edbac14..e65a994 100644 (file)
@@ -1,42 +1,28 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt _rdbms_mssql_common );
+
 use strict;
 use warnings;
 
 use Test::More;
 use Test::Exception;
-use Scope::Guard ();
 use Try::Tiny;
-use DBIx::Class::Optional::Dependencies ();
+use DBIx::Class::_Util 'scope_guard';
 use lib qw(t/lib);
 use DBICTest;
 
+my @tdeps = qw( test_rdbms_mssql_odbc test_rdbms_mssql_sybase test_rdbms_mssql_ado );
+plan skip_all => 'Test needs  ' . (join '  OR  ', map
+  { "[ @{[ DBIx::Class::Optional::Dependencies->req_missing_for( $_ ) ]} ]" }
+  @tdeps
+) unless scalar grep
+  { DBIx::Class::Optional::Dependencies->req_ok_for( $_ ) }
+  @tdeps
+;
+
 my ($dsn,  $user,  $pass)  = @ENV{map { "DBICTEST_MSSQL_ODBC_${_}" } qw/DSN USER PASS/};
 my ($dsn2, $user2, $pass2) = @ENV{map { "DBICTEST_MSSQL_${_}" }      qw/DSN USER PASS/};
 my ($dsn3, $user3, $pass3) = @ENV{map { "DBICTEST_MSSQL_ADO_${_}" }  qw/DSN USER PASS/};
 
-plan skip_all => 'Test needs ' .
-  (join ' and ', map { $_ ? $_ : () }
-    DBIx::Class::Optional::Dependencies->req_missing_for('test_dt'),
-    (join ' or ', map { $_ ? $_ : () }
-      DBIx::Class::Optional::Dependencies->req_missing_for('test_rdbms_mssql_odbc'),
-      DBIx::Class::Optional::Dependencies->req_missing_for('test_rdbms_mssql_sybase'),
-      DBIx::Class::Optional::Dependencies->req_missing_for('test_rdbms_mssql_ado')))
-  unless
-    DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt') && (
-    $dsn && DBIx::Class::Optional::Dependencies->req_ok_for('test_rdbms_mssql_odbc')
-    or
-    $dsn2 && DBIx::Class::Optional::Dependencies->req_ok_for('test_rdbms_mssql_sybase')
-    or
-    $dsn3 && DBIx::Class::Optional::Dependencies->req_ok_for('test_rdbms_mssql_ado'))
-      or (not $dsn || $dsn2 || $dsn3);
-
-if (not ($dsn || $dsn2 || $dsn3)) {
-  plan skip_all =>
-    'Set $ENV{DBICTEST_MSSQL_ODBC_DSN} and/or $ENV{DBICTEST_MSSQL_DSN} and/or '
-    .'$ENV{DBICTEST_MSSQL_ADO_DSN} _USER and _PASS to run this test' .
-    "\nWarning: This test drops and creates tables called 'event_small_dt' and"
-    ." 'track'.";
-}
-
 DBICTest::Schema->load_classes('EventSmallDT');
 
 my @connect_info = (
@@ -66,7 +52,7 @@ for my $connect_info (@connect_info) {
     }
   }
 
-  my $guard = Scope::Guard->new(sub{ cleanup($schema) });
+  my $guard = scope_guard { cleanup($schema) };
 
   # $^W because DBD::ADO is a piece of crap
   try { local $^W = 0; $schema->storage->dbh->do("DROP TABLE track") };
diff --git a/t/icdt/engine_specific/oracle.t b/t/icdt/engine_specific/oracle.t
new file mode 100644 (file)
index 0000000..4dc94b3
--- /dev/null
@@ -0,0 +1,113 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt test_rdbms_oracle );
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBICTest;
+
+# DateTime::Format::Oracle needs this set
+$ENV{NLS_DATE_FORMAT} = 'DD-MON-YY';
+$ENV{NLS_TIMESTAMP_FORMAT} = 'YYYY-MM-DD HH24:MI:SSXFF';
+$ENV{NLS_LANG} = 'AMERICAN_AMERICA.WE8ISO8859P1';
+$ENV{NLS_SORT} = "BINARY";
+$ENV{NLS_COMP} = "BINARY";
+
+my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_ORA_${_}" } qw/DSN USER PASS/};
+my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
+
+# older oracles do not support a TIMESTAMP datatype
+my $timestamp_datatype = ($schema->storage->_server_info->{normalized_dbms_version}||0) < 9
+  ? 'DATE'
+  : 'TIMESTAMP'
+;
+
+my $dbh = $schema->storage->dbh;
+
+#$dbh->do("alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SSXFF'");
+
+eval {
+  $dbh->do("DROP TABLE event");
+};
+$dbh->do(<<EOS);
+  CREATE TABLE event (
+    id number NOT NULL,
+    starts_at date NOT NULL,
+    created_on $timestamp_datatype NOT NULL,
+    varchar_date varchar(20),
+    varchar_datetime varchar(20),
+    skip_inflation date,
+    ts_without_tz date,
+    PRIMARY KEY (id)
+  )
+EOS
+
+# TODO is in effect for the rest of the tests
+local $TODO = 'FIXME - something odd is going on with Oracle < 9 datetime support'
+  if ($schema->storage->_server_info->{normalized_dbms_version}||0) < 9;
+
+lives_ok {
+
+# insert a row to play with
+my $new = $schema->resultset('Event')->create({ id => 1, starts_at => '06-MAY-07', created_on => '2009-05-03 21:17:18.5' });
+is($new->id, 1, "insert sucessful");
+
+my $event = $schema->resultset('Event')->find( 1 );
+
+is( ref($event->starts_at), 'DateTime', "starts_at inflated ok");
+
+is( $event->starts_at->month, 5, "DateTime methods work on inflated column");
+
+is( ref($event->created_on), 'DateTime', "created_on inflated ok");
+
+is( $event->created_on->nanosecond, 500_000_000, "DateTime methods work with nanosecond precision");
+
+my $dt = DateTime->now();
+$event->starts_at($dt);
+$event->created_on($dt);
+$event->update;
+
+is( $event->starts_at->month, $dt->month, "deflate ok");
+is( int $event->created_on->nanosecond, int $dt->nanosecond, "deflate ok with nanosecond precision");
+
+# test datetime_setup
+
+$schema->storage->disconnect;
+
+delete $ENV{NLS_DATE_FORMAT};
+delete $ENV{NLS_TIMESTAMP_FORMAT};
+
+$schema->connection($dsn, $user, $pass, {
+    on_connect_call => 'datetime_setup'
+});
+
+$dt = DateTime->now();
+
+my $timestamp = $dt->clone;
+$timestamp->set_nanosecond( int 500_000_000 );
+
+$event = $schema->resultset('Event')->find( 1 );
+$event->update({ starts_at => $dt, created_on => $timestamp });
+
+$event = $schema->resultset('Event')->find(1);
+
+is( $event->starts_at, $dt, 'DateTime round-trip as DATE' );
+is( $event->created_on, $timestamp, 'DateTime round-trip as TIMESTAMP' );
+
+is( int $event->created_on->nanosecond, int 500_000_000,
+  'TIMESTAMP nanoseconds survived' );
+
+} 'dateteime operations executed correctly';
+
+done_testing;
+
+# clean up our mess
+END {
+  if($schema && (my $dbh = $schema->storage->_dbh)) {
+    $dbh->do("DROP TABLE event");
+  }
+  undef $schema;
+}
+
similarity index 63%
rename from t/inflate/datetime_sqlanywhere.t
rename to t/icdt/engine_specific/sqlanywhere.t
index 676665f..0bac9dc 100644 (file)
@@ -1,36 +1,25 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt _rdbms_sqlanywhere_common );
+
 use strict;
 use warnings;
 
 use Test::More;
-use Scope::Guard ();
-use DBIx::Class::Optional::Dependencies ();
+use DBIx::Class::_Util 'scope_guard';
 use lib qw(t/lib);
 use DBICTest;
 
+my @tdeps = qw( test_rdbms_sqlanywhere test_rdbms_sqlanywhere_odbc );
+plan skip_all => 'Test needs  ' . (join '  OR  ', map
+  { "[ @{[ DBIx::Class::Optional::Dependencies->req_missing_for( $_ ) ]} ]" }
+  @tdeps
+) unless scalar grep
+  { DBIx::Class::Optional::Dependencies->req_ok_for( $_ ) }
+  @tdeps
+;
+
 my ($dsn, $user, $pass)    = @ENV{map { "DBICTEST_SQLANYWHERE_${_}" }      qw/DSN USER PASS/};
 my ($dsn2, $user2, $pass2) = @ENV{map { "DBICTEST_SQLANYWHERE_ODBC_${_}" } qw/DSN USER PASS/};
 
-plan skip_all => 'Test needs ' .
-  (join ' and ', map { $_ ? $_ : () }
-    DBIx::Class::Optional::Dependencies->req_missing_for('test_dt'),
-    (join ' or ', map { $_ ? $_ : () }
-      DBIx::Class::Optional::Dependencies->req_missing_for('test_rdbms_sqlanywhere'),
-      DBIx::Class::Optional::Dependencies->req_missing_for('test_rdbms_sqlanywhere_odbc')))
-  unless
-    DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt') && (
-    $dsn && DBIx::Class::Optional::Dependencies->req_ok_for('test_rdbms_sqlanywhere')
-    or
-    $dsn2 && DBIx::Class::Optional::Dependencies->req_ok_for('test_rdbms_sqlanywhere_odbc'))
-      or (not $dsn || $dsn2);
-
-if (not ($dsn || $dsn2)) {
-  plan skip_all => <<'EOF';
-Set $ENV{DBICTEST_SQLANYWHERE_DSN} and/or $ENV{DBICTEST_SQLANYWHERE_ODBC_DSN}
-_USER and _PASS to run this test'.
-Warning: This test drops and creates a table called 'event'";
-EOF
-}
-
 my @info = (
   [ $dsn,  $user,  $pass  ],
   [ $dsn2, $user2, $pass2 ],
@@ -49,7 +38,7 @@ foreach my $info (@info) {
     on_connect_call => 'datetime_setup',
   });
 
-  my $sg = Scope::Guard->new(sub { cleanup($schema) } );
+  my $sg = scope_guard { cleanup($schema) };
 
   eval { $schema->storage->dbh->do('DROP TABLE event') };
   $schema->storage->dbh->do(<<"SQL");
diff --git a/t/icdt/engine_specific/sqlite.t b/t/icdt/engine_specific/sqlite.t
new file mode 100644 (file)
index 0000000..f9b3210
--- /dev/null
@@ -0,0 +1,217 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt test_rdbms_sqlite );
+
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Warn;
+use Try::Tiny;
+use lib qw(t/lib);
+use DBICTest;
+
+# Test offline parser determination (formerly t/inflate/datetime_determine_parser.t)
+{
+  my $schema = DBICTest->init_schema(
+    no_deploy => 1, # Deploying would cause an early rebless
+  );
+
+  my $storage = $schema->storage;
+
+  if ($ENV{DBICTEST_VIA_REPLICATED}) {
+    $storage = $storage->master;
+  }
+  else {
+    is(
+      ref $storage, 'DBIx::Class::Storage::DBI',
+      'Starting with generic storage'
+    );
+  }
+
+  # Calling date_time_parser should cause the storage to be reblessed,
+  # so that we can pick up datetime_parser_type from subclasses
+  my $parser = $storage->datetime_parser();
+
+  is($parser, 'DateTime::Format::SQLite', 'Got expected storage-set datetime_parser');
+  isa_ok($storage, 'DBIx::Class::Storage::DBI::SQLite', 'storage');
+
+  ok(! $storage->connected, 'Not yet connected');
+}
+
+# so user's env doesn't screw us
+delete $ENV{DBIC_DT_SEARCH_OK};
+
+my $schema = DBICTest->init_schema();
+
+# inflation test
+my $event = $schema->resultset("Event")->find(1);
+
+isa_ok($event->starts_at, 'DateTime', 'DateTime returned');
+
+# klunky, but makes older Test::More installs happy
+my $starts = $event->starts_at;
+is("$starts", '2006-04-25T22:24:33', 'Correct date/time');
+
+my $dt_warn_re = qr/DateTime objects.+not supported properly/;
+
+my $row;
+
+{
+  local $ENV{DBIC_DT_SEARCH_OK} = 1;
+  local $SIG{__WARN__} = sub {
+    fail('Disabled warning still issued') if $_[0] =~ $dt_warn_re;
+    warn @_;
+  };
+  $row = $schema->resultset('Event')->search({ starts_at => $starts })->single
+}
+
+warnings_exist {
+  $row = $schema->resultset('Event')->search({ starts_at => $starts })->single
+} [$dt_warn_re],
+  'using a DateTime object in ->search generates a warning';
+
+{
+  local $TODO = "This stuff won't work without a -dt operator of some sort"
+    unless eval { require DBIx::Class::SQLMaker::DateOps };
+
+  is(eval { $row->id }, 1, 'DT in search');
+
+  local $ENV{DBIC_DT_SEARCH_OK} = 1;
+
+  ok($row =
+    $schema->resultset('Event')->search({ starts_at => { '>=' => $starts } })
+    ->single);
+
+  is(eval { $row->id }, 1, 'DT in search with condition');
+}
+
+# create using DateTime
+my $created = $schema->resultset('Event')->create({
+    starts_at => DateTime->new(year=>2006, month=>6, day=>18),
+    created_on => DateTime->new(year=>2006, month=>6, day=>23)
+});
+my $created_start = $created->starts_at;
+
+isa_ok($created->starts_at, 'DateTime', 'DateTime returned');
+is("$created_start", '2006-06-18T00:00:00', 'Correct date/time');
+
+## timestamp field
+isa_ok($event->created_on, 'DateTime', 'DateTime returned');
+
+## varchar fields
+isa_ok($event->varchar_date, 'DateTime', 'DateTime returned');
+isa_ok($event->varchar_datetime, 'DateTime', 'DateTime returned');
+
+## skip inflation field
+isnt(ref($event->skip_inflation), 'DateTime', 'No DateTime returned for skip inflation column');
+
+# klunky, but makes older Test::More installs happy
+my $createo = $event->created_on;
+is("$createo", '2006-06-22T21:00:05', 'Correct date/time');
+
+my $created_cron = $created->created_on;
+
+isa_ok($created->created_on, 'DateTime', 'DateTime returned');
+is("$created_cron", '2006-06-23T00:00:00', 'Correct date/time');
+
+## varchar field using inflate_date => 1
+my $varchar_date = $event->varchar_date;
+is("$varchar_date", '2006-07-23T00:00:00', 'Correct date/time');
+
+## varchar field using inflate_datetime => 1
+my $varchar_datetime = $event->varchar_datetime;
+is("$varchar_datetime", '2006-05-22T19:05:07', 'Correct date/time');
+
+## skip inflation field
+my $skip_inflation = $event->skip_inflation;
+is ("$skip_inflation", '2006-04-21 18:04:06', 'Correct date/time');
+
+# extra accessor tests with update_or_insert
+{
+  my $new = $schema->resultset("Track")->new( {
+    trackid => 100,
+    cd => 1,
+    title => 'Insert or Update',
+    last_updated_on => '1973-07-19 12:01:02'
+  } );
+  $new->update_or_insert;
+  ok($new->in_storage, 'update_or_insert insert ok');
+
+  # test in update mode
+  $new->title('Insert or Update - updated');
+  $new->update_or_insert;
+  is( $schema->resultset("Track")->find(100)->title, 'Insert or Update - updated', 'update_or_insert update ok');
+
+  # test get_inflated_columns with objects
+  my $event = $schema->resultset('Event')->search->first;
+  my %edata = $event->get_inflated_columns;
+  is($edata{'id'}, $event->id, 'got id');
+  isa_ok($edata{'starts_at'}, 'DateTime', 'start_at is DateTime object');
+  isa_ok($edata{'created_on'}, 'DateTime', 'create_on DateTime object');
+  is($edata{'starts_at'}, $event->starts_at, 'got start date');
+  is($edata{'created_on'}, $event->created_on, 'got created date');
+
+  # get_inflated_columns w/relation and accessor alias
+  isa_ok($new->updated_date, 'DateTime', 'have inflated object via accessor');
+  my %tdata = $new->get_inflated_columns;
+  is($tdata{'trackid'}, 100, 'got id');
+  isa_ok($tdata{'cd'}, 'DBICTest::CD', 'cd is CD object');
+  is($tdata{'cd'}->id, 1, 'cd object is id 1');
+  is(
+    $tdata{'position'},
+    $schema->resultset ('Track')->search ({cd => 1})->count,
+    'Ordered assigned proper position',
+  );
+  is($tdata{'title'}, 'Insert or Update - updated');
+  is($tdata{'last_updated_on'}, '1973-07-19T12:01:02');
+  isa_ok($tdata{'last_updated_on'}, 'DateTime', 'inflated accessored column');
+}
+
+# create and update with literals
+{
+  my $d = {
+    created_on => \ '2001-09-11',
+    starts_at => \[ '?' => '2001-10-26' ],
+  };
+
+  my $ev = $schema->resultset('Event')->create($d);
+
+  for my $col (qw(created_on starts_at)) {
+    ok (ref $ev->$col, "literal untouched in $col");
+    is_deeply( $ev->$col, $d->{$col});
+    is_deeply( $ev->get_inflated_column($col), $d->{$col});
+    is_deeply( $ev->get_column($col), $d->{$col});
+  }
+
+  $ev->discard_changes;
+
+  is_deeply(
+    { $ev->get_dirty_columns },
+    {}
+  );
+
+  for my $col (qw(created_on starts_at)) {
+    isa_ok ($ev->$col, "DateTime", "$col properly inflated on retrieve");
+  }
+
+  for my $meth (qw(set_inflated_columns set_columns)) {
+
+    $ev->$meth({%$d});
+
+    is_deeply(
+      { $ev->get_dirty_columns },
+      $d,
+      "Expected dirty cols after setting literals via $meth",
+    );
+
+    $ev->update;
+
+    for my $col (qw(created_on starts_at)) {
+      ok (ref $ev->$col, "literal untouched in $col updated via $meth");
+      is_deeply( $ev->$col, $d->{$col});
+      is_deeply( $ev->get_inflated_column($col), $d->{$col});
+      is_deeply( $ev->get_column($col), $d->{$col});
+    }
+  }
+}
+
+done_testing;
similarity index 81%
rename from t/inflate/datetime_sybase.t
rename to t/icdt/engine_specific/sybase.t
index 597f6a3..c63944e 100644 (file)
@@ -1,29 +1,16 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt test_rdbms_ase );
+
 use strict;
 use warnings;
 
 use Test::More;
 use Test::Exception;
-use Scope::Guard ();
-use Try::Tiny;
-use DBIx::Class::Optional::Dependencies ();
+use DBIx::Class::_Util 'scope_guard';
 use lib qw(t/lib);
 use DBICTest;
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt')
-. ' and ' .
-DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_ase')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt')
-    && DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_ase');
-
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_SYBASE_${_}" } qw/DSN USER PASS/};
 
-if (not ($dsn && $user)) {
-  plan skip_all =>
-    'Set $ENV{DBICTEST_SYBASE_DSN}, _USER and _PASS to run this test' .
-    "\nWarning: This test drops and creates a table called 'track' and " .
-    "'event_small_dt'";
-}
-
 DBICTest::Schema->load_classes('EventSmallDT');
 
 my @storage_types = (
@@ -42,7 +29,7 @@ for my $storage_type (@storage_types) {
     on_connect_call => 'datetime_setup',
   });
 
-  my $guard = Scope::Guard->new(sub { cleanup($schema) } );
+  my $guard = scope_guard { cleanup($schema) };
 
   $schema->storage->ensure_connected;
 
similarity index 93%
rename from t/inflate/datetime_mysql.t
rename to t/icdt/offline_mysql.t
index 44699ab..91bd3f6 100644 (file)
@@ -1,18 +1,16 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt_mysql );
+
 use strict;
 use warnings;
 
 use Test::More;
 use Test::Exception;
 use Test::Warn;
-use DBIx::Class::Optional::Dependencies ();
 use lib qw(t/lib);
 use DBICTest;
 use DBICTest::Schema;
 use DBIx::Class::_Util 'sigwarn_silencer';
 
-plan skip_all => 'Inflation tests need ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt_mysql')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt_mysql');
-
 {
   DBICTest::Schema->load_classes('EventTZ');
   local $SIG{__WARN__} = sigwarn_silencer( qr/extra \=\> .+? has been deprecated/ );
similarity index 60%
rename from t/inflate/datetime_pg.t
rename to t/icdt/offline_pg.t
index c02e9f8..0c0cb9b 100644 (file)
@@ -1,19 +1,36 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => qw( ic_dt_pg );
+
 use strict;
 use warnings;
 
 use Test::More;
 use Test::Warn;
-use DBIx::Class::Optional::Dependencies ();
 use lib qw(t/lib);
 use DBICTest;
 
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt_pg')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt_pg');
-
 DBICTest::Schema->load_classes('EventTZPg');
 
+{
+  my $s = DBICTest::Schema->connect('dbi:Pg:whatever');
+
+  ok (!$s->storage->_dbh, 'definitely not connected');
+
+  # Check that datetime_parser returns correctly before we explicitly connect.
+  my $store = ref $s->storage;
+  is($store, 'DBIx::Class::Storage::DBI', 'Started with generic storage');
+
+  my $parser = $s->storage->datetime_parser;
+  is( $parser, 'DateTime::Format::Pg', 'datetime_parser is as expected');
+
+  ok (!$s->storage->_dbh, 'still not connected');
+}
+
 my $schema = DBICTest->init_schema();
 
+# this may generate warnings under certain CI flags, hence do it outside of
+# the warnings_are below
+my $dt = DateTime->new( year => 2000, time_zone => "America/Chicago" );
+
 warnings_are {
   my $event = $schema->resultset("EventTZPg")->find(1);
   $event->update({created_on => '2009-01-15 17:00:00+00'});
diff --git a/t/inflate/datetime.t b/t/inflate/datetime.t
deleted file mode 100644 (file)
index 7062563..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-use strict;
-use warnings;
-
-use Test::More;
-use Test::Warn;
-use Try::Tiny;
-use lib qw(t/lib);
-use DBICTest;
-
-# so user's env doesn't screw us
-delete $ENV{DBIC_DT_SEARCH_OK};
-
-my $schema = DBICTest->init_schema();
-
-plan skip_all => 'DT inflation tests need ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt_sqlite')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt_sqlite');
-
-# inflation test
-my $event = $schema->resultset("Event")->find(1);
-
-isa_ok($event->starts_at, 'DateTime', 'DateTime returned');
-
-# klunky, but makes older Test::More installs happy
-my $starts = $event->starts_at;
-is("$starts", '2006-04-25T22:24:33', 'Correct date/time');
-
-my $dt_warn_re = qr/DateTime objects.+not supported properly/;
-
-my $row;
-
-{
-  local $ENV{DBIC_DT_SEARCH_OK} = 1;
-  local $SIG{__WARN__} = sub {
-    fail('Disabled warning still issued') if $_[0] =~ $dt_warn_re;
-    warn @_;
-  };
-  $row = $schema->resultset('Event')->search({ starts_at => $starts })->single
-}
-
-warnings_exist {
-  $row = $schema->resultset('Event')->search({ starts_at => $starts })->single
-} [$dt_warn_re],
-  'using a DateTime object in ->search generates a warning';
-
-{
-  local $TODO = "This stuff won't work without a -dt operator of some sort"
-    unless eval { require DBIx::Class::SQLMaker::DateOps };
-
-  is(eval { $row->id }, 1, 'DT in search');
-
-  local $ENV{DBIC_DT_SEARCH_OK} = 1;
-
-  ok($row =
-    $schema->resultset('Event')->search({ starts_at => { '>=' => $starts } })
-    ->single);
-
-  is(eval { $row->id }, 1, 'DT in search with condition');
-}
-
-# create using DateTime
-my $created = $schema->resultset('Event')->create({
-    starts_at => DateTime->new(year=>2006, month=>6, day=>18),
-    created_on => DateTime->new(year=>2006, month=>6, day=>23)
-});
-my $created_start = $created->starts_at;
-
-isa_ok($created->starts_at, 'DateTime', 'DateTime returned');
-is("$created_start", '2006-06-18T00:00:00', 'Correct date/time');
-
-## timestamp field
-isa_ok($event->created_on, 'DateTime', 'DateTime returned');
-
-## varchar fields
-isa_ok($event->varchar_date, 'DateTime', 'DateTime returned');
-isa_ok($event->varchar_datetime, 'DateTime', 'DateTime returned');
-
-## skip inflation field
-isnt(ref($event->skip_inflation), 'DateTime', 'No DateTime returned for skip inflation column');
-
-# klunky, but makes older Test::More installs happy
-my $createo = $event->created_on;
-is("$createo", '2006-06-22T21:00:05', 'Correct date/time');
-
-my $created_cron = $created->created_on;
-
-isa_ok($created->created_on, 'DateTime', 'DateTime returned');
-is("$created_cron", '2006-06-23T00:00:00', 'Correct date/time');
-
-## varchar field using inflate_date => 1
-my $varchar_date = $event->varchar_date;
-is("$varchar_date", '2006-07-23T00:00:00', 'Correct date/time');
-
-## varchar field using inflate_datetime => 1
-my $varchar_datetime = $event->varchar_datetime;
-is("$varchar_datetime", '2006-05-22T19:05:07', 'Correct date/time');
-
-## skip inflation field
-my $skip_inflation = $event->skip_inflation;
-is ("$skip_inflation", '2006-04-21 18:04:06', 'Correct date/time');
-
-done_testing;
diff --git a/t/inflate/datetime_determine_parser.t b/t/inflate/datetime_determine_parser.t
deleted file mode 100644 (file)
index 802c30e..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-use strict;
-use warnings;
-
-use Test::More;
-use lib qw(t/lib);
-use DBICTest;
-
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt_sqlite')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt_sqlite');
-
-my $schema = DBICTest->init_schema(
-    no_deploy => 1, # Deploying would cause an early rebless
-);
-
-is(
-    ref $schema->storage, 'DBIx::Class::Storage::DBI',
-    'Starting with generic storage'
-);
-
-# Calling date_time_parser should cause the storage to be reblessed,
-# so that we can pick up datetime_parser_type from subclasses
-
-my $parser = $schema->storage->datetime_parser();
-
-is($parser, 'DateTime::Format::SQLite', 'Got expected storage-set datetime_parser');
-isa_ok($schema->storage, 'DBIx::Class::Storage::DBI::SQLite', 'storage');
-
-done_testing;
diff --git a/t/inflate/datetime_oracle.t b/t/inflate/datetime_oracle.t
deleted file mode 100644 (file)
index 26a5357..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-use strict;
-use warnings;
-
-use Test::More;
-use Test::Exception;
-use DBIx::Class::Optional::Dependencies ();
-use lib qw(t/lib);
-use DBICTest;
-
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_rdbms_oracle')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_rdbms_oracle');
-
-my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_ORA_${_}" } qw/DSN USER PASS/};
-
-if (not ($dsn && $user && $pass)) {
-    plan skip_all => 'Set $ENV{DBICTEST_ORA_DSN}, _USER and _PASS to run this test. ' .
-         'Warning: This test drops and creates a table called \'track\'';
-}
-
-# DateTime::Format::Oracle needs this set
-$ENV{NLS_DATE_FORMAT} = 'DD-MON-YY';
-$ENV{NLS_TIMESTAMP_FORMAT} = 'YYYY-MM-DD HH24:MI:SSXFF';
-$ENV{NLS_LANG} = 'AMERICAN_AMERICA.WE8ISO8859P1';
-$ENV{NLS_SORT} = "BINARY";
-$ENV{NLS_COMP} = "BINARY";
-
-my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
-
-# older oracles do not support a TIMESTAMP datatype
-my $timestamp_datatype = ($schema->storage->_server_info->{normalized_dbms_version}||0) < 9
-  ? 'DATE'
-  : 'TIMESTAMP'
-;
-
-# Need to redefine the last_updated_on column
-my $col_metadata = $schema->class('Track')->column_info('last_updated_on');
-$schema->class('Track')->add_column( 'last_updated_on' => {
-    data_type => 'date' });
-$schema->class('Track')->add_column( 'last_updated_at' => {
-    data_type => $timestamp_datatype });
-
-my $dbh = $schema->storage->dbh;
-
-#$dbh->do("alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SSXFF'");
-
-eval {
-  $dbh->do("DROP TABLE track");
-};
-$dbh->do("CREATE TABLE track (trackid NUMBER(12), cd NUMBER(12), position NUMBER(12), title VARCHAR(255), last_updated_on DATE, last_updated_at $timestamp_datatype)");
-
-# TODO is in effect for the rest of the tests
-local $TODO = 'FIXME - something odd is going on with Oracle < 9 datetime support'
-  if ($schema->storage->_server_info->{normalized_dbms_version}||0) < 9;
-
-lives_ok {
-
-# insert a row to play with
-my $new = $schema->resultset('Track')->create({ trackid => 1, cd => 1, position => 1, title => 'Track1', last_updated_on => '06-MAY-07', last_updated_at => '2009-05-03 21:17:18.5' });
-is($new->trackid, 1, "insert sucessful");
-
-my $track = $schema->resultset('Track')->find( 1 );
-
-is( ref($track->last_updated_on), 'DateTime', "last_updated_on inflated ok");
-
-is( $track->last_updated_on->month, 5, "DateTime methods work on inflated column");
-
-#note '$track->last_updated_at => ', $track->last_updated_at;
-is( ref($track->last_updated_at), 'DateTime', "last_updated_at inflated ok");
-
-is( $track->last_updated_at->nanosecond, 500_000_000, "DateTime methods work with nanosecond precision");
-
-my $dt = DateTime->now();
-$track->last_updated_on($dt);
-$track->last_updated_at($dt);
-$track->update;
-
-is( $track->last_updated_on->month, $dt->month, "deflate ok");
-is( int $track->last_updated_at->nanosecond, int $dt->nanosecond, "deflate ok with nanosecond precision");
-
-# test datetime_setup
-
-$schema->storage->disconnect;
-
-delete $ENV{NLS_DATE_FORMAT};
-delete $ENV{NLS_TIMESTAMP_FORMAT};
-
-$schema->connection($dsn, $user, $pass, {
-    on_connect_call => 'datetime_setup'
-});
-
-$dt = DateTime->now();
-
-my $timestamp = $dt->clone;
-$timestamp->set_nanosecond( int 500_000_000 );
-
-$track = $schema->resultset('Track')->find( 1 );
-$track->update({ last_updated_on => $dt, last_updated_at => $timestamp });
-
-$track = $schema->resultset('Track')->find(1);
-
-is( $track->last_updated_on, $dt, 'DateTime round-trip as DATE' );
-is( $track->last_updated_at, $timestamp, 'DateTime round-trip as TIMESTAMP' );
-
-is( int $track->last_updated_at->nanosecond, int 500_000_000,
-  'TIMESTAMP nanoseconds survived' );
-
-} 'dateteime operations executed correctly';
-
-done_testing;
-
-# clean up our mess
-END {
-  if($schema && (my $dbh = $schema->storage->dbh)) {
-    $dbh->do("DROP TABLE track");
-  }
-  undef $schema;
-}
-
index b9ca3d8..b5e9d2f 100644 (file)
@@ -3,11 +3,11 @@ use warnings;
 
 use Test::More;
 
-use DBIx::Class::_Util 'modver_gt_or_eq';
+use DBIx::Class::_Util 'modver_gt_or_eq_and_lt';
 use base();
 BEGIN {
   plan skip_all => 'base.pm 2.20 (only present in perl 5.19.7) is known to break this test'
-    if modver_gt_or_eq(base => '2.19_01') and ! modver_gt_or_eq(base => '2.21');
+    if modver_gt_or_eq_and_lt( 'base', '2.19_01', '2.21' );
 }
 
 use Test::Exception;
index 30d63ec..63c31aa 100644 (file)
@@ -28,14 +28,12 @@ foreach my $serializer (@serializers) {
     }
 }
 
-plan (skip_all => "No suitable serializer found") unless $selected;
-
 DBICTest::Schema::Serialized->inflate_column( 'serialized',
     { inflate => $selected->{inflater},
       deflate => $selected->{deflater},
     },
 );
-Class::C3->reinitialize;
+Class::C3->reinitialize if DBIx::Class::_ENV_::OLD_MRO;
 
 my $struct_hash = {
     a => 1,
@@ -97,7 +95,7 @@ is_deeply (
 );
 
 #===== make sure make_column_dirty interacts reasonably with inflation
-$object = $rs->first;
+$object = $rs->search({}, { rows => 1 })->next;
 $object->update ({serialized => { x => 'y'}});
 
 $object->serialized->{x} = 'z'; # change state without notifying $object
diff --git a/t/lib/DBIC/DebugObj.pm b/t/lib/DBIC/DebugObj.pm
deleted file mode 100644 (file)
index c43bae9..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-package DBIC::DebugObj;
-
-use strict;
-use warnings;
-
-use Class::C3;
-
-use base qw/DBIx::Class::Storage::Statistics Exporter Class::Accessor::Fast/;
-
-__PACKAGE__->mk_accessors( qw/dbictest_sql_ref dbictest_bind_ref/ );
-
-
-=head2 new(PKG, SQL_REF, BIND_REF, ...)
-
-Creates a new instance that on subsequent queries will store
-the generated SQL to the scalar pointed to by SQL_REF and bind
-values to the array pointed to by BIND_REF.
-
-=cut
-
-sub new {
-  my $pkg = shift;
-  my $sql_ref = shift;
-  my $bind_ref = shift;
-
-  my $self = $pkg->SUPER::new(@_);
-
-  $self->debugfh(undef);
-
-  $self->dbictest_sql_ref($sql_ref);
-  $self->dbictest_bind_ref($bind_ref || []);
-
-  return $self;
-}
-
-sub query_start {
-  my $self = shift;
-
-  (${$self->dbictest_sql_ref}, @{$self->dbictest_bind_ref}) = @_;
-}
-
-sub query_end { }
-
-sub txn_begin { }
-
-sub txn_commit { }
-
-sub txn_rollback { }
-
-1;
diff --git a/t/lib/DBIC/SqlMakerTest.pm b/t/lib/DBIC/SqlMakerTest.pm
deleted file mode 100644 (file)
index 8fd047c..0000000
+++ /dev/null
@@ -1,165 +0,0 @@
-package DBIC::SqlMakerTest;
-
-use strict;
-use warnings;
-
-use base qw/Exporter/;
-
-use Carp;
-use SQL::Abstract::Test;
-
-our @EXPORT = qw/
-  is_same_sql_bind
-  is_same_sql
-  is_same_bind
-/;
-our @EXPORT_OK = qw/
-  eq_sql
-  eq_bind
-  eq_sql_bind
-/;
-
-sub is_same_sql_bind {
-  # unroll possible as_query arrayrefrefs
-  my @args;
-
-  for (1,2) {
-    my $chunk = shift @_;
-
-    if ( ref $chunk eq 'REF' and ref $$chunk eq 'ARRAY' ) {
-      my ($sql, @bind) = @$$chunk;
-      push @args, ($sql, \@bind);
-    }
-    else {
-      push @args, $chunk, shift @_;
-    }
-
-  }
-
-  push @args, shift @_;
-
-  croak "Unexpected argument(s) supplied to is_same_sql_bind: " . join ('; ', @_)
-    if @_;
-
-  @_ = @args;
-  goto &SQL::Abstract::Test::is_same_sql_bind;
-}
-
-*is_same_sql = \&SQL::Abstract::Test::is_same_sql;
-*is_same_bind = \&SQL::Abstract::Test::is_same_bind;
-*eq_sql = \&SQL::Abstract::Test::eq_sql;
-*eq_bind = \&SQL::Abstract::Test::eq_bind;
-*eq_sql_bind = \&SQL::Abstract::Test::eq_sql_bind;
-
-1;
-
-__END__
-
-
-=head1 NAME
-
-DBIC::SqlMakerTest - Helper package for testing sql_maker component of DBIC
-
-=head1 SYNOPSIS
-
-  use Test::More;
-  use DBIC::SqlMakerTest;
-
-  my ($sql, @bind) = $schema->storage->sql_maker->select(%args);
-  is_same_sql_bind(
-    $sql, \@bind,
-    $expected_sql, \@expected_bind,
-    'foo bar works'
-  );
-
-=head1 DESCRIPTION
-
-Exports functions that can be used to compare generated SQL and bind values.
-
-This is a thin wrapper around L<SQL::Abstract::Test>, which makes it easier
-to compare as_query sql/bind arrayrefrefs directly.
-
-=head1 FUNCTIONS
-
-=head2 is_same_sql_bind
-
-  is_same_sql_bind(
-    $given_sql, \@given_bind,
-    $expected_sql, \@expected_bind,
-    $test_msg
-  );
-
-  is_same_sql_bind(
-    $rs->as_query
-    $expected_sql, \@expected_bind,
-    $test_msg
-  );
-
-  is_same_sql_bind(
-    \[$given_sql, @given_bind],
-    $expected_sql, \@expected_bind,
-    $test_msg
-  );
-
-Compares given and expected pairs of C<($sql, \@bind)>, and calls
-L<Test::Builder/ok> on the result, with C<$test_msg> as message.
-
-=head2 is_same_sql
-
-  is_same_sql(
-    $given_sql,
-    $expected_sql,
-    $test_msg
-  );
-
-Compares given and expected SQL statement, and calls L<Test::Builder/ok> on the
-result, with C<$test_msg> as message.
-
-=head2 is_same_bind
-
-  is_same_bind(
-    \@given_bind,
-    \@expected_bind,
-    $test_msg
-  );
-
-Compares given and expected bind value lists, and calls L<Test::Builder/ok> on
-the result, with C<$test_msg> as message.
-
-=head2 eq_sql
-
-  my $is_same = eq_sql($given_sql, $expected_sql);
-
-Compares the two SQL statements. Returns true IFF they are equivalent.
-
-=head2 eq_bind
-
-  my $is_same = eq_sql(\@given_bind, \@expected_bind);
-
-Compares two lists of bind values. Returns true IFF their values are the same.
-
-=head2 eq_sql_bind
-
-  my $is_same = eq_sql_bind(
-    $given_sql, \@given_bind,
-    $expected_sql, \@expected_bind
-  );
-
-Compares the two SQL statements and the two lists of bind values. Returns true
-IFF they are equivalent and the bind values are the same.
-
-
-=head1 SEE ALSO
-
-L<SQL::Abstract::Test>, L<Test::More>, L<Test::Builder>.
-
-=head1 AUTHOR
-
-Norbert Buchmuller, <norbi@nix.hu>
-
-=head1 COPYRIGHT AND LICENSE
-
-Copyright 2008 by Norbert Buchmuller.
-
-This library is free software; you can redistribute it and/or modify
-it under the same terms as Perl itself.
index 6934092..c0c91c2 100644 (file)
@@ -4,43 +4,10 @@ package # hide from PAUSE
 use strict;
 use warnings;
 
-# this noop trick initializes the STDOUT, so that the TAP::Harness
-# issued IO::Select->can_read calls (which are blocking wtf wtf wtf)
-# keep spinning and scheduling jobs
-# This results in an overall much smoother job-queue drainage, since
-# the Harness blocks less
-# (ideally this needs to be addressed in T::H, but a quick patchjob
-# broke everything so tabling it for now)
-BEGIN {
-  if ($INC{'Test/Builder.pm'}) {
-    local $| = 1;
-    print "#\n";
-  }
-}
-
-use Module::Runtime 'module_notional_filename';
-BEGIN {
-  for my $mod (qw( DBIC::SqlMakerTest SQL::Abstract )) {
-    if ( $INC{ module_notional_filename($mod) } ) {
-      # FIXME this does not seem to work in BEGIN - why?!
-      #require Carp;
-      #$Carp::Internal{ (__PACKAGE__) }++;
-      #Carp::croak( __PACKAGE__ . " must be loaded before $mod" );
-
-      my ($fr, @frame) = 1;
-      while (@frame = caller($fr++)) {
-        last if $frame[1] !~ m|^t/lib/DBICTest|;
-      }
-
-      die __PACKAGE__ . " must be loaded before $mod (or modules using $mod) at $frame[1] line $frame[2]\n";
-    }
-  }
-}
-
-use DBICTest::RunMode;
+use DBICTest::Util qw( local_umask await_flock dbg DEBUG_TEST_CONCURRENCY_LOCKS );
 use DBICTest::Schema;
 use DBICTest::Util::LeakTracer qw/populate_weakregistry assert_empty_weakregistry/;
-use DBICTest::Util 'local_umask';
+use DBIx::Class::_Util qw( detected_reinvoked_destructor scope_guard );
 use Carp;
 use Path::Class::File ();
 use File::Spec;
@@ -49,7 +16,7 @@ use Config;
 
 =head1 NAME
 
-DBICTest - Library to be used by DBIx::Class test scripts.
+DBICTest - Library to be used by DBIx::Class test scripts
 
 =head1 SYNOPSIS
 
@@ -64,6 +31,26 @@ DBICTest - Library to be used by DBIx::Class test scripts.
 This module provides the basic utilities to write tests against
 DBIx::Class.
 
+=head1 EXPORTS
+
+The module does not export anything by default, nor provides individual
+function exports in the conventional sense. Instead the following tags are
+recognized:
+
+=head2 :DiffSQL
+
+Same as C<use SQL::Abstract::Test
+qw(L<is_same_sql_bind|SQL::Abstract::Test/is_same_sql_bind>
+L<is_same_sql|SQL::Abstract::Test/is_same_sql>
+L<is_same_bind|SQL::Abstract::Test/is_same_bind>)>
+
+=head2 :GlobalLock
+
+Some tests are very time sensitive and need to run on their own, without
+being disturbed by anything else grabbing CPU or disk IO. Hence why everything
+using C<DBICTest> grabs a shared lock, and the few tests that request a
+C<:GlobalLock> will ask for an exclusive one and block until they can get it.
+
 =head1 METHODS
 
 =head2 init_schema
@@ -80,18 +67,15 @@ DBIx::Class.
 This method removes the test SQLite database in t/var/DBIxClass.db
 and then creates a new, empty database.
 
-This method will call deploy_schema() by default, unless the
-no_deploy flag is set.
+This method will call L<deploy_schema()|/deploy_schema> by default, unless the
+C<no_deploy> flag is set.
 
-Also, by default, this method will call populate_schema() by
-default, unless the no_deploy or no_populate flags are set.
+Also, by default, this method will call L<populate_schema()|/populate_schema>
+by default, unless the C<no_deploy> or C<no_populate> flags are set.
 
 =cut
 
-# some tests are very time sensitive and need to run on their own, without
-# being disturbed by anything else grabbing CPU or disk IO. Hence why everything
-# using DBICTest grabs a shared lock, and the few tests that request a :GlobalLock
-# will ask for an exclusive one and block until they can get it
+# see L</:GlobalLock>
 our ($global_lock_fh, $global_exclusive_lock);
 sub import {
     my $self = shift;
@@ -104,24 +88,48 @@ sub import {
         or die "Unable to open $lockpath: $!";
     }
 
-    for (@_) {
-        if ($_ eq ':GlobalLock') {
-            flock ($global_lock_fh, LOCK_EX) or die "Unable to lock $lockpath: $!";
+    for my $exp (@_) {
+        if ($exp eq ':GlobalLock') {
+            DEBUG_TEST_CONCURRENCY_LOCKS > 1
+              and dbg "Waiting for EXCLUSIVE global lock...";
+
+            await_flock ($global_lock_fh, LOCK_EX) or die "Unable to lock $lockpath: $!";
+
+            DEBUG_TEST_CONCURRENCY_LOCKS > 1
+              and dbg "Got EXCLUSIVE global lock";
+
             $global_exclusive_lock = 1;
         }
+        elsif ($exp eq ':DiffSQL') {
+            require SQL::Abstract::Test;
+            my $into = caller(0);
+            for (qw(is_same_sql_bind is_same_sql is_same_bind)) {
+              no strict 'refs';
+              *{"${into}::$_"} = \&{"SQL::Abstract::Test::$_"};
+            }
+        }
         else {
-            croak "Unknown export $_ requested from $self";
+            croak "Unknown export $exp requested from $self";
         }
     }
 
     unless ($global_exclusive_lock) {
-        flock ($global_lock_fh, LOCK_SH) or die "Unable to lock $lockpath: $!";
+        DEBUG_TEST_CONCURRENCY_LOCKS > 1
+          and dbg "Waiting for SHARED global lock...";
+
+        await_flock ($global_lock_fh, LOCK_SH) or die "Unable to lock $lockpath: $!";
+
+        DEBUG_TEST_CONCURRENCY_LOCKS > 1
+          and dbg "Got SHARED global lock";
     }
 }
 
 END {
+    # referencing here delays destruction even more
     if ($global_lock_fh) {
-        # delay destruction even more
+      DEBUG_TEST_CONCURRENCY_LOCKS > 1
+        and dbg "Release @{[ $global_exclusive_lock ? 'EXCLUSIVE' : 'SHARED' ]} global lock (END)";
+      1;
     }
 }
 
@@ -151,6 +159,7 @@ END {
 
 $SIG{INT} = sub { _cleanup_dbfile(); exit 1 };
 
+my $need_global_cleanup;
 sub _cleanup_dbfile {
     # cleanup if this is us
     if (
@@ -160,6 +169,10 @@ sub _cleanup_dbfile {
         or
       $ENV{DBICTEST_LOCK_HOLDER} == $$
     ) {
+        if ($need_global_cleanup and my $dbh = DBICTest->schema->storage->_dbh) {
+          $dbh->disconnect;
+        }
+
         my $db_file = _sqlite_dbfilename();
         unlink $_ for ($db_file, "${db_file}-journal");
     }
@@ -222,11 +235,11 @@ sub _database {
         # set a *DBI* disconnect callback, to make sure the physical SQLite
         # file is still there (i.e. the test does not attempt to delete
         # an open database, which fails on Win32)
-        if (my $guard_cb = __mk_disconnect_guard($db_file)) {
+        if (! $storage->{master} and my $guard_cb = __mk_disconnect_guard($db_file)) {
           $dbh->{Callbacks} = {
             connect => sub { $guard_cb->('connect') },
             disconnect => sub { $guard_cb->('disconnect') },
-            DESTROY => sub { $guard_cb->('DESTROY') },
+            DESTROY => sub { &detected_reinvoked_destructor; $guard_cb->('DESTROY') },
           };
         }
       },
@@ -235,10 +248,16 @@ sub _database {
 }
 
 sub __mk_disconnect_guard {
-  return if DBIx::Class::_ENV_::PEEPEENESS; # leaks handles, delaying DESTROY, can't work right
 
   my $db_file = shift;
-  return unless -f $db_file;
+
+  return if (
+    # this perl leaks handles, delaying DESTROY, can't work right
+    DBIx::Class::_ENV_::PEEPEENESS
+      or
+    ! -f $db_file
+  );
+
 
   my $orig_inode = (stat($db_file))[1]
     or return;
@@ -264,6 +283,7 @@ sub __mk_disconnect_guard {
       return;
     }
     elsif ($event eq 'disconnect') {
+      return unless $connected; # we already disconnected earlier
       $connected = 0;
     }
     elsif ($event eq 'DESTROY' and ! $connected ) {
@@ -317,9 +337,20 @@ sub init_schema {
 
     my $schema;
 
+    if (
+      $ENV{DBICTEST_VIA_REPLICATED} &&=
+        ( !$args{storage_type} && !defined $args{sqlite_use_file} )
+    ) {
+      $args{storage_type} = ['::DBI::Replicated', { balancer_type => '::Random' }];
+      $args{sqlite_use_file} = 1;
+    }
+
+    my @dsn = $self->_database(%args);
+
     if ($args{compose_connection}) {
+      $need_global_cleanup = 1;
       $schema = DBICTest::Schema->compose_connection(
-                  'DBICTest', $self->_database(%args)
+                  'DBICTest', @dsn
                 );
     } else {
       $schema = DBICTest::Schema->compose_namespace('DBICTest');
@@ -330,7 +361,10 @@ sub init_schema {
     }
 
     if ( !$args{no_connect} ) {
-      $schema = $schema->connect($self->_database(%args));
+      $schema->connection(@dsn);
+
+      $schema->storage->connect_replicants(\@dsn)
+        if $ENV{DBICTEST_VIA_REPLICATED};
     }
 
     if ( !$args{no_deploy} ) {
@@ -346,7 +380,10 @@ sub init_schema {
 }
 
 END {
+  # Make sure we run after any cleanup in other END blocks
+  push @{ B::end_av()->object_2svref }, sub {
     assert_empty_weakregistry($weak_registry, 'quiet');
+  };
 }
 
 =head2 deploy_schema
@@ -366,8 +403,11 @@ sub deploy_schema {
     my $schema = shift;
     my $args = shift || {};
 
-    local $schema->storage->{debug}
-      if ($ENV{TRAVIS}||'') eq 'true';
+    my $guard;
+    if ( ($ENV{TRAVIS}||'') eq 'true' and my $old_dbg = $schema->storage->debug ) {
+      $guard = scope_guard { $schema->storage->debug($old_dbg) };
+      $schema->storage->debug(0);
+    }
 
     if ($ENV{"DBICTEST_SQLT_DEPLOY"}) {
         $schema->deploy($args);
@@ -397,8 +437,11 @@ sub populate_schema {
     my $self = shift;
     my $schema = shift;
 
-    local $schema->storage->{debug}
-      if ($ENV{TRAVIS}||'') eq 'true';
+    my $guard;
+    if ( ($ENV{TRAVIS}||'') eq 'true' and my $old_dbg = $schema->storage->debug ) {
+      $guard = scope_guard { $schema->storage->debug($old_dbg) };
+      $schema->storage->debug(0);
+    }
 
     $schema->populate('Genre', [
       [qw/genreid name/],
diff --git a/t/lib/DBICTest/AntiPattern/NullObject.pm b/t/lib/DBICTest/AntiPattern/NullObject.pm
new file mode 100644 (file)
index 0000000..dc99c96
--- /dev/null
@@ -0,0 +1,16 @@
+package DBICTest::AntiPattern::NullObject;
+
+use warnings;
+use strict;
+
+use overload
+  'bool'   => sub { 0 },
+  '""'     => sub { '' },
+  '0+'     => sub { 0 },
+  fallback => 1
+;
+
+our $null = bless {}, __PACKAGE__;
+sub AUTOLOAD { $null }
+
+1;
diff --git a/t/lib/DBICTest/AntiPattern/TrueZeroLen.pm b/t/lib/DBICTest/AntiPattern/TrueZeroLen.pm
new file mode 100644 (file)
index 0000000..aa7190a
--- /dev/null
@@ -0,0 +1,14 @@
+package DBICTest::AntiPattern::TrueZeroLen;
+
+use warnings;
+use strict;
+
+use overload
+  'bool'   => sub { 1 },
+  '""'     => sub { '' },
+  fallback => 1
+;
+
+sub new { bless {}, shift }
+
+1;
diff --git a/t/lib/DBICTest/Base.pm b/t/lib/DBICTest/Base.pm
new file mode 100644 (file)
index 0000000..7d2cb56
--- /dev/null
@@ -0,0 +1,12 @@
+package #hide from pause
+  DBICTest::Base;
+
+use strict;
+use warnings;
+
+# must load before any DBIx::Class* namespaces
+use DBICTest::RunMode;
+
+sub _skip_namespace_frames { '^DBICTest' }
+
+1;
index c732181..65f90d1 100644 (file)
@@ -4,10 +4,7 @@ package #hide from pause
 use strict;
 use warnings;
 
-# must load before any DBIx::Class* namespaces
-use DBICTest::RunMode;
-
-use base 'DBIx::Class::Core';
+use base qw(DBICTest::Base DBIx::Class::Core);
 
 #use base qw/DBIx::Class::Relationship::Cascade::Rekey DBIx::Class::Core/;
 
index 77d22f2..2441cb7 100644 (file)
@@ -4,11 +4,7 @@ package #hide from pause
 use strict;
 use warnings;
 
-# must load before any DBIx::Class* namespaces
-use DBICTest::RunMode;
-
-use base 'DBIx::Class::ResultSet';
-__PACKAGE__->_skip_namespace_frames('^DBICTest');
+use base qw(DBICTest::Base DBIx::Class::ResultSet);
 
 sub all_hri {
   return [ shift->search ({}, { result_class => 'DBIx::Class::ResultClass::HashRefInflator' })->all ];
index 010e3e9..f210c2d 100644 (file)
@@ -3,10 +3,329 @@ package #hide from pause
 
 use strict;
 use warnings;
+use base qw(DBICTest::Base DBIx::Class::Schema);
 
-# must load before any DBIx::Class* namespaces
-use DBICTest::RunMode;
+use Fcntl qw(:DEFAULT :seek :flock);
+use Time::HiRes 'sleep';
+use DBIx::Class::_Util 'scope_guard';
+use DBICTest::Util::LeakTracer qw(populate_weakregistry assert_empty_weakregistry);
+use DBICTest::Util qw( local_umask await_flock dbg DEBUG_TEST_CONCURRENCY_LOCKS );
+use namespace::clean;
 
-use base 'DBIx::Class::Schema';
+if( $ENV{DBICTEST_ASSERT_NO_SPURIOUS_EXCEPTION_ACTION} ) {
+  __PACKAGE__->exception_action( sub {
+
+    my ( $fr_num, $disarmed, $throw_exception_fr_num );
+    while( ! $disarmed and my @fr = caller(++$fr_num) ) {
+
+      $throw_exception_fr_num ||= (
+        $fr[3] eq 'DBIx::Class::ResultSource::throw_exception'
+          and
+        $fr_num
+      );
+
+      $disarmed = !! (
+        $fr[1] =~ / \A (?: \. [\/\\] )? x?t [\/\\] .+ \.t \z /x
+          and
+        (
+          $fr[3] =~ /\A (?:
+            Test::Exception::throws_ok
+              |
+            Test::Exception::dies_ok
+              |
+            Try::Tiny::try
+              |
+            \Q(eval)\E
+          ) \z /x
+            or
+          (
+            $fr[3] eq 'Test::Exception::lives_ok'
+              and
+            ( $::TODO or Test::Builder->new->in_todo )
+          )
+        )
+      );
+    }
+
+    Test::Builder->new->ok(0, join "\n",
+      'Unexpected &exception_action invocation',
+      '',
+      '  You almost certainly used eval/try instead of dbic_internal_try()',
+      "  Adjust *one* of the eval-ish constructs in the callstack starting" . DBICTest::Util::stacktrace($throw_exception_fr_num||())
+    ) unless $disarmed;
+
+    DBIx::Class::Exception->throw( $_[0] );
+  })
+}
+
+sub capture_executed_sql_bind {
+  my ($self, $cref) = @_;
+
+  $self->throw_exception("Expecting a coderef to run") unless ref $cref eq 'CODE';
+
+  require DBICTest::SQLTracerObj;
+
+  # hack around stupid, stupid API
+  no warnings 'redefine';
+  local *DBIx::Class::Storage::DBI::_format_for_trace = sub { $_[1] };
+  Class::C3->reinitialize if DBIx::Class::_ENV_::OLD_MRO;
+
+  # can not use local() due to an unknown number of storages
+  # (think replicated)
+  my $orig_states = { map
+    { $_ => $self->storage->$_ }
+    qw(debugcb debugobj debug)
+  };
+
+  my $sg = scope_guard {
+    $self->storage->$_ ( $orig_states->{$_} ) for keys %$orig_states;
+  };
+
+  $self->storage->debugcb(undef);
+  $self->storage->debugobj( my $tracer_obj = DBICTest::SQLTracerObj->new );
+  $self->storage->debug(1);
+
+  local $Test::Builder::Level = $Test::Builder::Level + 2;
+  $cref->();
+
+  return $tracer_obj->{sqlbinds} || [];
+}
+
+sub is_executed_querycount {
+  my ($self, $cref, $exp_counts, $msg) = @_;
+
+  local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+  $self->throw_exception("Expecting an hashref of counts or an integer representing total query count")
+    unless ref $exp_counts eq 'HASH' or (defined $exp_counts and ! ref $exp_counts);
+
+  my @got = map { $_->[0] } @{ $self->capture_executed_sql_bind($cref) };
+
+  return Test::More::is( @got, $exp_counts, $msg )
+    unless ref $exp_counts;
+
+  my $got_counts = { map { $_ => 0 } keys %$exp_counts };
+  $got_counts->{$_}++ for @got;
+
+  return Test::More::is_deeply(
+    $got_counts,
+    $exp_counts,
+    $msg,
+  );
+}
+
+sub is_executed_sql_bind {
+  my ($self, $cref, $sqlbinds, $msg) = @_;
+
+  local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+  $self->throw_exception("Expecting an arrayref of SQL/Bind pairs") unless ref $sqlbinds eq 'ARRAY';
+
+  my @expected = @$sqlbinds;
+
+  my @got = map { $_->[1] } @{ $self->capture_executed_sql_bind($cref) };
+
+
+  return Test::Builder->new->ok(1, $msg || "No queries executed while running $cref")
+    if !@got and !@expected;
+
+  require SQL::Abstract::Test;
+  my $ret = 1;
+  while (@expected or @got) {
+    my $left = shift @got;
+    my $right = shift @expected;
+
+    # allow the right side to "simplify" the entire shebang
+    if ($left and $right) {
+      $left = [ @$left ];
+      for my $i (1..$#$right) {
+        if (
+          ! ref $right->[$i]
+            and
+          ref $left->[$i] eq 'ARRAY'
+            and
+          @{$left->[$i]} == 2
+        ) {
+          $left->[$i] = $left->[$i][1]
+        }
+      }
+    }
+
+    $ret &= SQL::Abstract::Test::is_same_sql_bind(
+      \( $left || [] ),
+      \( $right || [] ),
+      $msg,
+    );
+  }
+
+  return $ret;
+}
+
+our $locker;
+END {
+  # we need the $locker to be referenced here for delayed destruction
+  if ($locker->{lock_name} and ($ENV{DBICTEST_LOCK_HOLDER}||0) == $$) {
+    DEBUG_TEST_CONCURRENCY_LOCKS
+      and dbg "$locker->{type} LOCK RELEASED (END): $locker->{lock_name}";
+  }
+}
+
+my $weak_registry = {};
+
+sub connection {
+  my $self = shift->next::method(@_);
+
+# MASSIVE FIXME
+# we can't really lock based on DSN, as we do not yet have a way to tell that e.g.
+# DBICTEST_MSSQL_DSN=dbi:Sybase:server=192.168.0.11:1433;database=dbtst
+#  and
+# DBICTEST_MSSQL_ODBC_DSN=dbi:ODBC:server=192.168.0.11;port=1433;database=dbtst;driver=FreeTDS;tds_version=8.0
+# are the same server
+# hence we lock everything based on sqlt_type or just globally if not available
+# just pretend we are python you know? :)
+
+
+  # when we get a proper DSN resolution sanitize to produce a portable lockfile name
+  # this may look weird and unnecessary, but consider running tests from
+  # windows over a samba share >.>
+  #utf8::encode($dsn);
+  #$dsn =~ s/([^A-Za-z0-9_\-\.\=])/ sprintf '~%02X', ord($1) /ge;
+  #$dsn =~ s/^dbi/dbi/i;
+
+  # provide locking for physical (non-memory) DSNs, so that tests can
+  # safely run in parallel. While the harness (make -jN test) does set
+  # an envvar, we can not detect when a user invokes prove -jN. Hence
+  # perform the locking at all times, it shouldn't hurt.
+  # the lock fh *should* inherit across forks/subprocesses
+  if (
+    ! $DBICTest::global_exclusive_lock
+      and
+    ( ! $ENV{DBICTEST_LOCK_HOLDER} or $ENV{DBICTEST_LOCK_HOLDER} == $$ )
+      and
+    ref($_[0]) ne 'CODE'
+      and
+    ($_[0]||'') !~ /^ (?i:dbi) \: SQLite \: (?: dbname\= )? (?: \:memory\: | t [\/\\] var [\/\\] DBIxClass\-) /x
+  ) {
+
+    my $locktype;
+
+    {
+      # guard against infinite recursion
+      local $ENV{DBICTEST_LOCK_HOLDER} = -1;
+
+      # we need to work with a forced fresh clone so that we do not upset any state
+      # of the main $schema (some tests examine it quite closely)
+      local $SIG{__WARN__} = sub {};
+      local $@;
+
+      # this will either give us an undef $locktype or will determine things
+      # properly with a default ( possibly connecting in the process )
+      eval {
+        my $s = ref($self)->connect(@{$self->storage->connect_info})->storage;
+
+        $locktype = $s->sqlt_type || 'generic';
+
+        # in case sqlt_type did connect, doesn't matter if it fails or something
+        $s->disconnect;
+      };
+    }
+
+    # Never hold more than one lock. This solves the "lock in order" issues
+    # unrelated tests may have
+    # Also if there is no connection - there is no lock to be had
+    if ($locktype and (!$locker or $locker->{type} ne $locktype)) {
+
+      # this will release whatever lock we may currently be holding
+      # which is fine since the type does not match as checked above
+      DEBUG_TEST_CONCURRENCY_LOCKS
+        and $locker
+        and dbg "$locker->{type} LOCK RELEASED (UNDEF): $locker->{lock_name}";
+
+      undef $locker;
+
+      my $lockpath = DBICTest::RunMode->tmpdir->file("_dbictest_$locktype.lock");
+
+      DEBUG_TEST_CONCURRENCY_LOCKS
+        and dbg "Waiting for $locktype LOCK: $lockpath...";
+
+      my $lock_fh;
+      {
+        my $u = local_umask(0); # so that the file opens as 666, and any user can lock
+        sysopen ($lock_fh, $lockpath, O_RDWR|O_CREAT) or die "Unable to open $lockpath: $!";
+      }
+
+      await_flock ($lock_fh, LOCK_EX) or die "Unable to lock $lockpath: $!";
+
+      DEBUG_TEST_CONCURRENCY_LOCKS
+        and dbg "Got $locktype LOCK: $lockpath";
+
+      # see if anyone was holding a lock before us, and wait up to 5 seconds for them to terminate
+      # if we do not do this we may end up trampling over some long-running END or somesuch
+      seek ($lock_fh, 0, SEEK_SET) or die "seek failed $!";
+      my $old_pid;
+      if (
+        read ($lock_fh, $old_pid, 100)
+          and
+        ($old_pid) = $old_pid =~ /^(\d+)$/
+      ) {
+        DEBUG_TEST_CONCURRENCY_LOCKS
+          and dbg "Post-grab WAIT for $old_pid START: $lockpath";
+
+        for (1..50) {
+          kill (0, $old_pid) or last;
+          sleep 0.1;
+        }
+
+        DEBUG_TEST_CONCURRENCY_LOCKS
+          and dbg "Post-grab WAIT for $old_pid FINISHED: $lockpath";
+      }
+
+      truncate $lock_fh, 0;
+      seek ($lock_fh, 0, SEEK_SET) or die "seek failed $!";
+      $lock_fh->autoflush(1);
+      print $lock_fh $$;
+
+      $ENV{DBICTEST_LOCK_HOLDER} ||= $$;
+
+      $locker = {
+        type => $locktype,
+        fh => $lock_fh,
+        lock_name => "$lockpath",
+      };
+    }
+  }
+
+  if ($INC{'Test/Builder.pm'}) {
+    populate_weakregistry ( $weak_registry, $self->storage );
+
+    my $cur_connect_call = $self->storage->on_connect_call;
+
+    $self->storage->on_connect_call([
+      (ref $cur_connect_call eq 'ARRAY'
+        ? @$cur_connect_call
+        : ($cur_connect_call || ())
+      ),
+      [sub {
+        populate_weakregistry( $weak_registry, shift->_dbh )
+      }],
+    ]);
+  }
+
+  return $self;
+}
+
+sub clone {
+  my $self = shift->next::method(@_);
+  populate_weakregistry ( $weak_registry, $self )
+    if $INC{'Test/Builder.pm'};
+  $self;
+}
+
+END {
+  # Make sure we run after any cleanup in other END blocks
+  push @{ B::end_av()->object_2svref }, sub {
+    assert_empty_weakregistry($weak_registry, 'quiet');
+  };
+}
 
 1;
index ab47d0c..93f917c 100644 (file)
@@ -13,6 +13,53 @@ BEGIN {
 
     die __PACKAGE__ . " must be loaded before DBIx::Class (or modules using DBIx::Class) at $frame[1] line $frame[2]\n";
   }
+
+  if ( $ENV{DBICTEST_VERSION_WARNS_INDISCRIMINATELY} ) {
+    my $ov = UNIVERSAL->can("VERSION");
+
+    require Carp;
+
+    no warnings 'redefine';
+    *UNIVERSAL::VERSION = sub {
+      Carp::carp( 'Argument "blah bleh bloh" isn\'t numeric in subroutine entry' );
+      &$ov;
+    };
+  }
+
+  if (
+    $ENV{DBICTEST_ASSERT_NO_SPURIOUS_EXCEPTION_ACTION}
+      or
+    # keep it always on during CI
+    (
+      ($ENV{TRAVIS}||'') eq 'true'
+        and
+      ($ENV{TRAVIS_REPO_SLUG}||'') =~ m|\w+/dbix-class$|
+    )
+  ) {
+    require Try::Tiny;
+    my $orig = \&Try::Tiny::try;
+
+    no warnings 'redefine';
+    *Try::Tiny::try = sub (&;@) {
+      my ($fr, $first_pkg) = 0;
+      while( $first_pkg = caller($fr++) ) {
+        last if $first_pkg !~ /^
+          __ANON__
+            |
+          \Q(eval)\E
+        $/x;
+      }
+
+      if ($first_pkg =~ /DBIx::Class/) {
+        require Test::Builder;
+        Test::Builder->new->ok(0,
+          'Using try{} within DBIC internals is a mistake - use dbic_internal_try{} instead'
+        );
+      }
+
+      goto $orig;
+    };
+  }
 }
 
 use Path::Class qw/file dir/;
@@ -36,7 +83,7 @@ sub tmpdir {
     my $reason_dir_unusable;
 
     my @parts = File::Spec->splitdir($dir);
-    if (@parts == 2 and $parts[1] =~ /^ [ \\ \/ ]? $/x ) {
+    if (@parts == 2 and $parts[1] =~ /^ [\/\\]? $/x ) {
       $reason_dir_unusable =
         'File::Spec->tmpdir returned a root directory instead of a designated '
       . 'tempdir (possibly https://rt.cpan.org/Ticket/Display.html?id=76663)';
@@ -197,15 +244,31 @@ sub is_author {
 }
 
 sub is_smoker {
-  return
-    ( ($ENV{TRAVIS}||'') eq 'true' )
-      ||
+  return (
     ( $ENV{AUTOMATED_TESTING} && ! $ENV{PERL5_CPANM_IS_RUNNING} && ! $ENV{RELEASE_TESTING} )
-  ;
+      or
+    __PACKAGE__->is_ci
+  );
+}
+
+sub is_ci {
+  return (
+    ($ENV{TRAVIS}||'') eq 'true'
+      and
+    ($ENV{TRAVIS_REPO_SLUG}||'') =~ m|\w+/dbix-class$|
+  )
 }
 
 sub is_plain {
-  return (! __PACKAGE__->is_smoker && ! __PACKAGE__->is_author && ! $ENV{RELEASE_TESTING} )
+  return (
+    ! $ENV{RELEASE_TESTING}
+      and
+    ! $ENV{DBICTEST_RUN_ALL_TESTS}
+      and
+    ! __PACKAGE__->is_smoker
+      and
+    ! __PACKAGE__->is_author
+  )
 }
 
 # Try to determine the root of a checkout/untar if possible
diff --git a/t/lib/DBICTest/SQLTracerObj.pm b/t/lib/DBICTest/SQLTracerObj.pm
new file mode 100644 (file)
index 0000000..5763639
--- /dev/null
@@ -0,0 +1,30 @@
+package # moar hide
+  DBICTest::SQLTracerObj;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Storage::Statistics';
+
+sub query_start {
+  my ($self, $sql, $bind) = @_;
+
+  my $op = ($sql =~ /^\s*(\S+)/)[0];
+
+  $sql =~ s/^ \s* \Q$op\E \s+ \[ .+? \]/$op/x
+    if $ENV{DBICTEST_VIA_REPLICATED};
+
+  push @{$self->{sqlbinds}}, [ $op, [ $sql, @{ $bind || [] } ] ];
+}
+
+# who the hell came up with this API >:(
+for my $txn (qw(begin rollback commit)) {
+  no strict 'refs';
+  *{"txn_$txn"} = sub { push @{$_[0]{sqlbinds}}, [ uc $txn => [ uc $txn ] ] };
+}
+
+sub svp_begin { push @{$_[0]{sqlbinds}}, [ SAVEPOINT => [ "SAVEPOINT $_[1]" ] ] }
+sub svp_release { push @{$_[0]{sqlbinds}}, [ RELEASE_SAVEPOINT => [ "RELEASE $_[1]" ] ] }
+sub svp_rollback { push @{$_[0]{sqlbinds}}, [ ROLLBACK_TO_SAVEPOINT => [ "ROLLBACK TO $_[1]" ] ] }
+
+1;
index b39ecbc..2e783a7 100644 (file)
@@ -7,13 +7,6 @@ no warnings 'qw';
 
 use base 'DBICTest::BaseSchema';
 
-use Fcntl qw/:DEFAULT :seek :flock/;
-use Time::HiRes 'sleep';
-use DBICTest::RunMode;
-use DBICTest::Util::LeakTracer qw/populate_weakregistry assert_empty_weakregistry/;
-use DBICTest::Util 'local_umask';
-use namespace::clean;
-
 __PACKAGE__->mk_group_accessors(simple => 'custom_attr');
 
 __PACKAGE__->load_classes(qw/
@@ -58,7 +51,7 @@ __PACKAGE__->load_classes(qw/
     'CD_to_Producer',
     'Dummy',    # this is a real result class we remove in the hook below
   ),
-  qw/SelfRefAlias TreeLike TwoKeyTreeLike Event EventTZ NoPrimaryKey/,
+  qw/SelfRefAlias TreeLike TwoKeyTreeLike Event NoPrimaryKey/,
   qw/Collection CollectionObject TypedObject Owners BooksInLibrary/,
   qw/ForceForeign Encoded/,
 );
@@ -69,160 +62,4 @@ sub sqlt_deploy_hook {
   $sqlt_schema->drop_table('dummy');
 }
 
-
-our $locker;
-END {
-  # we need the $locker to be referenced here for delayed destruction
-  if ($locker->{lock_name} and ($ENV{DBICTEST_LOCK_HOLDER}||0) == $$) {
-    #warn "$$ $0 $locker->{type} LOCK RELEASED";
-  }
-}
-
-my $weak_registry = {};
-
-sub connection {
-  my $self = shift->next::method(@_);
-
-# MASSIVE FIXME
-# we can't really lock based on DSN, as we do not yet have a way to tell that e.g.
-# DBICTEST_MSSQL_DSN=dbi:Sybase:server=192.168.0.11:1433;database=dbtst
-#  and
-# DBICTEST_MSSQL_ODBC_DSN=dbi:ODBC:server=192.168.0.11;port=1433;database=dbtst;driver=FreeTDS;tds_version=8.0
-# are the same server
-# hence we lock everything based on sqlt_type or just globally if not available
-# just pretend we are python you know? :)
-
-
-  # when we get a proper DSN resolution sanitize to produce a portable lockfile name
-  # this may look weird and unnecessary, but consider running tests from
-  # windows over a samba share >.>
-  #utf8::encode($dsn);
-  #$dsn =~ s/([^A-Za-z0-9_\-\.\=])/ sprintf '~%02X', ord($1) /ge;
-  #$dsn =~ s/^dbi/dbi/i;
-
-  # provide locking for physical (non-memory) DSNs, so that tests can
-  # safely run in parallel. While the harness (make -jN test) does set
-  # an envvar, we can not detect when a user invokes prove -jN. Hence
-  # perform the locking at all times, it shouldn't hurt.
-  # the lock fh *should* inherit across forks/subprocesses
-  #
-  # File locking is hard. Really hard. By far the best lock implementation
-  # I've seen is part of the guts of File::Temp. However it is sadly not
-  # reusable. Since I am not aware of folks doing NFS parallel testing,
-  # nor are we known to work on VMS, I am just going to punt this and
-  # use the portable-ish flock() provided by perl itself. If this does
-  # not work for you - patches more than welcome.
-  if (
-    ! $DBICTest::global_exclusive_lock
-      and
-    ( ! $ENV{DBICTEST_LOCK_HOLDER} or $ENV{DBICTEST_LOCK_HOLDER} == $$ )
-      and
-    ref($_[0]) ne 'CODE'
-      and
-    ($_[0]||'') !~ /^ (?i:dbi) \: SQLite \: (?: dbname\= )? (?: \:memory\: | t [\/\\] var [\/\\] DBIxClass\-) /x
-  ) {
-
-    my $locktype = do {
-      # guard against infinite recursion
-      local $ENV{DBICTEST_LOCK_HOLDER} = -1;
-
-      # we need to connect a forced fresh clone so that we do not upset any state
-      # of the main $schema (some tests examine it quite closely)
-      local $@;
-      my $storage = eval {
-        my $st = ref($self)->connect(@{$self->storage->connect_info})->storage;
-        $st->ensure_connected;  # do connect here, to catch a possible throw
-        $st;
-      };
-      $storage
-        ? do {
-          my $t = $storage->sqlt_type || 'generic';
-          eval { $storage->disconnect };
-          $t;
-        }
-        : undef
-      ;
-    };
-
-    # Never hold more than one lock. This solves the "lock in order" issues
-    # unrelated tests may have
-    # Also if there is no connection - there is no lock to be had
-    if ($locktype and (!$locker or $locker->{type} ne $locktype)) {
-
-      # this will release whatever lock we may currently be holding
-      # which is fine since the type does not match as checked above
-      undef $locker;
-
-      my $lockpath = DBICTest::RunMode->tmpdir->file("_dbictest_$locktype.lock");
-
-      #warn "$$ $0 $locktype GRABBING LOCK";
-      my $lock_fh;
-      {
-        my $u = local_umask(0); # so that the file opens as 666, and any user can lock
-        sysopen ($lock_fh, $lockpath, O_RDWR|O_CREAT) or die "Unable to open $lockpath: $!";
-      }
-      flock ($lock_fh, LOCK_EX) or die "Unable to lock $lockpath: $!";
-      #warn "$$ $0 $locktype LOCK GRABBED";
-
-      # see if anyone was holding a lock before us, and wait up to 5 seconds for them to terminate
-      # if we do not do this we may end up trampling over some long-running END or somesuch
-      seek ($lock_fh, 0, SEEK_SET) or die "seek failed $!";
-      my $old_pid;
-      if (
-        read ($lock_fh, $old_pid, 100)
-          and
-        ($old_pid) = $old_pid =~ /^(\d+)$/
-      ) {
-        for (1..50) {
-          kill (0, $old_pid) or last;
-          sleep 0.1;
-        }
-      }
-      #warn "$$ $0 $locktype POST GRAB WAIT";
-
-      truncate $lock_fh, 0;
-      seek ($lock_fh, 0, SEEK_SET) or die "seek failed $!";
-      $lock_fh->autoflush(1);
-      print $lock_fh $$;
-
-      $ENV{DBICTEST_LOCK_HOLDER} ||= $$;
-
-      $locker = {
-        type => $locktype,
-        fh => $lock_fh,
-        lock_name => "$lockpath",
-      };
-    }
-  }
-
-  if ($INC{'Test/Builder.pm'}) {
-    populate_weakregistry ( $weak_registry, $self->storage );
-
-    my $cur_connect_call = $self->storage->on_connect_call;
-
-    $self->storage->on_connect_call([
-      (ref $cur_connect_call eq 'ARRAY'
-        ? @$cur_connect_call
-        : ($cur_connect_call || ())
-      ),
-      [sub {
-        populate_weakregistry( $weak_registry, shift->_dbh )
-      }],
-    ]);
-  }
-
-  return $self;
-}
-
-sub clone {
-  my $self = shift->next::method(@_);
-  populate_weakregistry ( $weak_registry, $self )
-    if $INC{'Test/Builder.pm'};
-  $self;
-}
-
-END {
-  assert_empty_weakregistry($weak_registry, 'quiet');
-}
-
 1;
index a99eb7e..00c1ef6 100644 (file)
@@ -4,8 +4,8 @@ package # hide from PAUSE
 use warnings;
 use strict;
 
-use base qw/DBICTest::BaseResult/;
-use Carp qw/confess/;
+use base 'DBICTest::BaseResult';
+use DBICTest::Util 'check_customcond_args';
 
 __PACKAGE__->table('artist');
 __PACKAGE__->source_info({
@@ -51,26 +51,38 @@ __PACKAGE__->has_many(
     { order_by => { -asc => 'year'} },
 );
 
+__PACKAGE__->has_many(
+  cds_cref_cond => 'DBICTest::Schema::CD',
+  sub {
+    # This is for test purposes only. A regular user does not
+    # need to sanity check the passed-in arguments, this is what
+    # the tests are for :)
+    my $args = &check_customcond_args;
+
+    return (
+      { "$args->{foreign_alias}.artist" => { '=' => { -ident => "$args->{self_alias}.artistid"} },
+      },
+      $args->{self_result_object} && {
+        "$args->{foreign_alias}.artist" => $args->{self_rowobj}->artistid,  # keep old rowobj syntax as a test
+      }
+    );
+  },
+);
 
 __PACKAGE__->has_many(
   cds_80s => 'DBICTest::Schema::CD',
   sub {
-    my $args = shift;
-
     # This is for test purposes only. A regular user does not
     # need to sanity check the passed-in arguments, this is what
     # the tests are for :)
-    my @missing_args = grep { ! defined $args->{$_} }
-      qw/self_alias foreign_alias self_resultsource foreign_relname/;
-    confess "Required arguments not supplied to custom rel coderef: @missing_args\n"
-      if @missing_args;
+    my $args = &check_customcond_args;
 
     return (
-      { "$args->{foreign_alias}.artist" => { '=' => { -ident => "$args->{self_alias}.artistid"} },
+      { "$args->{foreign_alias}.artist" => { '=' => \ "$args->{self_alias}.artistid" },
         "$args->{foreign_alias}.year"   => { '>' => 1979, '<' => 1990 },
       },
-      $args->{self_rowobj} && {
-        "$args->{foreign_alias}.artist" => $args->{self_rowobj}->artistid,
+      $args->{self_result_object} && {
+        "$args->{foreign_alias}.artist" => { '=' => \[ '?',  $args->{self_result_object}->artistid ] },
         "$args->{foreign_alias}.year"   => { '>' => 1979, '<' => 1990 },
       }
     );
@@ -81,22 +93,17 @@ __PACKAGE__->has_many(
 __PACKAGE__->has_many(
   cds_84 => 'DBICTest::Schema::CD',
   sub {
-    my $args = shift;
-
     # This is for test purposes only. A regular user does not
     # need to sanity check the passed-in arguments, this is what
     # the tests are for :)
-    my @missing_args = grep { ! defined $args->{$_} }
-      qw/self_alias foreign_alias self_resultsource foreign_relname/;
-    confess "Required arguments not supplied to custom rel coderef: @missing_args\n"
-      if @missing_args;
+    my $args = &check_customcond_args;
 
     return (
       { "$args->{foreign_alias}.artist" => { -ident => "$args->{self_alias}.artistid" },
         "$args->{foreign_alias}.year"   => 1984,
       },
-      $args->{self_rowobj} && {
-        "$args->{foreign_alias}.artist" => $args->{self_rowobj}->artistid,
+      $args->{self_result_object} && {
+        "$args->{foreign_alias}.artist" => $args->{self_result_object}->artistid,
         "$args->{foreign_alias}.year"   => 1984,
       }
     );
@@ -107,15 +114,10 @@ __PACKAGE__->has_many(
 __PACKAGE__->has_many(
   cds_90s => 'DBICTest::Schema::CD',
   sub {
-    my $args = shift;
-
     # This is for test purposes only. A regular user does not
     # need to sanity check the passed-in arguments, this is what
     # the tests are for :)
-    my @missing_args = grep { ! defined $args->{$_} }
-      qw/self_alias foreign_alias self_resultsource foreign_relname/;
-    confess "Required arguments not supplied to custom rel coderef: @missing_args\n"
-      if @missing_args;
+    my $args = &check_customcond_args;
 
     return (
       { "$args->{foreign_alias}.artist" => { -ident => "$args->{self_alias}.artistid" },
@@ -150,13 +152,17 @@ __PACKAGE__->many_to_many('artworks', 'artwork_to_artist', 'artwork');
 __PACKAGE__->has_many(
     cds_without_genre => 'DBICTest::Schema::CD',
     sub {
-        my $args = shift;
+        # This is for test purposes only. A regular user does not
+        # need to sanity check the passed-in arguments, this is what
+        # the tests are for :)
+        my $args = &check_customcond_args;
+
         return (
           {
             "$args->{foreign_alias}.artist" => { -ident => "$args->{self_alias}.artistid" },
             "$args->{foreign_alias}.genreid" => undef,
-          }, $args->{self_rowobj} && {
-            "$args->{foreign_alias}.artist" => $args->{self_rowobj}->artistid,
+          }, $args->{self_result_object} && {
+            "$args->{foreign_alias}.artist" => $args->{self_result_object}->artistid,
             "$args->{foreign_alias}.genreid" => undef,
           }
         ),
@@ -174,7 +180,7 @@ sub sqlt_deploy_hook {
 
 sub store_column {
   my ($self, $name, $value) = @_;
-  $value = 'X '.$value if ($name eq 'name' && $value && $value =~ /(X )?store_column test/);
+  $value = 'X '.$value if ($name eq 'name' && defined $value && $value =~ /(X )?store_column test/);
   $self->next::method($name, $value);
 }
 
index 01ce450..d3f56b7 100644 (file)
@@ -4,8 +4,8 @@ package # hide from PAUSE
 use warnings;
 use strict;
 
-use base qw/DBICTest::BaseResult/;
-use Carp qw/confess/;
+use base 'DBICTest::BaseResult';
+use DBICTest::Util 'check_customcond_args';
 
 __PACKAGE__->table('cd_artwork');
 __PACKAGE__->add_columns(
@@ -21,32 +21,41 @@ __PACKAGE__->has_many('images', 'DBICTest::Schema::Image', 'artwork_id');
 __PACKAGE__->has_many('artwork_to_artist', 'DBICTest::Schema::Artwork_to_Artist', 'artwork_cd_id');
 __PACKAGE__->many_to_many('artists', 'artwork_to_artist', 'artist');
 
-# both to test manytomany with custom rel
-__PACKAGE__->many_to_many('artists_test_m2m', 'artwork_to_artist', 'artist_test_m2m');
-__PACKAGE__->many_to_many('artists_test_m2m_noopt', 'artwork_to_artist', 'artist_test_m2m_noopt');
+# both to test manytomany via double custom rel (deliberate misnamed accessor clash)
+__PACKAGE__->many_to_many('artist_limited_rank', 'artwork_to_artist_via_customcond', 'artist_limited_rank');
+__PACKAGE__->many_to_many('artist_limited_rank_opaque', 'artwork_to_artist_via_opaque_customcond', 'artist_limited_rank_opaque');
 
-# other test to manytomany
-__PACKAGE__->has_many('artwork_to_artist_test_m2m', 'DBICTest::Schema::Artwork_to_Artist',
+__PACKAGE__->has_many('artwork_to_artist_via_customcond', 'DBICTest::Schema::Artwork_to_Artist',
   sub {
-    my $args = shift;
-
     # This is for test purposes only. A regular user does not
     # need to sanity check the passed-in arguments, this is what
     # the tests are for :)
-    my @missing_args = grep { ! defined $args->{$_} }
-      qw/self_alias foreign_alias self_resultsource foreign_relname/;
-    confess "Required arguments not supplied to custom rel coderef: @missing_args\n"
-      if @missing_args;
+    my $args = &check_customcond_args;
 
     return (
       { "$args->{foreign_alias}.artwork_cd_id" => { -ident => "$args->{self_alias}.cd_id" },
       },
-      $args->{self_rowobj} && {
-        "$args->{foreign_alias}.artwork_cd_id" => $args->{self_rowobj}->cd_id,
+      $args->{self_result_object} && {
+        "$args->{foreign_alias}.artwork_cd_id" => $args->{self_result_object}->cd_id,
       }
     );
   }
 );
-__PACKAGE__->many_to_many('artists_test_m2m2', 'artwork_to_artist_test_m2m', 'artist');
+
+__PACKAGE__->has_many('artwork_to_artist_via_opaque_customcond', 'DBICTest::Schema::Artwork_to_Artist',
+  sub {
+    # This is for test purposes only. A regular user does not
+    # need to sanity check the passed-in arguments, this is what
+    # the tests are for :)
+    my $args = &check_customcond_args;
+
+    return (
+      { "$args->{foreign_alias}.artwork_cd_id" => { -ident => "$args->{self_alias}.cd_id" } },
+    );
+  }
+);
+
+__PACKAGE__->many_to_many('all_artists_via_opaque_customcond', 'artwork_to_artist_via_opaque_customcond', 'artist');
+
 
 1;
index 57326e2..c84d74f 100644 (file)
@@ -4,8 +4,8 @@ package # hide from PAUSE
 use warnings;
 use strict;
 
-use base qw/DBICTest::BaseResult/;
-use Carp qw/confess/;
+use base 'DBICTest::BaseResult';
+use DBICTest::Util 'check_customcond_args';
 
 __PACKAGE__->table('artwork_to_artist');
 __PACKAGE__->add_columns(
@@ -22,41 +22,34 @@ __PACKAGE__->set_primary_key(qw/artwork_cd_id artist_id/);
 __PACKAGE__->belongs_to('artwork', 'DBICTest::Schema::Artwork', 'artwork_cd_id');
 __PACKAGE__->belongs_to('artist', 'DBICTest::Schema::Artist', 'artist_id');
 
-__PACKAGE__->belongs_to('artist_test_m2m', 'DBICTest::Schema::Artist',
+__PACKAGE__->belongs_to('artist_limited_rank', 'DBICTest::Schema::Artist',
   sub {
-    my $args = shift;
-
     # This is for test purposes only. A regular user does not
     # need to sanity check the passed-in arguments, this is what
     # the tests are for :)
-    my @missing_args = grep { ! defined $args->{$_} }
-      qw/self_alias foreign_alias self_resultsource foreign_relname/;
-    confess "Required arguments not supplied to custom rel coderef: @missing_args\n"
-      if @missing_args;
+    my $args = &check_customcond_args;
 
     return (
       { "$args->{foreign_alias}.artistid" => { -ident => "$args->{self_alias}.artist_id" },
         "$args->{foreign_alias}.rank"     => { '<' => 10 },
       },
-      $args->{self_rowobj} && {
-        "$args->{foreign_alias}.artistid" => $args->{self_rowobj}->artist_id,
+      !$args->{self_result_object} ? () : {
+        "$args->{foreign_alias}.artistid" => $args->{self_result_object}->artist_id,
         "$args->{foreign_alias}.rank"   => { '<' => 10 },
-      }
+      },
+      !$args->{foreign_values} ? () : {
+        "$args->{self_alias}.artist_id" => $args->{foreign_values}{artistid},
+      },
     );
   }
 );
 
-__PACKAGE__->belongs_to('artist_test_m2m_noopt', 'DBICTest::Schema::Artist',
+__PACKAGE__->belongs_to('artist_limited_rank_opaque', 'DBICTest::Schema::Artist',
   sub {
-    my $args = shift;
-
     # This is for test purposes only. A regular user does not
     # need to sanity check the passed-in arguments, this is what
     # the tests are for :)
-    my @missing_args = grep { ! defined $args->{$_} }
-      qw/self_alias foreign_alias self_resultsource foreign_relname/;
-    confess "Required arguments not supplied to custom rel coderef: @missing_args\n"
-      if @missing_args;
+    my $args = &check_customcond_args;
 
     return (
       { "$args->{foreign_alias}.artistid" => { -ident => "$args->{self_alias}.artist_id" },
index 45fdf6f..1a0771b 100644 (file)
@@ -4,7 +4,8 @@ package # hide from PAUSE
 use warnings;
 use strict;
 
-use base qw/DBICTest::BaseResult/;
+use base 'DBICTest::BaseResult';
+use DBICTest::Util 'check_customcond_args';
 
 # this tests table name as scalar ref
 # DO NOT REMOVE THE \
@@ -54,6 +55,14 @@ __PACKAGE__->belongs_to( single_track => 'DBICTest::Schema::Track',
   { join_type => 'left'},
 );
 
+__PACKAGE__->belongs_to( single_track_opaque => 'DBICTest::Schema::Track',
+  sub {
+    my $args = &check_customcond_args;
+    \ " $args->{foreign_alias}.trackid = $args->{self_alias}.single_track ";
+  },
+  { join_type => 'left'},
+);
+
 # add a non-left single relationship for the complex prefetch tests
 __PACKAGE__->belongs_to( existing_single_track => 'DBICTest::Schema::Track',
   { 'foreign.trackid' => 'self.single_track' },
@@ -68,6 +77,9 @@ __PACKAGE__->has_many(
     cd_to_producer => 'DBICTest::Schema::CD_to_Producer' => 'cd'
 );
 
+__PACKAGE__->has_many( twokeys => 'DBICTest::Schema::TwoKeys', 'cd' );
+
+
 # the undef condition in this rel is *deliberate*
 # tests oddball legacy syntax
 __PACKAGE__->might_have(
@@ -118,7 +130,11 @@ __PACKAGE__->might_have(
     'last_track',
     'DBICTest::Schema::Track',
     sub {
-        my $args = shift;
+        # This is for test purposes only. A regular user does not
+        # need to sanity check the passed-in arguments, this is what
+        # the tests are for :)
+        my $args = &check_customcond_args;
+
         return (
             {
                 "$args->{foreign_alias}.trackid" => { '=' =>
index 5077bd0..10d49f7 100644 (file)
@@ -4,8 +4,8 @@ package # hide from PAUSE
 use warnings;
 use strict;
 
-use base qw/DBICTest::BaseResult/;
-use Carp qw/confess/;
+use base 'DBICTest::BaseResult';
+use DBICTest::Util 'check_customcond_args';
 
 __PACKAGE__->load_components(qw{
     +DBICTest::DeployComponent
@@ -53,6 +53,29 @@ __PACKAGE__->grouping_column ('cd');
 __PACKAGE__->belongs_to( cd => 'DBICTest::Schema::CD', undef, {
     proxy => { cd_title => 'title' },
 });
+# custom condition coderef
+__PACKAGE__->belongs_to( cd_cref_cond => 'DBICTest::Schema::CD',
+sub {
+  # This is for test purposes only. A regular user does not
+  # need to sanity check the passed-in arguments, this is what
+  # the tests are for :)
+  my $args = &check_customcond_args;
+
+  return (
+    {
+      "$args->{foreign_alias}.cdid" => { -ident => "$args->{self_alias}.cd" },
+    },
+
+    ! $args->{self_result_object} ? () : {
+     "$args->{foreign_alias}.cdid" => $args->{self_result_object}->get_column('cd')
+    },
+
+    ! $args->{foreign_values} ? () : {
+     "$args->{self_alias}.cd" => $args->{foreign_values}{cdid}
+    },
+  );
+}
+);
 __PACKAGE__->belongs_to( disc => 'DBICTest::Schema::CD' => 'cd', {
     proxy => 'year'
 });
@@ -76,28 +99,37 @@ __PACKAGE__->belongs_to(
 __PACKAGE__->has_many (
   next_tracks => __PACKAGE__,
   sub {
-    my $args = shift;
-
     # This is for test purposes only. A regular user does not
     # need to sanity check the passed-in arguments, this is what
     # the tests are for :)
-    my @missing_args = grep { ! defined $args->{$_} }
-      qw/self_alias foreign_alias self_resultsource foreign_relname/;
-    confess "Required arguments not supplied to custom rel coderef: @missing_args\n"
-      if @missing_args;
+    my $args = &check_customcond_args;
 
     return (
       { "$args->{foreign_alias}.cd"       => { -ident => "$args->{self_alias}.cd" },
         "$args->{foreign_alias}.position" => { '>' => { -ident => "$args->{self_alias}.position" } },
       },
-      $args->{self_rowobj} && {
-        "$args->{foreign_alias}.cd"       => $args->{self_rowobj}->get_column('cd'),
-        "$args->{foreign_alias}.position" => { '>' => $args->{self_rowobj}->pos },
+      $args->{self_result_object} && {
+        "$args->{foreign_alias}.cd"       => $args->{self_result_object}->get_column('cd'),
+        "$args->{foreign_alias}.position" => { '>' => $args->{self_result_object}->pos },
       }
     )
   }
 );
 
+__PACKAGE__->has_many (
+  deliberately_broken_all_cd_tracks => __PACKAGE__,
+  sub {
+    # This is for test purposes only. A regular user does not
+    # need to sanity check the passed-in arguments, this is what
+    # the tests are for :)
+    my $args = &check_customcond_args;
+
+    return {
+      "$args->{foreign_alias}.cd" => "$args->{self_alias}.cd"
+    };
+  }
+);
+
 our $hook_cb;
 
 sub sqlt_deploy_hook {
index ff8f980..d28cf60 100644 (file)
@@ -18,7 +18,7 @@ __PACKAGE__->belongs_to(
     {'foreign.artistid'=>'self.artist'},
 );
 
-__PACKAGE__->belongs_to( cd => 'DBICTest::Schema::CD', undef, { is_deferrable => 0, add_fk_index => 0 } );
+__PACKAGE__->belongs_to( cd => 'DBICTest::Schema::CD', undef, { is_deferrable => 0, on_update => undef, on_delete => undef, add_fk_index => 0 } );
 
 __PACKAGE__->has_many(
   'fourkeys_to_twokeys', 'DBICTest::Schema::FourKeys_to_TwoKeys', {
index 3ae7de5..6ee67d5 100644 (file)
@@ -11,7 +11,7 @@ __PACKAGE__->table('year2000cds');
 
 # need to operate on the instance for things to work
 __PACKAGE__->result_source_instance->view_definition( sprintf (
-  'SELECT %s FROM cd WHERE year = "2000"',
+  "SELECT %s FROM cd WHERE year = '2000'",
   join (', ', __PACKAGE__->columns),
 ));
 
diff --git a/t/lib/DBICTest/Stats.pm b/t/lib/DBICTest/Stats.pm
deleted file mode 100644 (file)
index 5a4544f..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-package DBICTest::Stats;
-use strict;
-use warnings;
-
-use base qw/DBIx::Class::Storage::Statistics/;
-
-sub txn_begin {
-  my $self = shift;
-
-  $self->{'TXN_BEGIN'}++;
-  return $self->{'TXN_BEGIN'};
-}
-
-sub txn_rollback {
-  my $self = shift;
-
-  $self->{'TXN_ROLLBACK'}++;
-  return $self->{'TXN_ROLLBACK'};
-}
-
-sub txn_commit {
-  my $self = shift;
-
-  $self->{'TXN_COMMIT'}++;
-  return $self->{'TXN_COMMIT'};
-}
-
-sub svp_begin {
-  my ($self, $name) = @_;
-
-  $self->{'SVP_BEGIN'}++;
-  return $self->{'SVP_BEGIN'};
-}
-
-sub svp_release {
-  my ($self, $name) = @_;
-
-  $self->{'SVP_RELEASE'}++;
-  return $self->{'SVP_RELEASE'};
-}
-
-sub svp_rollback {
-  my ($self, $name) = @_;
-
-  $self->{'SVP_ROLLBACK'}++;
-  return $self->{'SVP_ROLLBACK'};
-}
-
-sub query_start {
-  my ($self, $string, @bind) = @_;
-
-  $self->{'QUERY_START'}++;
-  return $self->{'QUERY_START'};
-}
-
-sub query_end {
-  my ($self, $string) = @_;
-
-  $self->{'QUERY_END'}++;
-  return $self->{'QUERY_START'};
-}
-
-1;
index 0cd2b12..f747210 100644 (file)
@@ -3,30 +3,113 @@ package DBICTest::Util;
 use warnings;
 use strict;
 
+# this noop trick initializes the STDOUT, so that the TAP::Harness
+# issued IO::Select->can_read calls (which are blocking wtf wtf wtf)
+# keep spinning and scheduling jobs
+# This results in an overall much smoother job-queue drainage, since
+# the Harness blocks less
+# (ideally this needs to be addressed in T::H, but a quick patchjob
+# broke everything so tabling it for now)
+BEGIN {
+  if ($INC{'Test/Builder.pm'}) {
+    local $| = 1;
+    print "#\n";
+  }
+}
+
+use constant DEBUG_TEST_CONCURRENCY_LOCKS =>
+  ( ($ENV{DBICTEST_DEBUG_CONCURRENCY_LOCKS}||'') =~ /^(\d+)$/ )[0]
+    ||
+  0
+;
+
 use Config;
+use Carp qw(cluck confess croak);
+use Fcntl ':flock';
+use Scalar::Util qw(blessed refaddr);
+use DBIx::Class::_Util 'scope_guard';
 
 use base 'Exporter';
-our @EXPORT_OK = qw/local_umask stacktrace/;
+our @EXPORT_OK = qw(
+  dbg stacktrace
+  local_umask
+  visit_namespaces
+  check_customcond_args
+  await_flock DEBUG_TEST_CONCURRENCY_LOCKS
+);
+
+if (DEBUG_TEST_CONCURRENCY_LOCKS) {
+  require DBI;
+  my $oc = DBI->can('connect');
+  no warnings 'redefine';
+  *DBI::connect = sub {
+    DBICTest::Util::dbg("Connecting to $_[1]");
+    goto $oc;
+  }
+}
+
+sub dbg ($) {
+  require Time::HiRes;
+  printf STDERR "\n%.06f  %5s %-78s %s\n",
+    scalar Time::HiRes::time(),
+    $$,
+    $_[0],
+    $0,
+  ;
+}
+
+# File locking is hard. Really hard. By far the best lock implementation
+# I've seen is part of the guts of File::Temp. However it is sadly not
+# reusable. Since I am not aware of folks doing NFS parallel testing,
+# nor are we known to work on VMS, I am just going to punt this and
+# use the portable-ish flock() provided by perl itself. If this does
+# not work for you - patches more than welcome.
+#
+# This figure esentially means "how long can a single test hold a
+# resource before everyone else gives up waiting and aborts" or
+# in other words "how long does the longest test-group legitimally run?"
+my $lock_timeout_minutes = 15;  # yes, that's long, I know
+my $wait_step_seconds = 0.25;
+
+sub await_flock ($$) {
+  my ($fh, $locktype) = @_;
+
+  my ($res, $tries);
+  while(
+    ! ( $res = flock( $fh, $locktype | LOCK_NB ) )
+      and
+    ++$tries <= $lock_timeout_minutes * 60 / $wait_step_seconds
+  ) {
+    select( undef, undef, undef, $wait_step_seconds );
 
-sub local_umask {
+    # "say something" every 10 cycles to work around RT#108390
+    # jesus christ our tooling is such a crock of shit :(
+    print "#\n" if not $tries % 10;
+  }
+
+  return $res;
+}
+
+
+sub local_umask ($) {
   return unless defined $Config{d_umask};
 
-  die 'Calling local_umask() in void context makes no sense'
+  croak 'Calling local_umask() in void context makes no sense'
     if ! defined wantarray;
 
-  my $old_umask = umask(shift());
+  my $old_umask = umask($_[0]);
   die "Setting umask failed: $!" unless defined $old_umask;
 
-  return bless \$old_umask, 'DBICTest::Util::UmaskGuard';
-}
-{
-  package DBICTest::Util::UmaskGuard;
-  sub DESTROY {
-    local ($@, $!);
-    eval { defined (umask ${$_[0]}) or die };
-    warn ( "Unable to reset old umask ${$_[0]}: " . ($!||'Unknown error') )
-      if ($@ || $!);
-  }
+  scope_guard(sub {
+    local ($@, $!, $?);
+
+    eval {
+      defined(umask $old_umask) or die "nope";
+      1;
+    } or cluck (
+      "Unable to reset old umask '$old_umask': " . ($! || 'Unknown error')
+    );
+  });
 }
 
 sub stacktrace {
@@ -34,7 +117,7 @@ sub stacktrace {
   $frame++;
   my (@stack, @frame);
 
-  while (@frame = caller($frame++)) {
+  while (@frame = CORE::caller($frame++)) {
     push @stack, [@frame[3,1,2]];
   }
 
@@ -44,4 +127,80 @@ sub stacktrace {
   return join "\tinvoked as ", map { sprintf ("%s at %s line %d\n", @$_ ) } @stack;
 }
 
+sub check_customcond_args ($) {
+  my $args = shift;
+
+  confess "Expecting a hashref"
+    unless ref $args eq 'HASH';
+
+  for (qw(rel_name foreign_relname self_alias foreign_alias)) {
+    confess "Custom condition argument '$_' must be a plain string"
+      if length ref $args->{$_} or ! length $args->{$_};
+  }
+
+  confess "Current and legacy rel_name arguments do not match"
+    if $args->{rel_name} ne $args->{foreign_relname};
+
+  confess "Custom condition argument 'self_resultsource' must be a rsrc instance"
+    unless defined blessed $args->{self_resultsource} and $args->{self_resultsource}->isa('DBIx::Class::ResultSource');
+
+  confess "Passed resultsource has no record of the supplied rel_name - likely wrong \$rsrc"
+    unless ref $args->{self_resultsource}->relationship_info($args->{rel_name});
+
+  my $struct_cnt = 0;
+
+  if (defined $args->{self_result_object} or defined $args->{self_rowobj} ) {
+    $struct_cnt++;
+    for (qw(self_result_object self_rowobj)) {
+      confess "Custom condition argument '$_' must be a result instance"
+        unless defined blessed $args->{$_} and $args->{$_}->isa('DBIx::Class::Row');
+    }
+
+    confess "Current and legacy self_result_object arguments do not match"
+      if refaddr($args->{self_result_object}) != refaddr($args->{self_rowobj});
+  }
+
+  if (defined $args->{foreign_values}) {
+    $struct_cnt++;
+
+    confess "Custom condition argument 'foreign_values' must be a hash reference"
+      unless ref $args->{foreign_values} eq 'HASH';
+  }
+
+  confess "Data structures supplied on both ends of a relationship"
+    if $struct_cnt == 2;
+
+  $args;
+}
+
+sub visit_namespaces {
+  my $args = { (ref $_[0]) ? %{$_[0]} : @_ };
+
+  my $visited_count = 1;
+
+  # A package and a namespace are subtly different things
+  $args->{package} ||= 'main';
+  $args->{package} = 'main' if $args->{package} =~ /^ :: (?: main )? $/x;
+  $args->{package} =~ s/^:://;
+
+  if ( $args->{action}->($args->{package}) ) {
+    my $ns =
+      ( ($args->{package} eq 'main') ? '' :  $args->{package} )
+        .
+      '::'
+    ;
+
+    $visited_count += visit_namespaces( %$args, package => $_ ) for
+      grep
+        # this happens sometimes on %:: traversal
+        { $_ ne '::main' }
+        map
+          { $_ =~ /^(.+?)::$/ ? "$ns$1" : () }
+          do { no strict 'refs'; keys %$ns }
+    ;
+  }
+
+  return $visited_count;
+}
+
 1;
index 48ec21d..b1de109 100644 (file)
@@ -5,13 +5,12 @@ use strict;
 
 use Carp;
 use Scalar::Util qw(isweak weaken blessed reftype);
-use DBIx::Class::_Util qw(refcount hrefaddr);
+use DBIx::Class::_Util qw(refcount hrefaddr refdesc);
 use DBIx::Class::Optional::Dependencies;
 use Data::Dumper::Concise;
-use DBICTest::Util 'stacktrace';
+use DBICTest::Util qw( stacktrace visit_namespaces );
 use constant {
-  CV_TRACING => DBIx::Class::Optional::Dependencies->req_ok_for ('test_leaks_heavy'),
-  SKIP_SCALAR_REFS => ( $] > 5.017 ) ? 1 : 0,
+  CV_TRACING => !DBICTest::RunMode->is_plain && DBIx::Class::Optional::Dependencies->req_ok_for ('test_leaks_heavy'),
 };
 
 use base 'Exporter';
@@ -21,15 +20,6 @@ my $refs_traced = 0;
 my $leaks_found = 0;
 my %reg_of_regs;
 
-# so we don't trigger stringification
-sub _describe_ref {
-  sprintf '%s%s(%s)',
-    (defined blessed $_[0]) ? blessed($_[0]) . '=' : '',
-    reftype $_[0],
-    hrefaddr $_[0],
-  ;
-}
-
 sub populate_weakregistry {
   my ($weak_registry, $target, $note) = @_;
 
@@ -52,20 +42,20 @@ sub populate_weakregistry {
       for keys %$reg;
   }
 
-  # FIXME/INVESTIGATE - something fishy is going on with refs to plain
-  # strings, perhaps something to do with the CoW work etc...
-  return $target if SKIP_SCALAR_REFS and reftype($target) eq 'SCALAR';
-
   if (! defined $weak_registry->{$refaddr}{weakref}) {
     $weak_registry->{$refaddr} = {
       stacktrace => stacktrace(1),
       weakref => $target,
     };
-    weaken( $weak_registry->{$refaddr}{weakref} );
-    $refs_traced++;
+
+    # on perl < 5.8.3 sometimes a weaken can throw (can't find RT)
+    # so guard against that unlikely event
+    local $@;
+    eval { weaken( $weak_registry->{$refaddr}{weakref} ); $refs_traced++ }
+      or delete $weak_registry->{$refaddr};
   }
 
-  my $desc = _describe_ref($target);
+  my $desc = refdesc $target;
   $weak_registry->{$refaddr}{slot_names}{$desc} = 1;
   if ($note) {
     $note =~ s/\s*\Q$desc\E\s*//g;
@@ -100,6 +90,11 @@ sub CLONE {
       $reg->{$new_addr} = $slot_info;
     }
   }
+
+  # Dummy NEXTSTATE ensuring the all temporaries on the stack are garbage
+  # collected before leaving this scope. Depending on the code above, this
+  # may very well be just a preventive measure guarding future modifications
+  undef;
 }
 
 sub visit_refs {
@@ -150,46 +145,20 @@ sub visit_refs {
       elsif (CV_TRACING and $type eq 'CODE') {
         $visited_cnt += visit_refs({ %$args, refs => [ map {
           ( !isweak($_) ) ? $_ : ()
-        } scalar PadWalker::closed_over($r) ] }); # scalar due to RT#92269
+        } values %{ scalar PadWalker::closed_over($r) } ] }); # scalar due to RT#92269
       }
       1;
-    } or warn "Could not descend into @{[ _describe_ref($r) ]}: $@\n";
+    } or warn "Could not descend into @{[ refdesc $r ]}: $@\n";
   }
   $visited_cnt;
 }
 
-sub visit_namespaces {
-  my $args = { (ref $_[0]) ? %{$_[0]} : @_ };
-
-  my $visited = 1;
-
-  $args->{package} ||= '::';
-  $args->{package} = '::' if $args->{package} eq 'main';
-
-  if ( $args->{action}->($args->{package}) ) {
-
-    my $base = $args->{package};
-    $base = '' if $base eq '::';
-
-
-    $visited += visit_namespaces({ %$args, package => $_ }) for map
-      { $_ =~ /(.+?)::$/ && "${base}::$1" }
-      grep
-        { $_ =~ /(?<!^main)::$/ }
-        do {  no strict 'refs'; keys %{ $base . '::'} }
-  }
-
-  return $visited;
-}
-
 # compiles a list of addresses stored as globals (possibly even catching
 # class data in the form of method closures), so we can skip them further on
 sub symtable_referenced_addresses {
 
   my $refs_per_pkg;
 
-  my $dummy_addresslist;
-
   my $seen_refs = {};
   visit_namespaces(
     action => sub {
@@ -197,41 +166,32 @@ sub symtable_referenced_addresses {
       no strict 'refs';
 
       my $pkg = shift;
-      $pkg = '' if $pkg eq '::';
-      $pkg .= '::';
 
       # the unless regex at the end skips some dangerous namespaces outright
       # (but does not prevent descent)
       $refs_per_pkg->{$pkg} += visit_refs (
         seen_refs => $seen_refs,
 
-        # FIXME FIXME FIXME
-        # This is so damn odd - if we feed a constsub {1} (or in fact almost
-        # anything other than the actionsub below, any scalarref will show
-        # up as a leak, trapped by... something...
-        # Ideally we should be able to const this to sub{1} and just return
-        # $seen_refs (in fact it is identical to the dummy list at the end of
-        # a run here). Alas this doesn't seem to work, so punt for now...
-        action => sub { ++$dummy_addresslist->{ hrefaddr $_[0] } },
+        action => sub { 1 },
 
         refs => [ map { my $sym = $_;
-          # *{"$pkg$sym"}{CODE} won't simply work - MRO-cached CVs are invisible there
-          ( CV_TRACING ? Class::MethodCache::get_cv("${pkg}$sym") : () ),
+          # *{"${pkg}::$sym"}{CODE} won't simply work - MRO-cached CVs are invisible there
+          ( CV_TRACING ? Class::MethodCache::get_cv("${pkg}::$sym") : () ),
 
-          ( defined *{"$pkg$sym"}{SCALAR} and length ref ${"$pkg$sym"} and ! isweak( ${"$pkg$sym"} ) )
-            ? ${"$pkg$sym"} : ()
+          ( defined *{"${pkg}::$sym"}{SCALAR} and length ref ${"${pkg}::$sym"} and ! isweak( ${"${pkg}::$sym"} ) )
+            ? ${"${pkg}::$sym"} : ()
           ,
 
           ( map {
-            ( defined *{"$pkg$sym"}{$_} and ! isweak(defined *{"$pkg$sym"}{$_}) )
-              ? *{"$pkg$sym"}{$_}
+            ( defined *{"${pkg}::$sym"}{$_} and ! isweak(defined *{"${pkg}::$sym"}{$_}) )
+              ? *{"${pkg}::$sym"}{$_}
               : ()
           } qw(HASH ARRAY IO GLOB) ),
 
-        } keys %$pkg ],
-      ) unless $pkg =~ /^ :: (?:
+        } keys %{"${pkg}::"} ],
+      ) unless $pkg =~ /^ (?:
         DB | next | B | .+? ::::ISA (?: ::CACHE ) | Class::C3
-      ) :: $/x;
+      ) $/x;
     }
   );
 
@@ -252,7 +212,7 @@ sub assert_empty_weakregistry {
   # in case we hooked bless any extra object creation will wreak
   # havoc during the assert phase
   local *CORE::GLOBAL::bless;
-  *CORE::GLOBAL::bless = sub { CORE::bless( $_[0], (@_ > 1) ? $_[1] : caller() ) };
+  *CORE::GLOBAL::bless = sub { CORE::bless( $_[0], (@_ > 1) ? $_[1] : CORE::caller() ) };
 
   croak 'Expecting a registry hashref' unless ref $weak_registry eq 'HASH';
 
@@ -275,19 +235,32 @@ sub assert_empty_weakregistry {
       if defined $weak_registry->{$addr}{weakref} and ! isweak( $weak_registry->{$addr}{weakref} );
   }
 
-  # the walk is very expensive - if we are $quiet (running in an END block)
-  # we do not really need to be too thorough
-  unless ($quiet) {
-    delete $weak_registry->{$_} for keys %{ symtable_referenced_addresses() };
-  }
-
+  # the symtable walk is very expensive
+  # if we are $quiet (running in an END block) we do not really need to be
+  # that thorough - can get by with only %Sub::Quote::QUOTED
+  delete $weak_registry->{$_} for $quiet
+    ? do {
+      my $refs = {};
+      visit_refs (
+        # only look at the closed over stuffs
+        refs => [ grep { length ref $_ } map { values %{$_->[2]} } grep { ref $_ eq 'ARRAY' } values %Sub::Quote::QUOTED ],
+        seen_refs => $refs,
+        action => sub { 1 },
+      );
+      keys %$refs;
+    }
+    : (
+      # full sumtable walk, starting from ::
+      keys %{ symtable_referenced_addresses() }
+    )
+  ;
 
   for my $addr (sort { $weak_registry->{$a}{display_name} cmp $weak_registry->{$b}{display_name} } keys %$weak_registry) {
 
     next if ! defined $weak_registry->{$addr}{weakref};
 
     $leaks_found++ unless $tb->in_todo;
-    $tb->ok (0, "Leaked $weak_registry->{$addr}{display_name}");
+    $tb->ok (0, "Expected garbage collection of $weak_registry->{$addr}{display_name}");
 
     my $diag = do {
       local $Data::Dumper::Maxdepth = 1;
@@ -325,20 +298,27 @@ sub assert_empty_weakregistry {
 #      Devel::MAT::Dumper::dumpfh( $fh );
 #      close ($fh) or die $!;
 #
-#      use POSIX;
+#      require POSIX;
 #      POSIX::_exit(1);
 #    }
   }
 
   if (! $quiet and !$leaks_found and ! $tb->in_todo) {
-    $tb->ok(1, sprintf "No leaks found at %s line %d", (caller())[1,2] );
+    $tb->ok(1, sprintf "No leaks found at %s line %d", (CORE::caller())[1,2] );
   }
 }
 
 END {
-  if ($INC{'Test/Builder.pm'}) {
-    my $tb = Test::Builder->new;
-
+  if (
+    $INC{'Test/Builder.pm'}
+      and
+    my $tb = do {
+      local $@;
+      my $t = eval { Test::Builder->new }
+        or warn "Test::Builder->new failed:\n$@\n";
+      $t;
+    }
+  ) {
     # we check for test passage - a leak may be a part of a TODO
     if ($leaks_found and !$tb->is_passing) {
 
@@ -352,6 +332,24 @@ END {
     else {
       $tb->note("Auto checked $refs_traced references for leaks - none detected");
     }
+
+    # also while we are here and not in plain runmode: make sure we never
+    # loaded any of the strictures XS bullshit (it's a leak in a sense)
+    unless (
+      $ENV{MOO_FATAL_WARNINGS}
+        or
+      # FIXME - SQLT loads strictures explicitly, /facedesk
+      # remove this INC check when 0fb58589 and 45287c815 are rectified
+      $INC{'SQL/Translator.pm'}
+        or
+      DBICTest::RunMode->is_plain
+    ) {
+      for (qw(indirect multidimensional bareword::filehandles)) {
+        exists $INC{ Module::Runtime::module_notional_filename($_) }
+          and
+        $tb->ok(0, "$_ load should not have been attempted!!!" )
+      }
+    }
   }
 }
 
diff --git a/t/lib/PrefetchBug.pm b/t/lib/PrefetchBug.pm
deleted file mode 100644 (file)
index 278bf5b..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-package
-    PrefetchBug;
-
-use strict;
-use warnings;
-
-use base qw/DBIx::Class::Schema/;
-
-__PACKAGE__->load_classes();
-
-1;
index 64ddc33..67c2028 100644 (file)
@@ -370,4 +370,4 @@ CREATE INDEX "fourkeys_to_twokeys_idx_f_foo_f_bar_f_hello_f_goodbye" ON "fourkey
 CREATE INDEX "fourkeys_to_twokeys_idx_t_artist_t_cd" ON "fourkeys_to_twokeys" ("t_artist", "t_cd");
 
 CREATE VIEW "year2000cds" AS
-    SELECT cdid, artist, title, year, genreid, single_track FROM cd WHERE year = "2000";
+    SELECT cdid, artist, title, year, genreid, single_track FROM cd WHERE year = '2000';
diff --git a/t/multi_create/find_or_multicreate.t b/t/multi_create/find_or_multicreate.t
new file mode 100644 (file)
index 0000000..762b962
--- /dev/null
@@ -0,0 +1,71 @@
+use strict;
+use warnings;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest;
+
+my $schema = DBICTest->init_schema( no_populate => 1 );
+
+my $t11 = $schema->resultset('Track')->find_or_create({
+  trackid => 1,
+  title => 'Track one cd one',
+  cd => {
+    year => 1,
+    title => 'CD one',
+    very_long_artist_relationship => {
+      name => 'Artist one',
+    }
+  }
+});
+
+my $t12 = $schema->resultset('Track')->find_or_create({
+  trackid => 2,
+  title => 'Track two cd one',
+  cd => {
+    title => 'CD one',
+    very_long_artist_relationship => {
+      name => 'Artist one',
+    }
+  }
+});
+
+# FIXME - MC should be smart enough to infer this on its own...
+$schema->resultset('Artist')->create({ name => 'Artist two' });
+
+my $t2 = $schema->resultset('Track')->find_or_create({
+  trackid => 3,
+  title => 'Track one cd one',
+  cd => {
+    year => 1,
+    title => 'CD one',
+    very_long_artist_relationship => {
+      name => 'Artist two',
+    }
+  }
+});
+
+is_deeply(
+  $schema->resultset('Artist')->search({}, {
+    prefetch => { cds => 'tracks' },
+    order_by => 'tracks.title',
+  })->all_hri,
+  [
+    { artistid => 1, charfield => undef, name => "Artist one", rank => 13, cds => [
+      { artist => 1, cdid => 1, genreid => undef, single_track => undef, title => "CD one", year => 1, tracks => [
+        { cd => 1, last_updated_at => undef, last_updated_on => undef, position => 1, title => "Track one cd one", trackid => 1 },
+        { cd => 1, last_updated_at => undef, last_updated_on => undef, position => 2, title => "Track two cd one", trackid => 2 },
+      ]},
+    ]},
+    { artistid => 2, charfield => undef, name => "Artist two", rank => 13, cds => [
+      { artist => 2, cdid => 2, genreid => undef, single_track => undef, title => "CD one", year => 1, tracks => [
+        { cd => 2, last_updated_at => undef, last_updated_on => undef, position => 1, title => "Track one cd one", trackid => 3 },
+      ]},
+    ]},
+  ],
+  'Expected state of database after several find_or_create rounds'
+);
+
+
+done_testing;
+
index ef5c9a8..9a5adbe 100644 (file)
@@ -59,7 +59,7 @@ for my $type (qw/has_one might_have/) {
   my $cd_title = "Test $type cd";
   my $artist_names = [ map { "Artist via $type $_" } (1, 2) ];
 
-  my $someartist = $artist_rs->next;
+  my $someartist = $artist_rs->search({}, { rows => 1 })->next;
 
   lives_ok (sub {
     my $cd = $schema->resultset('CD')->create ({
index 6c1efd8..54cf04e 100644 (file)
@@ -3,11 +3,10 @@ use warnings;
 
 use Test::More;
 use Test::Exception;
+use Test::Warn;
 use lib qw(t/lib);
 use DBICTest;
 
-plan tests => 91;
-
 my $schema = DBICTest->init_schema();
 
 lives_ok ( sub {
@@ -403,8 +402,11 @@ lives_ok ( sub {
 
   $kurt_cobain->{cds} = [ $in_utero ];
 
+  warnings_exist {
+    $schema->resultset('Artist')->populate([ $kurt_cobain ]);
+  }  qr/\QFast-path populate() with supplied related objects is not possible/;
+
 
-  $schema->resultset('Artist')->populate([ $kurt_cobain ]); # %)
   my $artist = $schema->resultset('Artist')->find({name => 'Kurt Cobain'});
 
   is($artist->name, 'Kurt Cobain', 'Artist insertion ok');
@@ -468,4 +470,4 @@ lives_ok ( sub {
   is ($m2m_cd->first->producers->first->name, 'Cowboy Neal', 'Correct producer row created');
 }, 'Test multi create over many_to_many');
 
-1;
+done_testing;
index 9cbc3da..dc08306 100644 (file)
@@ -9,19 +9,22 @@ use DBICTest;
 my $schema = DBICTest->init_schema();
 
 my $cd = $schema->resultset('CD')->next;
+$cd->tracks->delete;
 
-lives_ok {
-  $cd->tracks->delete;
+$schema->resultset('CD')->related_resultset('tracks')->delete;
 
-  my @tracks = map
-    { $cd->create_related('tracks', { title => "t_$_", position => $_ }) }
-    (4,2,5,1,3)
-  ;
+is $cd->tracks->count, 0, 'No tracks';
 
-  for (@tracks) {
-    $_->discard_changes;
-    $_->delete;
-  }
-} 'Creation/deletion of out-of order tracks successful';
+$cd->create_related('tracks', { title => "t_$_", position => $_ })
+  for (4,2,3,1,5);
+
+is $cd->tracks->count, 5, 'Created 5 tracks';
+
+# a txn should force the implicit pos reload, regardless of order
+$schema->txn_do(sub {
+  $cd->tracks->delete_all
+});
+
+is $cd->tracks->count, 0, 'Successfully deleted everything';
 
 done_testing;
index 6452a94..5196620 100644 (file)
@@ -4,18 +4,16 @@ use warnings;
 use Test::More;
 use Test::Deep;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
-my $orig_debug = $schema->storage->debug;
 
 my $cdrs = $schema->resultset('CD')->search({ 'me.artist' => { '!=', 2 }});
 
 my $cd_data = { map {
   $_->cdid => {
     siblings => $cdrs->search ({ artist => $_->get_column('artist') })->count - 1,
-    track_titles => [ map { $_->title } ($_->tracks->all) ],
+    track_titles => [ sort $_->tracks->get_column('title')->all ],
   },
 } ( $cdrs->all ) };
 
@@ -36,10 +34,10 @@ is_same_sql_bind(
     SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track,
            (SELECT COUNT( * )
               FROM cd siblings
-            WHERE siblings.artist = me.artist
+            WHERE me.artist != ?
+              AND siblings.artist = me.artist
               AND siblings.cdid != me.cdid
               AND siblings.cdid != ?
-              AND me.artist != ?
            ),
            tracks.trackid, tracks.cd, tracks.position, tracks.title, tracks.last_updated_on, tracks.last_updated_at
       FROM cd me
@@ -50,12 +48,12 @@ is_same_sql_bind(
   [
 
     # subselect
-    [ { sqlt_datatype => 'integer', dbic_colname => 'siblings.cdid' }
-      => 23414 ],
-
     [ { sqlt_datatype => 'integer', dbic_colname => 'me.artist' }
       => 2 ],
 
+    [ { sqlt_datatype => 'integer', dbic_colname => 'siblings.cdid' }
+      => 23414 ],
+
     # outher WHERE
     [ { sqlt_datatype => 'integer', dbic_colname => 'me.artist' }
       => 2 ],
@@ -63,26 +61,19 @@ is_same_sql_bind(
   'Expected SQL on correlated realiased subquery'
 );
 
-my $queries = 0;
-$schema->storage->debugcb(sub { $queries++; });
-$schema->storage->debug(1);
-
-cmp_deeply (
-  { map
-    { $_->cdid => {
-      track_titles => [ map { $_->title } ($_->tracks->all) ],
-      siblings => $_->get_column ('sibling_count'),
-    } }
-    $c_rs->all
-  },
-  $cd_data,
-  'Proper information retrieved from correlated subquery'
-);
-
-is ($queries, 1, 'Only 1 query fired to retrieve everything');
-
-$schema->storage->debug($orig_debug);
-$schema->storage->debugcb(undef);
+$schema->is_executed_querycount( sub {
+  cmp_deeply (
+    { map
+      { $_->cdid => {
+        track_titles => [ sort map { $_->title } ($_->tracks->all) ],
+        siblings => $_->get_column ('sibling_count'),
+      } }
+      $c_rs->all
+    },
+    $cd_data,
+    'Proper information retrieved from correlated subquery'
+  );
+}, 1, 'Only 1 query fired to retrieve everything');
 
 # now add an unbalanced select/as pair
 $c_rs = $c_rs->search ({}, {
@@ -102,15 +93,15 @@ is_same_sql_bind(
     SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track,
            (SELECT COUNT( * )
               FROM cd siblings
-            WHERE siblings.artist = me.artist
+            WHERE me.artist != ?
+              AND siblings.artist = me.artist
               AND siblings.cdid != me.cdid
               AND siblings.cdid != ?
-              AND me.artist != ?
            ),
            (SELECT MIN( year ), MAX( year )
               FROM cd siblings
-            WHERE siblings.artist = me.artist
-              AND me.artist != ?
+            WHERE me.artist != ?
+              AND siblings.artist = me.artist
            ),
            tracks.trackid, tracks.cd, tracks.position, tracks.title, tracks.last_updated_on, tracks.last_updated_at
       FROM cd me
@@ -121,12 +112,12 @@ is_same_sql_bind(
   [
 
     # first subselect
-    [ { sqlt_datatype => 'integer', dbic_colname => 'siblings.cdid' }
-      => 23414 ],
-
     [ { sqlt_datatype => 'integer', dbic_colname => 'me.artist' }
       => 2 ],
 
+    [ { sqlt_datatype => 'integer', dbic_colname => 'siblings.cdid' }
+      => 23414 ],
+
     # second subselect
     [ { sqlt_datatype => 'integer', dbic_colname => 'me.artist' }
       => 2 ],
@@ -138,6 +129,8 @@ is_same_sql_bind(
   'Expected SQL on correlated realiased subquery'
 );
 
+$schema->storage->disconnect;
+
 # test for subselect identifier leakage
 # NOTE - the hodge-podge mix of literal and regular identifuers is *deliberate*
 for my $quote_names (0,1) {
index 4311e80..f973575 100644 (file)
@@ -3,10 +3,7 @@ use warnings;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
-
-plan tests => 23;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
@@ -15,7 +12,6 @@ my $cd_rs = $schema->resultset('CD')->search (
   { prefetch => ['tracks', 'artist'] },
 );
 
-
 is($cd_rs->count, 5, 'CDs with tracks count');
 is($cd_rs->search_related('tracks')->count, 15, 'Tracks associated with CDs count (before SELECT()ing)');
 
@@ -77,26 +73,23 @@ is_same_sql_bind (
       => 4 ] ],
 );
 
-
 {
   local $TODO = "Chaining with prefetch is fundamentally broken";
+  $schema->is_executed_querycount( sub {
 
-  my $queries;
-  $schema->storage->debugcb ( sub { $queries++ } );
-  $schema->storage->debug (1);
-
-  my $cds = $cd2->search_related ('artist', {}, { prefetch => { cds => 'tracks' }, join => 'twokeys' })
+    my $cds = $cd2->search_related ('artist', {}, { prefetch => { cds => 'tracks' }, join => 'twokeys' })
                   ->search_related ('cds');
 
-  my $tracks = $cds->search_related ('tracks');
-
-  is($tracks->count, 2, "2 Tracks counted on cd via artist via one of the cds");
-  is(scalar($tracks->all), 2, "2 Tracks prefetched on cd via artist via one of the cds");
-  is($tracks->count, 2, "Cached 2 Tracks counted on cd via artist via one of the cds");
+    my $tracks = $cds->search_related ('tracks');
 
-  is($cds->count, 2, "2 CDs counted on artist via one of the cds");
-  is(scalar($cds->all), 2, "2 CDs prefetched on artist via one of the cds");
-  is($cds->count, 2, "Cached 2 CDs counted on artist via one of the cds");
+    is($tracks->count, 2, "2 Tracks counted on cd via artist via one of the cds");
+    is(scalar($tracks->all), 2, "2 Tracks prefetched on cd via artist via one of the cds");
+    is($tracks->count, 2, "Cached 2 Tracks counted on cd via artist via one of the cds");
 
-  is ($queries, 3, '2 counts + 1 prefetch?');
+    is($cds->count, 2, "2 CDs counted on artist via one of the cds");
+    is(scalar($cds->all), 2, "2 CDs prefetched on artist via one of the cds");
+    is($cds->count, 2, "Cached 2 CDs counted on artist via one of the cds");
+  }, 3, '2 counts + 1 prefetch?' );
 }
+
+done_testing;
index d4b50aa..fa0b79f 100644 (file)
@@ -3,8 +3,7 @@ use strict;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
index 5213e73..468a27a 100644 (file)
@@ -15,29 +15,20 @@ $schema->resultset('CD')->create({
   },
 });
 
-my $orig_debug = $schema->storage->debug;
-
-my $queries = 0;
-$schema->storage->debugcb(sub { $queries++; });
-$schema->storage->debug(1);
-
-my $cd = $schema->resultset('CD')->search( {}, { prefetch => 'artist' })->next;
-
-cmp_deeply
-  { $cd->get_columns },
-  { artist => 0, cdid => 0, genreid => 0, single_track => 0, title => '', year => 0 },
-  'Expected CD columns present',
-;
-
-cmp_deeply
-  { $cd->artist->get_columns },
-  { artistid => 0, charfield => 0, name => "", rank => 0 },
-  'Expected Artist columns present',
-;
-
-is $queries, 1, 'Only one query fired - prefetch worked';
-
-$schema->storage->debugcb(undef);
-$schema->storage->debug($orig_debug);
+$schema->is_executed_querycount( sub {
+  my $cd = $schema->resultset('CD')->search( {}, { prefetch => 'artist' })->next;
+
+  cmp_deeply
+    { $cd->get_columns },
+    { artist => 0, cdid => 0, genreid => 0, single_track => 0, title => '', year => 0 },
+    'Expected CD columns present',
+  ;
+
+  cmp_deeply
+    { $cd->artist->get_columns },
+    { artistid => 0, charfield => 0, name => "", rank => 0 },
+    'Expected Artist columns present',
+  ;
+}, 1, 'Only one query fired - prefetch worked' );
 
 done_testing;
index 0eed0a7..0f6f59a 100644 (file)
@@ -4,15 +4,13 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 
 my $ROWS = DBIx::Class::SQLMaker::LimitDialects->__rows_bindtype;
 my $OFFSET = DBIx::Class::SQLMaker::LimitDialects->__offset_bindtype;
 
 my $schema = DBICTest->init_schema();
-my $sdebug = $schema->storage->debug;
 
 my $cd_rs = $schema->resultset('CD')->search (
   { 'tracks.cd' => { '!=', undef } },
@@ -25,10 +23,12 @@ for ($cd_rs->all) {
   is ($_->tracks->count, 3, '3 tracks for CD' . $_->id );
 }
 
+my @cdids = sort $cd_rs->get_column ('cdid')->all;
+
 # Test a belongs_to prefetch of a has_many
 {
   my $track_rs = $schema->resultset ('Track')->search (
-    { 'me.cd' => { -in => [ $cd_rs->get_column ('cdid')->all ] } },
+    { 'me.cd' => { -in => \@cdids } },
     {
       select => [
         'me.cd',
@@ -49,21 +49,13 @@ for ($cd_rs->all) {
   is($track_rs->count, 5, 'Prefetched count with groupby');
   is($track_rs->all, 5, 'Prefetched objects with groupby');
 
-  {
-    my $query_cnt = 0;
-    $schema->storage->debugcb ( sub { $query_cnt++ } );
-    $schema->storage->debug (1);
-
+  $schema->is_executed_querycount( sub {
     while (my $collapsed_track = $track_rs->next) {
       my $cdid = $collapsed_track->get_column('cd');
       is($collapsed_track->get_column('track_count'), 3, "Correct count of tracks for CD $cdid" );
       ok($collapsed_track->cd->title, "Prefetched title for CD $cdid" );
     }
-
-    is ($query_cnt, 1, 'Single query on prefetched titles');
-    $schema->storage->debugcb (undef);
-    $schema->storage->debug ($sdebug);
-  }
+  }, 1, 'Single query on prefetched titles');
 
   # Test sql by hand, as the sqlite db will simply paper over
   # improper group/select combinations
@@ -82,7 +74,7 @@ for ($cd_rs->all) {
       me
     )',
     [ map { [ { sqlt_datatype => 'integer', dbic_colname => 'me.cd' }
-      => $_ ] } ($cd_rs->get_column ('cdid')->all) ],
+      => $_ ] } @cdids ],
     'count() query generated expected SQL',
   );
 
@@ -101,7 +93,7 @@ for ($cd_rs->all) {
       WHERE ( me.cd IN ( ?, ?, ?, ?, ? ) )
     )',
     [ map { [ { sqlt_datatype => 'integer', dbic_colname => 'me.cd' }
-      => $_ ] } ( ($cd_rs->get_column ('cdid')->all) x 2 ) ],
+      => $_ ] } (@cdids) x 2 ],
     'next() query generated expected SQL',
   );
 
@@ -190,22 +182,16 @@ for ($cd_rs->all) {
   my ($top_cd) = $most_tracks_rs->all;
   is ($top_cd->id, 2, 'Correct cd fetched on top'); # 2 because of the slice(1,1) earlier
 
-  my $query_cnt = 0;
-  $schema->storage->debugcb ( sub { $query_cnt++ } );
-  $schema->storage->debug (1);
-
-  is ($top_cd->get_column ('track_count'), 4, 'Track count fetched correctly');
-  is ($top_cd->tracks->count, 4, 'Count of prefetched tracks rs still correct');
-  is ($top_cd->tracks->all, 4, 'Number of prefetched track objects still correct');
-  is (
-    $top_cd->liner_notes->notes,
-    'Buy Whiskey!',
-    'Correct liner pre-fetched with top cd',
-  );
-
-  is ($query_cnt, 0, 'No queries executed during prefetched data access');
-  $schema->storage->debugcb (undef);
-  $schema->storage->debug ($sdebug);
+  $schema->is_executed_querycount( sub {
+    is ($top_cd->get_column ('track_count'), 4, 'Track count fetched correctly');
+    is ($top_cd->tracks->count, 4, 'Count of prefetched tracks rs still correct');
+    is ($top_cd->tracks->all, 4, 'Number of prefetched track objects still correct');
+    is (
+      $top_cd->liner_notes->notes,
+      'Buy Whiskey!',
+      'Correct liner pre-fetched with top cd',
+    );
+  }, 0, 'No queries executed during prefetched data access');
 }
 
 {
@@ -256,20 +242,14 @@ for ($cd_rs->all) {
   my ($top_cd) = $most_tracks_rs->all;
   is ($top_cd->id, 2, 'Correct cd fetched on top'); # 2 because of the slice(1,1) earlier
 
-  my $query_cnt = 0;
-  $schema->storage->debugcb ( sub { $query_cnt++ } );
-  $schema->storage->debug (1);
-
-  is ($top_cd->get_column ('track_count'), 4, 'Track count fetched correctly');
-  is (
-    $top_cd->liner_notes->notes,
-    'Buy Whiskey!',
-    'Correct liner pre-fetched with top cd',
-  );
-
-  is ($query_cnt, 0, 'No queries executed during prefetched data access');
-  $schema->storage->debugcb (undef);
-  $schema->storage->debug ($sdebug);
+  $schema->is_executed_querycount( sub {
+    is ($top_cd->get_column ('track_count'), 4, 'Track count fetched correctly');
+    is (
+      $top_cd->liner_notes->notes,
+      'Buy Whiskey!',
+      'Correct liner pre-fetched with top cd',
+    );
+  }, 0, 'No queries executed during prefetched data access');
 }
 
 
@@ -305,7 +285,7 @@ for ($cd_rs->all) {
 # RT 47779, test group_by as a scalar ref
 {
   my $track_rs = $schema->resultset ('Track')->search (
-    { 'me.cd' => { -in => [ $cd_rs->get_column ('cdid')->all ] } },
+    { 'me.cd' => { -in => \@cdids } },
     {
       select => [
         'me.cd',
@@ -334,7 +314,7 @@ for ($cd_rs->all) {
       me
     )',
     [ map { [ { sqlt_datatype => 'integer', dbic_colname => 'me.cd' }
-      => $_ ] } ($cd_rs->get_column ('cdid')->all) ],
+      => $_ ] } (@cdids) ],
     'count() query generated expected SQL',
   );
 }
index 09df99c..63e431a 100644 (file)
@@ -5,8 +5,7 @@ use Test::More;
 use Test::Deep;
 use Test::Exception;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
index ac839f0..f2980e7 100644 (file)
@@ -3,8 +3,7 @@ use strict;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
index 2228142..e051ce3 100644 (file)
@@ -257,24 +257,20 @@ if ($ENV{TEST_VERBOSE}) {
     for @lines;
 }
 
-{
-  my $queries = 0;
-  $schema->storage->debugcb(sub { $queries++ });
-  my $orig_debug = $schema->storage->debug;
-  $schema->storage->debug (1);
-
+$schema->is_executed_querycount( sub {
   for my $use_next (0, 1) {
     my @random_cds;
+    my $rs_r = $rs_random;
     if ($use_next) {
       warnings_exist {
-        while (my $o = $rs_random->next) {
+        while (my $o = $rs_r->next) {
           push @random_cds, $o;
         }
       } qr/performed an eager cursor slurp underneath/,
       'Warned on auto-eager cursor';
     }
     else {
-      @random_cds = $rs_random->all;
+      @random_cds = $rs_r->all;
     }
 
     is (@random_cds, 6, 'object count matches');
@@ -306,11 +302,7 @@ if ($ENV{TEST_VERBOSE}) {
       }
     }
   }
-
-  $schema->storage->debugcb(undef);
-  $schema->storage->debug($orig_debug);
-  is ($queries, 2, "Only two queries for two prefetch calls total");
-}
+}, 2, "Only two queries for two prefetch calls total");
 
 # can't cmp_deeply a random set - need *some* order
 my $ord_rs = $rs->search({}, {
index 31b2585..665005b 100644 (file)
@@ -2,11 +2,11 @@ use strict;
 use warnings;
 
 use Test::More;
+use Test::Warn;
 use lib qw(t/lib);
 use DBICTest;
 
 my $schema = DBICTest->init_schema();
-my $sdebug = $schema->storage->debug;
 
 #( 1 -> M + M )
 my $cd_rs = $schema->resultset('CD')->search( { 'me.title' => 'Forkful of bees' } );
@@ -15,33 +15,24 @@ 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 );
+$schema->is_executed_querycount( sub {
+  my $pcr = $pr_cd_rs;
+  my $pr_tracks_rs;
 
-my $queries = 0;
-$schema->storage->debugcb( sub { $queries++ } );
-$schema->storage->debug(1);
+  warnings_exist {
+    $pr_tracks_rs = $pcr->first->tracks;
+  } [], 'no warning on attempt to prefetch several same level has_many\'s (1 -> M + M)' ;
 
-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;
+  is( $pr_tracks_rs->count, $tracks_count,
+    'equal count of prefetched relations over several same level has_many\'s (1 -> M + M)'
+  );
 
-ok( !$o_mm_warn,
-'no warning on attempt to prefetch several same level has_many\'s (1 -> M + M)'
-);
+  is( $pr_tracks_rs->all, $tracks_count,
+    'equal amount of objects returned with and without prefetch over several same level has_many\'s (1 -> M + M)'
+  );
 
-is( $queries, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query' );
-$schema->storage->debugcb(undef);
-$schema->storage->debug($sdebug);
+}, 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)'
-);
-is( $pr_tracks_rs->all, $tracks_rs->all,
-'equal amount of objects returned with and without prefetch over several same level has_many\'s (1 -> M + M)'
-);
 
 #( M -> 1 -> M + M )
 my $note_rs =
@@ -52,32 +43,22 @@ my $pr_note_rs =
 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' );
-$schema->storage->debugcb(undef);
-$schema->storage->debug($sdebug);
-
-is( $pr_tags_count, $tags_count,
-'equal count of prefetched relations over several same level has_many\'s (M -> 1 -> M + M)'
-);
-is( $pr_tags_rs->all, $tags_rs->all,
-'equal amount of objects with and without prefetch over several same level has_many\'s (M -> 1 -> M + M)'
-);
+$schema->is_executed_querycount( sub {
+  my $pnr = $pr_note_rs;
+  my $pr_tags_rs;
+
+  warnings_exist {
+    $pr_tags_rs = $pnr->first->cd->tags;
+  } [], 'no warning on attempt to prefetch several same level has_many\'s (M -> 1 -> M + M)';
+
+  is( $pr_tags_rs->count, $tags_count,
+    'equal count of prefetched relations over several same level has_many\'s (M -> 1 -> M + M)'
+  );
+  is( $pr_tags_rs->all, $tags_count,
+    'equal amount of objects with and without prefetch over several same level has_many\'s (M -> 1 -> M + M)'
+  );
+
+}, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query' );
+
 
 done_testing;
index 1623937..d3998e0 100644 (file)
@@ -6,6 +6,7 @@ use Test::Deep;
 use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
+use DBIx::Class::_Util 'sigwarn_silencer';
 
 my $schema = DBICTest->init_schema();
 
@@ -124,4 +125,31 @@ my $art_rs_prefetch = $art_rs->search({}, {
 cmp_deeply( $art_rs_prefetch->next, $artist_with_extras );
 
 
+for my $order (
+  [ [qw( cds.cdid tracks.position )] ],
+
+  [ [qw( artistid tracks.cd tracks.position )],
+    'we need to proxy the knowledge from the collapser that tracks.cd is a stable sorter for CDs' ],
+) {
+
+  my $cds_rs_prefetch = $art_rs->related_resultset('cds')->search({}, {
+    order_by => [ $order->[0], qw(producer.name tracks_2.position) ],
+    result_class => 'DBIx::Class::ResultClass::HashRefInflator',
+    prefetch => [
+      { tracks => { cd_single => 'tracks' } },
+      { cd_to_producer => 'producer' },
+    ],
+  });
+
+  local $SIG{__WARN__} = sigwarn_silencer(qr/Unable to properly collapse has_many results/) if $order->[1];
+
+  cmp_deeply( $cds_rs_prefetch->next, $artist_with_extras->{cds}[0], '1st cd structure matches' );
+  cmp_deeply( $cds_rs_prefetch->next, $artist_with_extras->{cds}[1], '2nd cd structure matches' );
+
+  # INTERNALS! (a.k.a boars, gore and whores) DO NOT CARGOCULT!!!
+  local $TODO = $order->[1] if $order->[1];
+  ok( $cds_rs_prefetch->_resolved_attrs->{_ordered_for_collapse}, 'ordered_for_collapse detected properly' );
+}
+
+
 done_testing;
index b8a4477..65a2c39 100644 (file)
@@ -4,8 +4,7 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 
 my ($ROWS, $OFFSET) = (
index 811942e..f79b38e 100644 (file)
@@ -12,20 +12,14 @@ my $artist = $schema->resultset ('Artist')->find ({artistid => 1});
 is ($artist->cds->count, 3, 'Correct number of CDs');
 is ($artist->cds->search_related ('genre')->count, 1, 'Only one of the cds has a genre');
 
-my $queries = 0;
-my $orig_cb = $schema->storage->debugcb;
-$schema->storage->debugcb(sub { $queries++ });
-$schema->storage->debug(1);
-
-my $pref = $schema->resultset ('Artist')
+$schema->is_executed_querycount( sub {
+  my $pref = $schema->resultset ('Artist')
                      ->search ({ 'me.artistid' => $artist->id }, { prefetch => { cds => 'genre' } })
                       ->next;
 
-is ($pref->cds->count, 3, 'Correct number of CDs prefetched');
-is ($pref->cds->search_related ('genre')->count, 1, 'Only one of the prefetched cds has a prefetched genre');
+  is ($pref->cds->count, 3, 'Correct number of CDs prefetched');
+  is ($pref->cds->search_related ('genre')->count, 1, 'Only one of the prefetched cds has a prefetched genre');
 
-is ($queries, 1, 'All happened within one query only');
-$schema->storage->debugcb($orig_cb);
-$schema->storage->debug(0);
+}, 1, 'All happened within one query only');
 
 done_testing;
index 8a7035c..729dbde 100644 (file)
@@ -25,11 +25,7 @@ is (
 );
 
 # this still should emit no queries:
-{
-  my $queries = 0;
-  my $orig_debug = $schema->storage->debug;
-  $schema->storage->debugcb(sub { $queries++; });
-  $schema->storage->debug(1);
+$schema->is_executed_querycount( sub {
 
   my $cds = $art->cds;
   is (
@@ -47,10 +43,6 @@ is (
     );
   }
 
-  $schema->storage->debug($orig_debug);
-  $schema->storage->debugcb(undef);
-
-  is ($queries, 0, 'No queries on prefetched operations');
-}
+}, 0, 'No queries on prefetched operations');
 
 done_testing;
index 26d3354..75107c7 100644 (file)
@@ -7,33 +7,26 @@ use lib qw(t/lib);
 use DBICTest;
 
 my $schema = DBICTest->init_schema();
-my $orig_debug = $schema->storage->debug;
 
-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/ ],
+my $rs;
+$schema->is_executed_querycount( sub {
+  my $search = { 'artist.name' => 'Caterwauler McCrae' };
+  my $attr = { prefetch => [ qw/artist liner_notes/ ],
              order_by => 'me.cdid' };
 
-my $rs = $schema->resultset("CD")->search($search, $attr);
-my @cd = $rs->all;
-
-is($cd[0]->title, 'Spoonful of bees', 'First record returned ok');
+  $rs = $schema->resultset("CD")->search($search, $attr);
+  my @cd = $rs->all;
 
-ok(!defined $cd[0]->liner_notes, 'No prefetch for NULL LEFT join');
+  is($cd[0]->title, 'Spoonful of bees', 'First record returned ok');
 
-is($cd[1]->{_relationship_data}{liner_notes}->notes, 'Buy Whiskey!', 'Prefetch for present LEFT JOIN');
+  ok(!defined $cd[0]->liner_notes, 'No prefetch for NULL LEFT join');
 
-is(ref $cd[1]->liner_notes, 'DBICTest::LinerNotes', 'Prefetch returns correct class');
+  is($cd[1]->{_relationship_data}{liner_notes}->notes, 'Buy Whiskey!', 'Prefetch for present LEFT JOIN');
 
-is($cd[2]->{_inflated_column}{artist}->name, 'Caterwauler McCrae', 'Prefetch on parent object ok');
+  is(ref $cd[1]->liner_notes, 'DBICTest::LinerNotes', 'Prefetch returns correct class');
 
-is($queries, 1, 'prefetch ran only 1 select statement');
-
-$schema->storage->debug($orig_debug);
-$schema->storage->debugobj->callback(undef);
+  is($cd[2]->{_inflated_column}{artist}->name, 'Caterwauler McCrae', 'Prefetch on parent object ok');
+}, 1, 'prefetch ran only 1 select statement');
 
 # test for partial prefetch via columns attr
 my $cd = $schema->resultset('CD')->find(1,
@@ -42,66 +35,50 @@ my $cd = $schema->resultset('CD')->find(1,
       join => { 'artist' => {} }
     }
 );
-ok(eval { $cd->artist->name eq 'Caterwauler McCrae' }, 'single related column prefetched');
+is( $cd->artist->name, '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(
-  { 'me.tagid' => 1 },
-  {
-    prefetch => { cd => 'artist' }
-  }
-);
-
-my $tag = $rs->first;
+my $tag;
+$schema->is_executed_querycount( sub {
+  $rs = $schema->resultset('Tag')->search(
+    { 'me.tagid' => 1 },
+    {
+      prefetch => { cd => 'artist' }
+    }
+  );
 
-is( $tag->cd->title, 'Spoonful of bees', 'step 1 ok for nested prefetch' );
+  $tag = $rs->first;
 
-is( $tag->cd->artist->name, 'Caterwauler McCrae', 'step 2 ok for nested prefetch');
+  is( $tag->cd->title, 'Spoonful of bees', 'step 1 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)');
+  is( $tag->cd->artist->name, 'Caterwauler McCrae', 'step 2 ok for nested prefetch');
+}, 1, 'nested prefetch ran exactly 1 select statement');
 
-$queries = 0;
 
-is($tag->search_related('cd')->search_related('artist')->first->name,
+$schema->is_executed_querycount( sub {
+  is($tag->search_related('cd')->search_related('artist')->first->name,
    'Caterwauler McCrae',
    'chained belongs_to->belongs_to search_related ok');
+}, 0, 'chained search_related after belongs_to->belongs_to prefetch ran no queries');
 
-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');
+$schema->is_executed_querycount( sub {
+  $cd = $schema->resultset('CD')->find(1, { prefetch => 'artist' });
 
-is($queries, 1, 'find with prefetch ran exactly 1 select statement (excluding column_info)');
+  is($cd->{_inflated_column}{artist}->name, 'Caterwauler McCrae', 'artist prefetched correctly on find');
+}, 1, 'find with prefetch ran exactly 1 select statement (excluding column_info)');
 
-$queries = 0;
+$schema->is_executed_querycount( sub {
+  $cd = $schema->resultset('CD')->find(1, { prefetch => { cd_to_producer => 'producer' }, order_by => 'producer.producerid' });
 
-$schema->storage->debugcb(sub { $queries++; });
+  is($cd->producers->first->name, 'Matt S Trout', 'many_to_many accessor ok');
+}, 1, 'many_to_many accessor with nested prefetch ran exactly 1 query');
 
-$cd = $schema->resultset('CD')->find(1, { prefetch => { cd_to_producer => 'producer' }, order_by => 'producer.producerid' });
+$schema->is_executed_querycount( sub {
+  my $producers = $cd->search_related('cd_to_producer')->search_related('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);
+  is($producers->first->name, 'Matt S Trout', 'chained many_to_many search_related ok');
+}, 0, 'chained search_related after many_to_many prefetch ran no queries');
 
 $rs = $schema->resultset('Tag')->search(
   {},
@@ -167,7 +144,7 @@ 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',
+$cd = $schema->resultset('Artist')->search({ artistid => 1 })->first->create_related('cds',
     {
     title   => 'Unproduced Single',
     year    => 2007
@@ -180,27 +157,22 @@ my $left_join = $schema->resultset('CD')->search(
 
 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' } },
+my $tree_like;
+$schema->is_executed_querycount( sub {
+  $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');
+  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');
+}, 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;
@@ -210,15 +182,15 @@ $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',
+is( $tree_like->children->first->children->first->name, 'quux',
    'Tree search_related with prefetch ok');
 
-$tree_like = eval { $schema->resultset('TreeLike')->search(
+$tree_like = $schema->resultset('TreeLike')->search(
     { 'children.id' => 3, 'children_2.id' => 6 },
     { join => [qw/children 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');
+  )->first->children->first;
+is( $tree_like->name, 'fong', 'Tree with multiple has_many joins ok');
 
 $rs = $schema->resultset('Artist');
 $rs->create({ artistid => 4, name => 'Unknown singer-songwriter' });
@@ -274,32 +246,24 @@ sub make_hash_struc {
     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);
+my $prefetch_result;
+$schema->is_executed_querycount( sub {
+  $prefetch_result = make_hash_struc($art_rs_pr);
+}, 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_deeply(
-  [ sort map { $_->title } $art_rs_pr->search_related('cds')->search_related('tracks')->all ],
-  [ 'Apiary', 'Beehind You', 'Boring Name', 'Boring Song', 'Fowlin', 'Howlin',
-    'No More Ideas', 'Sad', 'Sticky Honey', 'Stripy', 'Stung with Success',
-    'Suicidal', 'The Bees Knees', 'Under The Weather', 'Yowlin' ],
-  'chained has_many->has_many search_related ok'
-);
-
-is($queries, 0, 'chained search_related after has_many->has_many prefetch ran no queries');
-
-$schema->storage->debug($orig_debug);
-$schema->storage->debugobj->callback(undef);
+$schema->is_executed_querycount( sub {
+  is_deeply(
+    [ sort map { $_->title } $art_rs_pr->search_related('cds')->search_related('tracks')->all ],
+    [ 'Apiary', 'Beehind You', 'Boring Name', 'Boring Song', 'Fowlin', 'Howlin',
+      'No More Ideas', 'Sad', 'Sticky Honey', 'Stripy', 'Stung with Success',
+      'Suicidal', 'The Bees Knees', 'Under The Weather', 'Yowlin' ],
+    'chained has_many->has_many search_related ok'
+  );
+}, 0, 'chained search_related after has_many->has_many prefetch ran no queries');
 
 done_testing;
index 588b125..316035d 100644 (file)
@@ -9,10 +9,6 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-my $queries;
-my $debugcb = sub { $queries++; };
-my $orig_debug = $schema->storage->debug;
-
 lives_ok ( sub {
   my $no_prefetch = $schema->resultset('Track')->search_related(cd =>
     {
@@ -73,16 +69,12 @@ lives_ok ( sub {
 {
   my $cd = $schema->resultset('CD')->search({}, { prefetch => 'cd_to_producer' })->find(1);
 
-  $queries = 0;
-  $schema->storage->debugcb ($debugcb);
-  $schema->storage->debug (1);
-
-  is( $cd->cd_to_producer->count, 3 ,'Count of prefetched m2m links via accessor' );
-  is( scalar $cd->cd_to_producer->all, 3, 'Amount of prefetched m2m link objects via accessor' );
-  is( $cd->search_related('cd_to_producer')->count, 3, 'Count of prefetched m2m links via search_related' );
-  is( scalar $cd->search_related('cd_to_producer')->all, 3, 'Amount of prefetched m2m links via search_related' );
-
-  is($queries, 0, 'No queries ran so far');
+  $schema->is_executed_querycount( sub {
+      is( $cd->cd_to_producer->count, 3 ,'Count of prefetched m2m links via accessor' );
+    is( scalar $cd->cd_to_producer->all, 3, 'Amount of prefetched m2m link objects via accessor' );
+    is( $cd->search_related('cd_to_producer')->count, 3, 'Count of prefetched m2m links via search_related' );
+    is( scalar $cd->search_related('cd_to_producer')->all, 3, 'Amount of prefetched m2m links via search_related' );
+  }, 0, 'No queries ran so far');
 
   is( scalar $cd->cd_to_producer->search_related('producer')->all, 3,
       'Amount of objects via search_related off prefetched linker' );
@@ -97,16 +89,12 @@ lives_ok ( sub {
   is( $cd->producers->count, 3,
       'Count via m2m accessor' );
 
-  $queries = 0;
-
-  is( $cd->cd_to_producer->count, 3 ,'Review count of prefetched m2m links via accessor' );
-  is( scalar $cd->cd_to_producer->all, 3, 'Review amount of prefetched m2m link objects via accessor' );
-  is( $cd->search_related('cd_to_producer')->count, 3, 'Review count of prefetched m2m links via search_related' );
-  is( scalar $cd->search_related('cd_to_producer')->all, 3, 'Rreview amount of prefetched m2m links via search_related' );
-
-  is($queries, 0, 'Still no queries on prefetched linker');
-  $schema->storage->debugcb (undef);
-  $schema->storage->debug ($orig_debug);
+  $schema->is_executed_querycount( sub {
+    is( $cd->cd_to_producer->count, 3 ,'Review count of prefetched m2m links via accessor' );
+    is( scalar $cd->cd_to_producer->all, 3, 'Review amount of prefetched m2m link objects via accessor' );
+    is( $cd->search_related('cd_to_producer')->count, 3, 'Review count of prefetched m2m links via search_related' );
+    is( scalar $cd->search_related('cd_to_producer')->all, 3, 'Rreview amount of prefetched m2m links via search_related' );
+  }, 0, 'Still no queries on prefetched linker');
 }
 
 # tests with distinct => 1
@@ -169,21 +157,18 @@ lives_ok (sub {
     is($rs->all, 1, 'distinct with prefetch (objects)');
     is($rs->count, 1, 'distinct with prefetch (count)');
 
-    $queries = 0;
-    $schema->storage->debugcb ($debugcb);
-    $schema->storage->debug (1);
+    local $TODO = "This makes another 2 trips to the database, it can't be right";
+    $schema->is_executed_querycount( sub {
 
-    # artist -> 2 cds -> 2 genres -> 2 cds for each genre + distinct = 2
-    is($rs->search_related('cds')->all, 2, 'prefetched distinct with prefetch (objects)');
-    is($rs->search_related('cds')->count, 2, 'prefetched distinct with prefetch (count)');
+      # the is() calls are not todoified
+      local $TODO;
 
-    {
-      local $TODO = "This makes another 2 trips to the database, it can't be right";
-      is ($queries, 0, 'No extra queries fired (prefetch survives search_related)');
-    }
+      # artist -> 2 cds -> 2 genres -> 2 cds for each genre + distinct = 2
+      is($rs->search_related('cds')->all, 2, 'prefetched distinct with prefetch (objects)');
+      is($rs->search_related('cds')->count, 2, 'prefetched distinct with prefetch (count)');
+
+    }, 0, 'No extra queries fired (prefetch survives search_related)');
 
-    $schema->storage->debugcb (undef);
-    $schema->storage->debug ($orig_debug);
 }, 'distinct generally works with prefetch on deep search_related chains');
 
 # pathological "user knows what they're doing" case
@@ -200,7 +185,7 @@ lives_ok (sub {
   });
 
   is_deeply(
-    $rs->all_hri,
+    $rs->search({}, { order_by => 'me.title' })->all_hri,
     [
       { title => "Caterwaulin' Blues", max_trk => 3 },
       { title => "Come Be Depressed With Us", max_trk => 3 },
index 480dc40..28b3b8a 100644 (file)
@@ -6,8 +6,7 @@ use warnings;
 use Test::More;
 use Test::Exception;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 
 my $ROWS = DBIx::Class::SQLMaker::LimitDialects->__rows_bindtype;
@@ -152,8 +151,8 @@ throws_ok (
       {'tracks.title' => { '!=' => 'foo' }},
       { order_by => \ 'some oddball literal sql', join => { cds => 'tracks' } }
     )->next
-  }, qr/A required group_by clause could not be constructed automatically/,
-) || exit;
+  }, qr/Unable to programatically derive a required group_by from the supplied order_by criteria/,
+);
 
 my $artist = $use_prefetch->search({'cds.title' => $artist_many_cds->cds->first->title })->next;
 is($artist->cds->count, 1, "count on search limiting prefetched has_many");
index e86dfc6..87f635e 100644 (file)
@@ -3,12 +3,11 @@ use warnings;
 
 use Test::More;
 use Test::Exception;
+use Test::Warn;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
-my $sdebug = $schema->storage->debug;
 
 # has_a test
 my $cd = $schema->resultset("CD")->find(4);
@@ -33,17 +32,14 @@ $artist->create_related( 'cds', {
 my $big_flop_cd = ($artist->search_related('cds'))[3];
 is( $big_flop_cd->title, 'Big Flop', 'create_related ok' );
 
-{ # make sure we are not making pointless select queries when a FK IS NULL
-  my $queries = 0;
-  $schema->storage->debugcb(sub { $queries++; });
-  $schema->storage->debug(1);
+# make sure we are not making pointless select queries when a FK IS NULL
+$schema->is_executed_querycount( sub {
   $big_flop_cd->genre; #should not trigger a select query
-  is($queries, 0, 'No SELECT made for belongs_to if key IS NULL');
+},  0, 'No SELECT made for belongs_to if key IS NULL');
+
+$schema->is_executed_querycount( sub {
   $big_flop_cd->genre_inefficient; #should trigger a select query
-  is($queries, 1, 'SELECT made for belongs_to if key IS NULL when undef_on_null_fk disabled');
-  $schema->storage->debug($sdebug);
-  $schema->storage->debugcb(undef);
-}
+}, 1, 'SELECT made for belongs_to if key IS NULL when undef_on_null_fk disabled');
 
 my( $rs_from_list ) = $artist->search_related_rs('cds');
 isa_ok( $rs_from_list, 'DBIx::Class::ResultSet', 'search_related_rs in list context returns rs' );
@@ -195,11 +191,24 @@ is( $prod_rs->first->name, 'Testy McProducer',
     'many_to_many add_to_$rel($hash) ok' );
 $cd->add_to_producers({ name => 'Jack Black' });
 is( $prod_rs->count(), 2, 'many_to_many add_to_$rel($hash) count ok' );
-$cd->set_producers($schema->resultset('Producer')->all);
-is( $cd->producers->count(), $prod_before_count+2,
-    'many_to_many set_$rel(@objs) count ok' );
-$cd->set_producers($schema->resultset('Producer')->find(1));
-is( $cd->producers->count(), 1, 'many_to_many set_$rel($obj) count ok' );
+
+warnings_like {
+  $cd->set_producers($schema->resultset('Producer')->all);
+  is( $cd->producers->count(), $prod_before_count+2,
+      'many_to_many set_$rel(@objs) count ok' );
+
+  $cd->set_producers($schema->resultset('Producer')->find(1));
+  is( $cd->producers->count(), 1, 'many_to_many set_$rel($obj) count ok' );
+} [
+  ( qr/\QCalling 'set_producers' with a list of items to link to is deprecated, use an arrayref instead/ ) x 2
+], 'Warnings on deprecated invocation of set_* found';
+
+warnings_like {
+  is( $cd->producers( producerid => '666' )->count, 0 );
+} [
+  qr/\Qsearch( %condition ) is deprecated/
+], 'Warning properly bubbled from search()';
+
 $cd->set_producers([$schema->resultset('Producer')->all]);
 is( $cd->producers->count(), $prod_before_count+2,
     'many_to_many set_$rel(\@objs) count ok' );
@@ -208,11 +217,11 @@ is( $cd->producers->count(), 1, 'many_to_many set_$rel([$obj]) count ok' );
 
 throws_ok {
   $cd->remove_from_producers({ fake => 'hash' })
-} qr/needs an object/, 'remove_from_$rel($hash) dies correctly';
+} qr/expects an object/, 'remove_from_$rel($hash) dies correctly';
 
 throws_ok {
   $cd->add_to_producers()
-} qr/needs an object or hashref/, 'add_to_$rel(undef) dies correctly';
+} qr/expects an object or hashref/, 'add_to_$rel(undef) dies correctly';
 
 # many_to_many stresstest
 my $twokey = $schema->resultset('TwoKeys')->find(1,1);
@@ -232,7 +241,7 @@ is( $twokey->fourkeys_to_twokeys->count, 0,
 
 
 my $undef_artist_cd = $schema->resultset("CD")->new_result({ 'title' => 'badgers', 'year' => 2007 });
-is($undef_artist_cd->has_column_loaded('artist'), '', 'FK not loaded');
+ok(! $undef_artist_cd->has_column_loaded('artist'), 'FK not loaded');
 is($undef_artist_cd->search_related('artist')->count, 0, '0=1 search when FK does not exist and object not yet in db');
 lives_ok {
      $undef_artist_cd->related_resultset('artist')->new({name => 'foo'});
@@ -267,6 +276,22 @@ is_same_sql_bind (
 $undir_maps = $schema->resultset("Artist")->find(2)->artist_undirected_maps;
 is($undir_maps->count, 1, 'found 1 undirected map for artist 2');
 
+{
+  my $artist_to_mangle = $schema->resultset('Artist')->find(2);
+
+  $artist_to_mangle->set_from_related( artist_undirected_maps => { id1 => 42 } );
+
+  ok( ! $artist_to_mangle->is_changed, 'Unresolvable set_from_related did not alter object' );
+
+  $artist_to_mangle->set_from_related( artist_undirected_maps => {} );
+  ok( $artist_to_mangle->is_changed, 'Definitive set_from_related did alter object' );
+  is (
+    $artist_to_mangle->id,
+    undef,
+    'Correctly unset id on definitive outcome of OR condition',
+  );
+}
+
 my $mapped_rs = $undir_maps->search_related('mapped_artists');
 
 my @art = $mapped_rs->all;
index 98b8b45..b9bf5fa 100644 (file)
@@ -3,9 +3,9 @@ use warnings;
 
 use Test::More;
 use Test::Exception;
+use Test::Warn;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
@@ -44,7 +44,7 @@ is_same_sql_bind(
   )',
   [
     [
-      { sqlt_datatype => 'integer', dbic_colname => 'me.artist' }
+      {}
         => 21
     ],
     [
@@ -152,15 +152,20 @@ is_deeply(
 
 } 'prefetchy-fetchy-fetch';
 
+# create_related a plain cd via the equoivalent coderef cond, with no extra conditions
+lives_ok {
+  $artist->create_related('cds_cref_cond', { title => 'related creation via coderef cond', year => '2010' } );
+} 'created_related with simple condition works';
 
 # try to create_related a 80s cd
 throws_ok {
   $artist->create_related('cds_80s', { title => 'related creation 1' });
-} qr/\QCustom relationship 'cds_80s' not definitive - returns conditions instead of values for column(s): 'year'/,
+} qr/\QUnable to complete value inferrence - custom relationship 'cds_80s' on source 'Artist' returns conditions instead of values for column(s): 'year'/,
 'Create failed - complex cond';
 
 # now supply an explicit arg overwriting the ambiguous cond
-my $id_2020 = $artist->create_related('cds_80s', { title => 'related creation 2', year => '2020' })->id;
+my $cd_2020 = $artist->create_related('cds_80s', { title => 'related creation 2', year => '2020' });
+my $id_2020 = $cd_2020->id;
 is(
   $schema->resultset('CD')->find($id_2020)->title,
   'related creation 2',
@@ -178,7 +183,7 @@ is(
 # try a specific everything via a non-simplified rel
 throws_ok {
   $artist->create_related('cds_90s', { title => 'related_creation 4', year => '2038' });
-} qr/\QCustom relationship 'cds_90s' does not resolve to a join-free condition fragment/,
+} qr/\QRelationship 'cds_90s' on source 'Artist' does not resolve to a join-free condition fragment/,
 'Create failed - non-simplified rel';
 
 # Do a self-join last-entry search
@@ -212,38 +217,111 @@ is_deeply (
   'last_track via insane subquery condition works, even without prefetch',
 );
 
+
 my $artwork = $schema->resultset('Artwork')->search({},{ order_by => 'cd_id' })->first;
-my @artists = $artwork->artists->all;
+my @artists = $artwork->artists->search({}, { order_by => 'artistid' } );
 is(scalar @artists, 2, 'the two artists are associated');
 
 my @artwork_artists = $artwork->artwork_to_artist->all;
 foreach (@artwork_artists) {
   lives_ok {
     my $artista = $_->artist;
-    my $artistb = $_->artist_test_m2m;
+    my $artistb = $_->artist_limited_rank;
     ok($artista->rank < 10 ? $artistb : 1, 'belongs_to with custom rel works.');
-    my $artistc = $_->artist_test_m2m_noopt;
+    my $artistc = $_->artist_limited_rank_opaque;
     ok($artista->rank < 10 ? $artistc : 1, 'belongs_to with custom rel works even in non-simplified.');
   } 'belongs_to works with custom rels';
 }
 
-@artists = ();
-lives_ok {
-  @artists = $artwork->artists_test_m2m2->all;
-} 'manytomany with extended rels in the has many works';
-is(scalar @artists, 2, 'two artists');
+is(
+  $schema->resultset('Artwork')
+          ->related_resultset( 'artwork_to_artist_via_opaque_customcond' )
+           ->related_resultset( 'artist' )
+            ->search({}, { collapse => 1 })
+             ->count,
+  2,
+  'Custom rel works correctly',
+);
 
-@artists = ();
-lives_ok {
-  @artists = $artwork->artists_test_m2m->all;
-} 'can fetch many to many with optimized version';
-is(scalar @artists, 1, 'only one artist is associated');
+is (
+  scalar $artwork->all_artists_via_opaque_customcond->all,
+  2,
+  'Expected two m2m associated artist objects via opaque costom cond'
+);
 
-@artists = ();
-lives_ok {
-  @artists = $artwork->artists_test_m2m_noopt->all;
-} 'can fetch many to many with non-optimized version';
-is(scalar @artists, 1, 'only one artist is associated');
+for (qw( artist_limited_rank artist_limited_rank_opaque )) {
+  is(
+    $schema->resultset('Artwork')
+            ->related_resultset( 'artwork_to_artist_via_opaque_customcond' )
+             ->related_resultset( $_ )
+              ->search({}, { collapse => 1 })
+               ->count,
+    1,
+    'Condition over double custom rel works correctly',
+  );
+
+  is (
+    scalar $artwork->$_->all,
+    1,
+    'Expected one m2m associated artist object via opaque custom cond + conditional far cond'
+  );
+
+  cmp_ok(
+    $artwork->${\"remove_from_$_"} ( $artists[1] ),
+    '==',
+    0,
+    'deletion action reports 0'
+  );
+
+  is (
+    scalar $artwork->all_artists_via_opaque_customcond->all,
+    2,
+    'Indeed nothing was removed'
+  );
+
+  cmp_ok(
+    $artwork->${\"remove_from_$_"} ( $artists[0] ),
+    '==',
+    1,
+    'Removal reports correct count'
+  );
+
+  is (
+    scalar $artwork->all_artists_via_opaque_customcond->all,
+    1,
+    'Indeed removed the matching artist'
+  );
+
+  $artwork->${\"set_$_"}([]);
+
+  is (
+    scalar $artwork->all_artists_via_opaque_customcond->all,
+    0,
+    'Everything removed via limited far cond'
+  );
+
+  # can't use the opaque one - need set_from_related to work
+  $artwork->set_artist_limited_rank( \@artists );
+
+  {
+    local $TODO = 'Taking into account the relationship bridge condition is not likely to ever work... unless we get DQ hooked somehow';
+
+    is (
+      scalar $artwork->all_artists_via_opaque_customcond->all,
+      1,
+      'Re-Addition passed through only one of the artists'
+    );
+  }
+
+  throws_ok { $artwork->set_all_artists_via_opaque_customcond( \@artists ) }
+    qr/\QRelationship 'artwork_to_artist_via_opaque_customcond' on source 'Artwork' does not resolve to a join-free condition fragment/;
+
+  is (
+    scalar $artwork->all_artists_via_opaque_customcond->all,
+    2,
+    'Everything still there as expected'
+  );
+}
 
 
 # Make a single for each last_track
@@ -269,4 +347,71 @@ is_deeply (
   'Prefetched singles in proper order'
 );
 
+# test set_from_related/find_related with a belongs_to custom condition
+my $preexisting_cd = $schema->resultset('CD')->find(1);
+
+my $cd_single_track = $schema->resultset('CD')->create({
+  artist => $artist,
+  title => 'one one one',
+  year => 2001,
+  tracks => [{ title => 'uno uno uno' }]
+});
+
+my $single_track = $cd_single_track->tracks->next;
+
+is(
+  $single_track->cd_cref_cond->title,
+  $cd_single_track->title,
+  'Got back the expected single-track cd title',
+);
+
+is_deeply
+  { $schema->resultset('Track')->find({ cd_cref_cond => { cdid => $cd_single_track->id } })->get_columns },
+  { $single_track->get_columns },
+  'Proper find with related via coderef cond',
+;
+
+warnings_exist {
+  is_same_sql_bind(
+    $single_track->deliberately_broken_all_cd_tracks->as_query,
+    '(
+      SELECT me.trackid, me.cd, me.position, me.title, me.last_updated_on, me.last_updated_at
+        FROM track track__row
+        JOIN track me
+          ON me.cd = ?
+      WHERE track__row.trackid = ?
+    )',
+    [
+      [{ dbic_colname => "me.cd", sqlt_datatype => "integer" }
+        => "track__row.cd" ],
+      [{ dbic_colname => "track__row.trackid", sqlt_datatype => "integer" }
+        => 19 ],
+    ],
+    'Expected nonsensical JOIN cond',
+  ),
+} qr/\Qrelationship 'deliberately_broken_all_cd_tracks' on source 'Track' specifies equality of column 'cd' and the *VALUE* 'cd' (you did not use the { -ident => ... } operator)/,
+  'Warning on 99.9999% malformed custom cond'
+;
+
+$single_track->set_from_related( cd_cref_cond => undef );
+ok $single_track->is_column_changed('cd');
+is $single_track->get_column('cd'), undef, 'UNset from related via coderef cond';
+is $single_track->cd, undef, 'UNset related object via coderef cond';
+
+$single_track->discard_changes;
+
+$single_track->set_from_related( cd_cref_cond => $preexisting_cd );
+ok $single_track->is_column_changed('cd');
+is $single_track->get_column('cd'), 1, 'set from related via coderef cond';
+is_deeply
+  { $single_track->cd->get_columns },
+  { $preexisting_cd->get_columns },
+  'set from related via coderef cond inflates properly',
+;
+
+throws_ok {
+  local $schema->source('Track')->relationship_info('cd_cref_cond')->{cond} = sub { 1,2,3 };
+  $schema->resultset('Track')->find({ cd_cref_cond => {} });
+} qr/\QA custom condition coderef can return at most 2 conditions, but relationship 'cd_cref_cond' on source 'Track' returned extra values: 3/;
+
 done_testing;
diff --git a/t/relationship/custom_opaque.t b/t/relationship/custom_opaque.t
new file mode 100644 (file)
index 0000000..1139c6a
--- /dev/null
@@ -0,0 +1,51 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib 't/lib';
+use DBICTest;
+
+my $schema = DBICTest->init_schema( no_populate => 1, quote_names => 1 );
+
+$schema->resultset('CD')->create({
+  title => 'Equinoxe',
+  year => 1978,
+  artist => { name => 'JMJ' },
+  genre => { name => 'electro' },
+  tracks => [
+    { title => 'e1' },
+    { title => 'e2' },
+    { title => 'e3' },
+  ],
+  single_track => {
+    title => 'o1',
+    cd => {
+      title => 'Oxygene',
+      year => 1976,
+      artist => { name => 'JMJ' },
+    },
+  },
+});
+
+my $cd = $schema->resultset('CD')->search({ single_track => { '!=', undef } })->first;
+
+$schema->is_executed_sql_bind(
+  sub { is( eval{$cd->single_track_opaque->title}, 'o1', 'Found correct single track' ) },
+  [
+    [
+      'SELECT "me"."trackid", "me"."cd", "me"."position", "me"."title", "me"."last_updated_on", "me"."last_updated_at"
+          FROM cd "cd__row"
+          JOIN "track" "me"
+            ON me.trackid = cd__row.single_track
+        WHERE "cd__row"."cdid" = ?
+      ',
+      [
+        { dbic_colname => "cd__row.cdid", sqlt_datatype => "integer" }
+          => 2
+      ]
+    ],
+  ],
+);
+
+done_testing;
diff --git a/t/relationship/resolve_relationship_condition.t b/t/relationship/resolve_relationship_condition.t
new file mode 100644 (file)
index 0000000..1d4cb62
--- /dev/null
@@ -0,0 +1,32 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+
+use lib 't/lib';
+use DBICTest;
+
+my $schema = DBICTest->init_schema();
+
+for (
+  { year => [1,2] },
+  { year => ['-and',1,2] },
+  { -or => [ year => 1, year => 2 ] },
+  { -and => [ year => 1, year => 2 ] },
+) {
+  throws_ok {
+    $schema->source('Track')->_resolve_relationship_condition(
+      rel_name => 'cd_cref_cond',
+      self_alias => 'me',
+      foreign_alias => 'cd',
+      foreign_values => $_
+    );
+  } qr/
+    \Qis not a column on related source 'CD'\E
+      |
+    \QValue supplied for '...{foreign_values}{year}' is not a direct equivalence expression\E
+  /x;
+}
+
+done_testing;
index c7cce7a..5dde83d 100644 (file)
@@ -6,10 +6,8 @@ use Test::Exception;
 use Test::Warn;
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::SqlMakerTest;
 
 my $schema = DBICTest->init_schema();
-my $sdebug = $schema->storage->debug;
 
 my $artist = $schema->resultset ('Artist')->find(1);
 
@@ -79,28 +77,30 @@ throws_ok {
 # expect a create, after a failed search using *only* the
 # *current* relationship and the unique column constraints
 # (so no year)
-my @sql;
-$schema->storage->debugcb(sub { push @sql, $_[1] });
-$schema->storage->debug (1);
-
-$genre->update_or_create_related ('cds', {
-  title => 'the best thing since vertical toasters',
-  artist => $artist,
-  year => 2012,
-});
-
-$schema->storage->debugcb(undef);
-$schema->storage->debug ($sdebug);
-
-my ($search_sql) = $sql[0] =~ /^(SELECT .+?)\:/;
-is_same_sql (
-  $search_sql,
-  'SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
-    FROM cd me
-    WHERE ( me.artist = ? AND me.title = ? AND me.genreid = ? )
-  ',
-  'expected select issued',
-);
+$schema->is_executed_sql_bind( sub {
+  $genre->update_or_create_related ('cds', {
+    title => 'the best thing since vertical toasters',
+    artist => $artist,
+    year => 2012,
+  });
+}, [
+  [
+    'SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
+        FROM cd me
+      WHERE ( me.artist = ? AND me.genreid = ? AND me.title = ? )
+    ',
+    1,
+    2,
+    "the best thing since vertical toasters",
+  ],
+  [
+    'INSERT INTO cd ( artist, genreid, title, year) VALUES ( ?, ?, ?, ? )',
+    1,
+    2,
+    "the best thing since vertical toasters",
+    2012,
+  ],
+], 'expected select issued' );
 
 # a has_many search without a unique constraint makes no sense
 # but I am not sure what to test for - leaving open
index efd5e6e..3b43e9c 100644 (file)
@@ -4,8 +4,7 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 my $art_rs = $schema->resultset('Artist');
@@ -43,8 +42,8 @@ my $rank_resolved_bind = [
 {
   is_same_sql_bind(
     $art_rs->as_query,
-    "(SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me WHERE ( ( ( rank = ? ) AND ( name = ? ) ) ) )",
-    [ $rank_resolved_bind, $name_resolved_bind ],
+    "(SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me WHERE name = ? AND rank = ? )",
+    [ $name_resolved_bind, $rank_resolved_bind ],
   );
 }
 
@@ -53,8 +52,8 @@ my $rscol = $art_rs->get_column( 'charfield' );
 {
   is_same_sql_bind(
     $rscol->as_query,
-    "(SELECT me.charfield FROM artist me WHERE ( ( ( rank = ? ) AND ( name = ? ) ) ) )",
-    [ $rank_resolved_bind, $name_resolved_bind ],
+    "(SELECT me.charfield FROM artist me WHERE name = ? AND rank = ? )",
+    [ $name_resolved_bind, $rank_resolved_bind ],
   );
 }
 
index c0f8110..6d75977 100644 (file)
@@ -5,8 +5,7 @@ use Test::More;
 use Test::Exception;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
@@ -40,8 +39,6 @@ is_same_sql_bind (
   'Resultset-class attributes do not seep outside of the subselect',
 );
 
-$schema->storage->debug(1);
-
 is_same_sql_bind(
   $schema->resultset('CD')->search ({}, {
     rows => 2,
@@ -70,5 +67,4 @@ is_same_sql_bind(
   [ [{ sqlt_datatype => 'integer' } => 2 ] ],
 );
 
-
 done_testing;
index d93dcd1..7f25d99 100644 (file)
@@ -3,8 +3,7 @@ use warnings;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema;
 
@@ -16,8 +15,6 @@ my $where_bind = {
 my $rs;
 
 {
-    local $TODO = 'bind args order needs fixing (semifor)';
-
     # First, the simple cases...
     $rs = $schema->resultset('Artist')->search(
             { artistid => 1 },
@@ -37,7 +34,6 @@ my $rs;
     is ( $rs->count, 1, 'where/bind last' );
 
     # and the complex case
-    local $TODO = 'bind args order needs fixing (semifor)';
     $rs = $schema->resultset('CustomSql')->search({}, { bind => [ 1999 ] })
         ->search({ 'artistid' => 1 }, {
             where => \'title like ?',
similarity index 72%
rename from t/96_is_deteministic_value.t
rename to t/resultset/create_with_rs_inherited_values.t
index d71886b..8a0acd3 100644 (file)
@@ -3,16 +3,11 @@ use warnings;
 
 use Test::More;
 use Test::Exception;
+use Math::BigInt;
 
 use lib qw(t/lib);
 use DBICTest;
 
-BEGIN {
-  require DBIx::Class;
-  plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt')
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt');
-}
-
 my $schema = DBICTest->init_schema();
 my $artist_rs = $schema->resultset('Artist');
 my $cd_rs = $schema->resultset('CD');
@@ -30,9 +25,8 @@ my $cd_rs = $schema->resultset('CD');
  }
 
  {
-   my $formatter = DateTime::Format::Strptime->new(pattern => '%Y');
-   my $dt = DateTime->new(year => 2006, month => 06, day => 06,
-                          formatter => $formatter );
+   my $dt = Math::BigInt->new(2006);
+
    my $cd;
    lives_ok {
      $cd = $cd_rs->search({ year => $dt})->create
diff --git a/t/resultset/find_on_subquery_cond.t b/t/resultset/find_on_subquery_cond.t
new file mode 100644 (file)
index 0000000..af2ca51
--- /dev/null
@@ -0,0 +1,34 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+
+use lib qw(t/lib);
+use DBICTest;
+
+my $schema = DBICTest->init_schema();
+my $rs = $schema->resultset('Artist');
+
+for my $id (
+  2,
+  \' = 2 ',
+  \[ '= ?', 2 ],
+) {
+  lives_ok {
+    is( $rs->find({ artistid => $id })->id, 2 )
+  } "Correctly found artist with id of @{[ explain $id ]}";
+}
+
+for my $id (
+  2,
+  \'2',
+  \[ '?', 2 ],
+) {
+  my $cond = { artistid => { '=', $id } };
+  lives_ok {
+    is( $rs->find($cond)->id, 2 )
+  } "Correctly found artist with id of @{[ explain $cond ]}";
+}
+
+done_testing;
index 1fa917a..e6bedc2 100644 (file)
@@ -502,4 +502,31 @@ sub cmp_structures {
   cmp_deeply($left, $right, $msg||()) or next INFTYPE;
 }
 
+{
+  package DBICTest::_DoubleResult;
+
+  sub inflate_result {
+    my $class = shift;
+    return map { DBIx::Class::ResultClass::HashRefInflator->inflate_result(@_) } (1,2);
+  }
+}
+
+my $oxygene_rs = $schema->resultset('CD')->search({ 'me.title' => 'Oxygene' });
+
+is_deeply(
+  [ $oxygene_rs->search({}, { result_class => 'DBICTest::_DoubleResult' })->all ],
+  [ ({ $oxygene_rs->single->get_columns }) x 2 ],
+);
+
+is_deeply(
+  [ $oxygene_rs->search({}, {
+    result_class => 'DBICTest::_DoubleResult', prefetch => [qw(artist tracks)],
+    order_by => [qw(me.cdid tracks.title)],
+  })->all ],
+  [ (@{$oxygene_rs->search({}, {
+    prefetch=> [qw(artist tracks)],
+    order_by => [qw(me.cdid tracks.title)],
+  })->all_hri}) x 2 ],
+);
+
 done_testing;
index 4f082f5..db55ac4 100644 (file)
@@ -2,6 +2,7 @@ use strict;
 use warnings;
 
 use Test::More;
+use Math::BigInt;
 
 use lib qw(t/lib);
 use DBICTest;
@@ -42,24 +43,18 @@ is_deeply (
   'extra columns returned by get_inflated_columns without inflatable columns',
 );
 
-SKIP: {
-  skip (
-    "+select/get_inflated_columns tests need " . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt'),
-    1
-  ) unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_dt');
-
-  $schema->class('CD')->inflate_column( 'year',
-    { inflate => sub { DateTime->new( year => shift ) },
-      deflate => sub { shift->year } }
-  );
+# Test object inflation
+$schema->class('CD')->inflate_column( 'year',
+  { inflate => sub { Math::BigInt->new( shift ) },
+    deflate => sub { shift() . '' } }
+);
 
-  $basecols{year} = DateTime->new ( year => $basecols{year} );
+$basecols{year} = Math::BigInt->new( $basecols{year} );
 
-  is_deeply (
-    { $plus_rs->first->get_inflated_columns, %todo_rel_inflation_override },
-    { %basecols, tr_cnt => $track_cnt },
-    'extra columns returned by get_inflated_columns',
-  );
-}
+is_deeply (
+  { $plus_rs->first->get_inflated_columns, %todo_rel_inflation_override },
+  { %basecols, tr_cnt => $track_cnt },
+  'extra columns returned by get_inflated_columns',
+);
 
 done_testing;
index b089ecc..e89369f 100644 (file)
@@ -5,6 +5,7 @@ use Test::More;
 use lib qw(t/lib);
 use DBICTest;
 use B::Deparse;
+use DBIx::Class::_Util 'perlstring';
 
 # globally set for the rest of test
 # the rowparser maker does not order its hashes by default for the miniscule
@@ -251,48 +252,62 @@ is_same_src (
     my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids);
 
     while ($cur_row_data = (
-      ( $rows_pos >= 0 and $_[0][$rows_pos++] )
-        ||
-      ( $_[1] and $rows_pos = -1 and $_[1]->() )
+      (
+        $rows_pos >= 0
+          and
+        (
+          $_[0][$rows_pos++]
+            or
+          ( ($rows_pos = -1), undef )
+        )
+      )
+        or
+      ( $_[1] and $_[1]->() )
     ) ) {
 
-      $cur_row_ids{0} = $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0";
-      $cur_row_ids{1} = $cur_row_data->[1] // "\0NULL\xFF$rows_pos\xFF1\0";
-      $cur_row_ids{3} = $cur_row_data->[3] // "\0NULL\xFF$rows_pos\xFF3\0";
-      $cur_row_ids{4} = $cur_row_data->[4] // "\0NULL\xFF$rows_pos\xFF4\0";
-      $cur_row_ids{5} = $cur_row_data->[5] // "\0NULL\xFF$rows_pos\xFF5\0";
+      ( @cur_row_ids{0,1,3,4,5} = (
+        ( $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0" ),
+        ( $cur_row_data->[1] // "\0NULL\xFF$rows_pos\xFF1\0" ),
+        ( $cur_row_data->[3] // "\0NULL\xFF$rows_pos\xFF3\0" ),
+        ( $cur_row_data->[4] // "\0NULL\xFF$rows_pos\xFF4\0" ),
+        ( $cur_row_data->[5] // "\0NULL\xFF$rows_pos\xFF5\0" ),
+      ) ),
 
       # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
-      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}} and (unshift @{$_[2]}, $cur_row_data) and last;
+      ( $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}} and (unshift @{$_[2]}, $cur_row_data) and last ),
 
       # the rowdata itself for root node
-      $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}} //= $_[0][$result_pos++] = [{ artist => $cur_row_data->[5], title => $cur_row_data->[4], year => $cur_row_data->[2] }];
+      ( $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}} //= $_[0][$result_pos++] = [{ artist => $cur_row_data->[5], title => $cur_row_data->[4], year => $cur_row_data->[2] }] ),
 
       # prefetch data of single_track (placed in root)
-      $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{single_track} //= $collapse_idx[1]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}} = [];
-      defined($cur_row_data->[1]) or bless( $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{single_track}, __NBC__ );
+      ( $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{single_track} //= $collapse_idx[1]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}} = [] ),
+      ( defined($cur_row_data->[1]) or bless( $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{single_track}, __NBC__ ) ),
 
       # prefetch data of cd (placed in single_track)
-      $collapse_idx[1]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{cd} //= $collapse_idx[2]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}} = [];
+      ( $collapse_idx[1]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{cd} //= $collapse_idx[2]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}} = [] ),
 
       # prefetch data of artist ( placed in single_track->cd)
-      $collapse_idx[2]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{artist} //= $collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}} = [{ artistid => $cur_row_data->[1] }];
+      ( $collapse_idx[2]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{artist} //= $collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}} = [{ artistid => $cur_row_data->[1] }] ),
 
       # prefetch data of cds (if available)
-      (! $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} )
-        and
-      push @{$collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{cds}}, (
-        $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} = [{ cdid => $cur_row_data->[3] }]
-      );
-      defined($cur_row_data->[3]) or bless( $collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{cds}, __NBC__ );
+      (
+        (! $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} )
+          and
+        push @{$collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{cds}}, (
+          $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} = [{ cdid => $cur_row_data->[3] }]
+        )
+      ),
+      ( defined($cur_row_data->[3]) or bless( $collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{cds}, __NBC__ ) ),
 
       # prefetch data of tracks (if available)
-      (! $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} )
-        and
-      push @{$collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{tracks}}, (
-        $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} = [{ title => $cur_row_data->[0] }]
-      );
-      defined($cur_row_data->[0]) or bless( $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{tracks}, __NBC__ );
+      (
+        (! $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} )
+          and
+        push @{$collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{tracks}}, (
+          $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} = [{ title => $cur_row_data->[0] }]
+        )
+      ),
+      ( defined($cur_row_data->[0]) or bless( $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}}[1]{tracks}, __NBC__ ) ),
 
     }
     $#{$_[0]} = $result_pos - 1;
@@ -308,50 +323,64 @@ is_same_src (
     prune_null_branches => 1,
   }))[0],
   ' my $rows_pos = 0;
-    my ($result_pos, @collapse_idx, $cur_row_data);
+    my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids);
 
     while ($cur_row_data = (
-      ( $rows_pos >= 0 and $_[0][$rows_pos++] )
-        ||
-      ( $_[1] and $rows_pos = -1 and $_[1]->() )
+      (
+        $rows_pos >= 0
+          and
+        (
+          $_[0][$rows_pos++]
+            or
+          ( ($rows_pos = -1), undef )
+        )
+      )
+        or
+      ( $_[1] and $_[1]->() )
     ) ) {
 
+      ( @cur_row_ids{0, 1, 3, 4, 5} = @{$cur_row_data}[0, 1, 3, 4, 5] ),
+
       # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
-      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_data->[4]}{$cur_row_data->[5]} and (unshift @{$_[2]}, $cur_row_data) and last;
+      ( $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}} and (unshift @{$_[2]}, $cur_row_data) and last ),
 
       # the rowdata itself for root node
-      $collapse_idx[0]{$cur_row_data->[4]}{$cur_row_data->[5]} //= $_[0][$result_pos++] = { artist => $cur_row_data->[5], title => $cur_row_data->[4], year => $cur_row_data->[2] };
+      ( $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}} //= $_[0][$result_pos++] = { artist => $cur_row_data->[5], title => $cur_row_data->[4], year => $cur_row_data->[2] } ),
 
       # prefetch data of single_track (placed in root)
-      (! defined($cur_row_data->[1]) ) ? $collapse_idx[0]{$cur_row_data->[4]}{$cur_row_data->[5]}{single_track} = undef : do {
-        $collapse_idx[0]{$cur_row_data->[4]}{$cur_row_data->[5]}{single_track} //= $collapse_idx[1]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]};
+      ( (! defined($cur_row_data->[1]) ) ? $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}}{single_track} = undef : do {
+        ( $collapse_idx[0]{$cur_row_ids{4}}{$cur_row_ids{5}}{single_track} //= $collapse_idx[1]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}} ),
 
         # prefetch data of cd (placed in single_track)
-        $collapse_idx[1]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]}{cd} //= $collapse_idx[2]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]};
+        ( $collapse_idx[1]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}{cd} //= $collapse_idx[2]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}} ),
 
         # prefetch data of artist ( placed in single_track->cd)
-        $collapse_idx[2]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]}{artist} //= $collapse_idx[3]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]} = { artistid => $cur_row_data->[1] };
+        ( $collapse_idx[2]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}{artist} //= $collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}} = { artistid => $cur_row_data->[1] } ),
 
         # prefetch data of cds (if available)
-        (! defined $cur_row_data->[3] ) ? $collapse_idx[3]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]}{cds} = [] : do {
+        ( (! defined $cur_row_data->[3] ) ? $collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}{cds} = [] : do {
 
-          (! $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]} )
-            and
-          push @{$collapse_idx[3]{$cur_row_data->[1]}{$cur_row_data->[4]}{$cur_row_data->[5]}{cds}}, (
-            $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]} = { cdid => $cur_row_data->[3] }
-          );
+          (
+            (! $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} )
+              and
+            push @{$collapse_idx[3]{$cur_row_ids{1}}{$cur_row_ids{4}}{$cur_row_ids{5}}{cds}}, (
+              $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} = { cdid => $cur_row_data->[3] }
+            )
+          ),
 
           # prefetch data of tracks (if available)
-          ( ! defined $cur_row_data->[0] ) ? $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]}{tracks} = [] : do {
-
-            (! $collapse_idx[5]{$cur_row_data->[0]}{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]} )
-              and
-            push @{$collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]}{tracks}}, (
-              $collapse_idx[5]{$cur_row_data->[0]}{$cur_row_data->[1]}{$cur_row_data->[3]}{$cur_row_data->[4]}{$cur_row_data->[5]} = { title => $cur_row_data->[0] }
-            );
-          };
-        };
-      };
+          (( ! defined $cur_row_data->[0] ) ? $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}}{tracks} = [] : do {
+
+            (
+              (! $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} )
+                and
+              push @{$collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}}{tracks}}, (
+                $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{3}}{$cur_row_ids{4}}{$cur_row_ids{5}} = { title => $cur_row_data->[0] }
+              )
+            ),
+          } ),
+        } ),
+      } ),
     }
     $#{$_[0]} = $result_pos - 1;
   ',
@@ -428,56 +457,74 @@ is_same_src (
     my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids);
 
     while ($cur_row_data = (
-      ( $rows_pos >= 0 and $_[0][$rows_pos++] )
-        ||
-      ( $_[1] and $rows_pos = -1 and $_[1]->() )
+      (
+        $rows_pos >= 0
+          and
+        (
+          $_[0][$rows_pos++]
+            or
+          ( ($rows_pos = -1), undef )
+        )
+      )
+        or
+      ( $_[1] and $_[1]->() )
     ) ) {
 
-      $cur_row_ids{0} = $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0";
-      $cur_row_ids{1} = $cur_row_data->[1] // "\0NULL\xFF$rows_pos\xFF1\0";
-      $cur_row_ids{5} = $cur_row_data->[5] // "\0NULL\xFF$rows_pos\xFF5\0";
-      $cur_row_ids{6} = $cur_row_data->[6] // "\0NULL\xFF$rows_pos\xFF6\0";
-      $cur_row_ids{8} = $cur_row_data->[8] // "\0NULL\xFF$rows_pos\xFF8\0";
-      $cur_row_ids{10} = $cur_row_data->[10] // "\0NULL\xFF$rows_pos\xFF10\0";
+      ( @cur_row_ids{0, 1, 5, 6, 8, 10} = (
+        $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0",
+        $cur_row_data->[1] // "\0NULL\xFF$rows_pos\xFF1\0",
+        $cur_row_data->[5] // "\0NULL\xFF$rows_pos\xFF5\0",
+        $cur_row_data->[6] // "\0NULL\xFF$rows_pos\xFF6\0",
+        $cur_row_data->[8] // "\0NULL\xFF$rows_pos\xFF8\0",
+        $cur_row_data->[10] // "\0NULL\xFF$rows_pos\xFF10\0",
+      ) ),
 
       # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
-      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{1}} and (unshift @{$_[2]}, $cur_row_data) and last;
-
-      $collapse_idx[0]{$cur_row_ids{1}} //= $_[0][$result_pos++] = [{ genreid => $cur_row_data->[4], latest_cd => $cur_row_data->[7], year => $cur_row_data->[3] }];
-
-      $collapse_idx[0]{$cur_row_ids{1}}[1]{existing_single_track} //= $collapse_idx[1]{$cur_row_ids{1}} = [];
-      $collapse_idx[1]{$cur_row_ids{1}}[1]{cd} //= $collapse_idx[2]{$cur_row_ids{1}} = [];
-      $collapse_idx[2]{$cur_row_ids{1}}[1]{artist} //= $collapse_idx[3]{$cur_row_ids{1}} = [{ artistid => $cur_row_data->[1] }];
-
-      (! $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}} )
-        and
-      push @{ $collapse_idx[3]{$cur_row_ids{1}}[1]{cds} }, (
-        $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}} = [{ cdid => $cur_row_data->[6], genreid => $cur_row_data->[9], year => $cur_row_data->[2] }]
-      );
-      defined($cur_row_data->[6]) or bless( $collapse_idx[3]{$cur_row_ids{1}}[1]{cds}, __NBC__ );
-
-      (! $collapse_idx[5]{$cur_row_ids{1}}{$cur_row_ids{6}}{$cur_row_ids{8}} )
-        and
-      push @{ $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}}[1]{tracks} }, (
-        $collapse_idx[5]{$cur_row_ids{1}}{$cur_row_ids{6}}{$cur_row_ids{8}} = [{ title => $cur_row_data->[8] }]
-      );
-      defined($cur_row_data->[8]) or bless( $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}}[1]{tracks}, __NBC__ );
-
-      (! $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}} )
-        and
-      push @{ $collapse_idx[0]{$cur_row_ids{1}}[1]{tracks} }, (
-        $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}} = [{ title => $cur_row_data->[5] }]
-      );
-      defined($cur_row_data->[5]) or bless( $collapse_idx[0]{$cur_row_ids{1}}[1]{tracks}, __NBC__ );
-
-      $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}}[1]{lyrics} //= $collapse_idx[7]{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} = [];
-      defined($cur_row_data->[10]) or bless( $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}}[1]{lyrics}, __NBC__ );
-
-      (! $collapse_idx[8]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} )
-        and
-      push @{ $collapse_idx[7]{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}}[1]{existing_lyric_versions} }, (
-        $collapse_idx[8]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} = [{ lyric_id => $cur_row_data->[10], text => $cur_row_data->[0] }]
-      );
+      ( $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{1}} and (unshift @{$_[2]}, $cur_row_data) and last ),
+
+      ( $collapse_idx[0]{$cur_row_ids{1}} //= $_[0][$result_pos++] = [{ genreid => $cur_row_data->[4], latest_cd => $cur_row_data->[7], year => $cur_row_data->[3] }] ),
+
+      ( $collapse_idx[0]{$cur_row_ids{1}}[1]{existing_single_track} //= $collapse_idx[1]{$cur_row_ids{1}} = [] ),
+      ( $collapse_idx[1]{$cur_row_ids{1}}[1]{cd} //= $collapse_idx[2]{$cur_row_ids{1}} = [] ),
+      ( $collapse_idx[2]{$cur_row_ids{1}}[1]{artist} //= $collapse_idx[3]{$cur_row_ids{1}} = [{ artistid => $cur_row_data->[1] }] ),
+
+      (
+        (! $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}} )
+          and
+        push @{ $collapse_idx[3]{$cur_row_ids{1}}[1]{cds} }, (
+          $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}} = [{ cdid => $cur_row_data->[6], genreid => $cur_row_data->[9], year => $cur_row_data->[2] }]
+        )
+      ),
+      ( defined($cur_row_data->[6]) or bless( $collapse_idx[3]{$cur_row_ids{1}}[1]{cds}, __NBC__ ) ),
+
+      (
+        (! $collapse_idx[5]{$cur_row_ids{1}}{$cur_row_ids{6}}{$cur_row_ids{8}} )
+          and
+        push @{ $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}}[1]{tracks} }, (
+          $collapse_idx[5]{$cur_row_ids{1}}{$cur_row_ids{6}}{$cur_row_ids{8}} = [{ title => $cur_row_data->[8] }]
+        )
+      ),
+      ( defined($cur_row_data->[8]) or bless( $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}}[1]{tracks}, __NBC__ ) ),
+
+      (
+        (! $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}} )
+          and
+        push @{ $collapse_idx[0]{$cur_row_ids{1}}[1]{tracks} }, (
+          $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}} = [{ title => $cur_row_data->[5] }]
+        )
+      ),
+      ( defined($cur_row_data->[5]) or bless( $collapse_idx[0]{$cur_row_ids{1}}[1]{tracks}, __NBC__ ) ),
+
+      ( $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}}[1]{lyrics} //= $collapse_idx[7]{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} = [] ),
+      ( defined($cur_row_data->[10]) or bless( $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}}[1]{lyrics}, __NBC__ ) ),
+
+      (
+        (! $collapse_idx[8]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} )
+          and
+        push @{ $collapse_idx[7]{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}}[1]{existing_lyric_versions} }, (
+          $collapse_idx[8]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} = [{ lyric_id => $cur_row_data->[10], text => $cur_row_data->[0] }]
+        )
+      ),
     }
 
     $#{$_[0]} = $result_pos - 1;
@@ -492,59 +539,76 @@ is_same_src (
     prune_null_branches => 1,
   }))[0],
   ' my $rows_pos = 0;
-    my ($result_pos, @collapse_idx, $cur_row_data);
+    my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids);
 
     while ($cur_row_data = (
-      ( $rows_pos >= 0 and $_[0][$rows_pos++] )
-        ||
-      ( $_[1] and $rows_pos = -1 and $_[1]->() )
+      (
+        $rows_pos >= 0
+          and
+        (
+          $_[0][$rows_pos++]
+            or
+          ( ($rows_pos = -1), undef )
+        )
+      )
+        or
+      ( $_[1] and $_[1]->() )
     ) ) {
 
-      # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
-      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_data->[1]} and (unshift @{$_[2]}, $cur_row_data) and last;
+      ( @cur_row_ids{( 0, 1, 5, 6, 8, 10 )} = @{$cur_row_data}[( 0, 1, 5, 6, 8, 10 )] ),
 
-      $collapse_idx[0]{$cur_row_data->[1]} //= $_[0][$result_pos++] = [{ genreid => $cur_row_data->[4], latest_cd => $cur_row_data->[7], year => $cur_row_data->[3] }];
+      # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
+      ( $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{1}} and (unshift @{$_[2]}, $cur_row_data) and last ),
 
-      $collapse_idx[0]{$cur_row_data->[1]}[1]{existing_single_track} //= $collapse_idx[1]{$cur_row_data->[1]} = [];
-      $collapse_idx[1]{$cur_row_data->[1]}[1]{cd} //= $collapse_idx[2]{$cur_row_data->[1]} = [];
-      $collapse_idx[2]{$cur_row_data->[1]}[1]{artist} //= $collapse_idx[3]{$cur_row_data->[1]} = [{ artistid => $cur_row_data->[1] }];
+      ( $collapse_idx[0]{$cur_row_ids{1}} //= $_[0][$result_pos++] = [{ genreid => $cur_row_data->[4], latest_cd => $cur_row_data->[7], year => $cur_row_data->[3] }] ),
 
-      (! defined($cur_row_data->[6])) ? $collapse_idx[3]{$cur_row_data->[1]}[1]{cds} = [] : do {
-        (! $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[6]} )
-          and
-        push @{ $collapse_idx[3]{$cur_row_data->[1]}[1]{cds} }, (
-          $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[6]} = [{ cdid => $cur_row_data->[6], genreid => $cur_row_data->[9], year => $cur_row_data->[2] }]
-        );
+      ( $collapse_idx[0]{$cur_row_ids{1}}[1]{existing_single_track} //= $collapse_idx[1]{$cur_row_ids{1}} = [] ),
+      ( $collapse_idx[1]{$cur_row_ids{1}}[1]{cd} //= $collapse_idx[2]{$cur_row_ids{1}} = [] ),
+      ( $collapse_idx[2]{$cur_row_ids{1}}[1]{artist} //= $collapse_idx[3]{$cur_row_ids{1}} = [{ artistid => $cur_row_data->[1] }] ),
 
-        (! defined($cur_row_data->[8]) ) ? $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[6]}[1]{tracks} = [] : do {
-
-          (! $collapse_idx[5]{$cur_row_data->[1]}{$cur_row_data->[6]}{$cur_row_data->[8]} )
+      ( (! defined($cur_row_data->[6])) ? $collapse_idx[3]{$cur_row_ids{1}}[1]{cds} = [] : do {
+        (
+          (! $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}} )
             and
-          push @{ $collapse_idx[4]{$cur_row_data->[1]}{$cur_row_data->[6]}[1]{tracks} }, (
-            $collapse_idx[5]{$cur_row_data->[1]}{$cur_row_data->[6]}{$cur_row_data->[8]} = [{ title => $cur_row_data->[8] }]
-          );
-        };
-      };
+          push @{ $collapse_idx[3]{$cur_row_ids{1}}[1]{cds} }, (
+            $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}} = [{ cdid => $cur_row_data->[6], genreid => $cur_row_data->[9], year => $cur_row_data->[2] }]
+          )
+        ),
+
+        ( (! defined($cur_row_data->[8]) ) ? $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}}[1]{tracks} = [] : do {
+          (
+            (! $collapse_idx[5]{$cur_row_ids{1}}{$cur_row_ids{6}}{$cur_row_ids{8}} )
+              and
+            push @{ $collapse_idx[4]{$cur_row_ids{1}}{$cur_row_ids{6}}[1]{tracks} }, (
+              $collapse_idx[5]{$cur_row_ids{1}}{$cur_row_ids{6}}{$cur_row_ids{8}} = [{ title => $cur_row_data->[8] }]
+            )
+          ),
+        } ),
+      } ),
 
-      (! defined($cur_row_data->[5]) ) ? $collapse_idx[0]{$cur_row_data->[1]}[1]{tracks} = [] : do {
+      ( (! defined($cur_row_data->[5]) ) ? $collapse_idx[0]{$cur_row_ids{1}}[1]{tracks} = [] : do {
 
-        (! $collapse_idx[6]{$cur_row_data->[1]}{$cur_row_data->[5]} )
-          and
-        push @{ $collapse_idx[0]{$cur_row_data->[1]}[1]{tracks} }, (
-          $collapse_idx[6]{$cur_row_data->[1]}{$cur_row_data->[5]} = [{ title => $cur_row_data->[5] }]
-        );
+        (
+          (! $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}} )
+            and
+          push @{ $collapse_idx[0]{$cur_row_ids{1}}[1]{tracks} }, (
+            $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}} = [{ title => $cur_row_data->[5] }]
+          )
+        ),
 
-        (! defined($cur_row_data->[10]) ) ? $collapse_idx[6]{$cur_row_data->[1]}{$cur_row_data->[5]}[1]{lyrics} = [] : do {
+        ( (! defined($cur_row_data->[10]) ) ? $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}}[1]{lyrics} = [] : do {
 
-          $collapse_idx[6]{$cur_row_data->[1]}{$cur_row_data->[5]}[1]{lyrics} //= $collapse_idx[7]{$cur_row_data->[1]}{$cur_row_data->[5]}{$cur_row_data->[10]} = [];
+          ( $collapse_idx[6]{$cur_row_ids{1}}{$cur_row_ids{5}}[1]{lyrics} //= $collapse_idx[7]{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} = [] ),
 
-          (! $collapse_idx[8]{$cur_row_data->[0]}{$cur_row_data->[1]}{$cur_row_data->[5]}{$cur_row_data->[10]} )
-            and
-          push @{ $collapse_idx[7]{$cur_row_data->[1]}{$cur_row_data->[5]}{$cur_row_data->[10]}[1]{existing_lyric_versions} }, (
-            $collapse_idx[8]{$cur_row_data->[0]}{$cur_row_data->[1]}{$cur_row_data->[5]}{$cur_row_data->[10]} = [{ lyric_id => $cur_row_data->[10], text => $cur_row_data->[0] }]
-          );
-        };
-      };
+          (
+            (! $collapse_idx[8]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} )
+              and
+            push @{ $collapse_idx[7]{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}}[1]{existing_lyric_versions} }, (
+              $collapse_idx[8]{$cur_row_ids{0}}{$cur_row_ids{1}}{$cur_row_ids{5}}{$cur_row_ids{10}} = [{ lyric_id => $cur_row_data->[10], text => $cur_row_data->[0] }]
+            )
+          ),
+        } ),
+      } ),
     }
 
     $#{$_[0]} = $result_pos - 1;
@@ -609,58 +673,74 @@ is_same_src (
     my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids);
 
     while ($cur_row_data = (
-      ( $rows_pos >= 0 and $_[0][$rows_pos++] )
-        ||
-      ( $_[1] and $rows_pos = -1 and $_[1]->() )
+      (
+        $rows_pos >= 0
+          and
+        (
+          $_[0][$rows_pos++]
+            or
+          ( ($rows_pos = -1), undef )
+        )
+      )
+        or
+      ( $_[1] and $_[1]->() )
     ) ) {
 
-      $cur_row_ids{0} = $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0";
-      $cur_row_ids{2} = $cur_row_data->[2] // "\0NULL\xFF$rows_pos\xFF2\0";
-      $cur_row_ids{3} = $cur_row_data->[3] // "\0NULL\xFF$rows_pos\xFF3\0";
-      $cur_row_ids{4} = $cur_row_data->[4] // "\0NULL\xFF$rows_pos\xFF4\0";
-      $cur_row_ids{8} = $cur_row_data->[8] // "\0NULL\xFF$rows_pos\xFF8\0";
+      ( @cur_row_ids{( 0, 2, 3, 4, 8 )} = (
+        $cur_row_data->[0] // "\0NULL\xFF$rows_pos\xFF0\0",
+        $cur_row_data->[2] // "\0NULL\xFF$rows_pos\xFF2\0",
+        $cur_row_data->[3] // "\0NULL\xFF$rows_pos\xFF3\0",
+        $cur_row_data->[4] // "\0NULL\xFF$rows_pos\xFF4\0",
+        $cur_row_data->[8] // "\0NULL\xFF$rows_pos\xFF8\0",
+      )),
 
       # cache expensive set of ops in a non-existent rowid slot
-      $cur_row_ids{10} = (
-        ( ( defined $cur_row_data->[0] ) && (join "\xFF", q{}, $cur_row_data->[0], q{} ))
+      ( $cur_row_ids{10} = (
+        ( ( defined $cur_row_data->[0] ) && (join "\xFF", q{}, $cur_row_ids{0}, q{} ))
           or
-        ( ( defined $cur_row_data->[2] ) && (join "\xFF", q{}, $cur_row_data->[2], q{} ))
+        ( ( defined $cur_row_data->[2] ) && (join "\xFF", q{}, $cur_row_ids{2}, q{} ))
           or
         "\0$rows_pos\0"
-      );
+      )),
 
       # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
-      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{10}} and (unshift @{$_[2]}, $cur_row_data) and last;
+      ( $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{10}} and (unshift @{$_[2]}, $cur_row_data) and last ),
 
-      $collapse_idx[0]{$cur_row_ids{10}} //= $_[0][$result_pos++] = [{ year => $$cur_row_data[1] }];
+      ( $collapse_idx[0]{$cur_row_ids{10}} //= $_[0][$result_pos++] = [{ year => $$cur_row_data[1] }] ),
 
-      $collapse_idx[0]{$cur_row_ids{10}}[1]{single_track} //= ($collapse_idx[1]{$cur_row_ids{0}} = [{ trackid => $cur_row_data->[0] }]);
-      defined($cur_row_data->[0]) or bless ( $collapse_idx[0]{$cur_row_ids{10}}[1]{single_track}, __NBC__ );
+      ( $collapse_idx[0]{$cur_row_ids{10}}[1]{single_track} //= ($collapse_idx[1]{$cur_row_ids{0}} = [{ trackid => $cur_row_data->[0] }]) ),
+      ( defined($cur_row_data->[0]) or bless ( $collapse_idx[0]{$cur_row_ids{10}}[1]{single_track}, __NBC__ ) ),
 
-      $collapse_idx[1]{$cur_row_ids{0}}[1]{cd} //= $collapse_idx[2]{$cur_row_ids{0}} = [];
+      ( $collapse_idx[1]{$cur_row_ids{0}}[1]{cd} //= $collapse_idx[2]{$cur_row_ids{0}} = [] ),
 
-      $collapse_idx[2]{$cur_row_ids{0}}[1]{artist} //= ($collapse_idx[3]{$cur_row_ids{0}} = [{ artistid => $cur_row_data->[6] }]);
+      ( $collapse_idx[2]{$cur_row_ids{0}}[1]{artist} //= ($collapse_idx[3]{$cur_row_ids{0}} = [{ artistid => $cur_row_data->[6] }]) ),
 
-      (! $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}} )
-        and
-      push @{$collapse_idx[3]{$cur_row_ids{0}}[1]{cds}}, (
+      (
+        (! $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}} )
+          and
+        push @{$collapse_idx[3]{$cur_row_ids{0}}[1]{cds}}, (
           $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}} = [{ cdid => $cur_row_data->[4], genreid => $cur_row_data->[7], year => $cur_row_data->[5] }]
-      );
-      defined($cur_row_data->[4]) or bless ( $collapse_idx[3]{$cur_row_ids{0}}[1]{cds}, __NBC__ );
-
-      (! $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{4}}{$cur_row_ids{8}} )
-        and
-      push @{$collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}}[1]{tracks}}, (
-          $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{4}}{$cur_row_ids{8}} = [{ title => $cur_row_data->[8] }]
-      );
-      defined($cur_row_data->[8]) or bless ( $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}}[1]{tracks}, __NBC__ );
-
-      (! $collapse_idx[6]{$cur_row_ids{2}}{$cur_row_ids{3}} )
-        and
-      push @{$collapse_idx[0]{$cur_row_ids{10}}[1]{tracks}}, (
+        )
+      ),
+      ( defined($cur_row_data->[4]) or bless ( $collapse_idx[3]{$cur_row_ids{0}}[1]{cds}, __NBC__ ) ),
+
+      (
+        (! $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{4}}{$cur_row_ids{8}} )
+          and
+        push @{$collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}}[1]{tracks}}, (
+            $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{4}}{$cur_row_ids{8}} = [{ title => $cur_row_data->[8] }]
+        )
+      ),
+      ( defined($cur_row_data->[8]) or bless ( $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}}[1]{tracks}, __NBC__ ) ),
+
+      (
+        (! $collapse_idx[6]{$cur_row_ids{2}}{$cur_row_ids{3}} )
+          and
+        push @{$collapse_idx[0]{$cur_row_ids{10}}[1]{tracks}}, (
           $collapse_idx[6]{$cur_row_ids{2}}{$cur_row_ids{3}} = [{ cd => $$cur_row_data[2], title => $cur_row_data->[3] }]
-      );
-      defined($cur_row_data->[2]) or bless ( $collapse_idx[0]{$cur_row_ids{10}}[1]{tracks}, __NBC__ );
+        )
+      ),
+      ( defined($cur_row_data->[2]) or bless ( $collapse_idx[0]{$cur_row_ids{10}}[1]{tracks}, __NBC__ ) ),
     }
 
     $#{$_[0]} = $result_pos - 1;
@@ -679,66 +759,74 @@ is_same_src (
     my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids);
 
     while ($cur_row_data = (
-      ( $rows_pos >= 0 and $_[0][$rows_pos++] )
-        ||
-      ( $_[1] and $rows_pos = -1 and $_[1]->() )
+      (
+        $rows_pos >= 0
+          and
+        (
+          $_[0][$rows_pos++]
+            or
+          ( ($rows_pos = -1), undef )
+        )
+      )
+        or
+      ( $_[1] and $_[1]->() )
     ) ) {
 
       # do not care about nullability here
-      $cur_row_ids{0} = $cur_row_data->[0];
-      $cur_row_ids{2} = $cur_row_data->[2];
-      $cur_row_ids{3} = $cur_row_data->[3];
-      $cur_row_ids{4} = $cur_row_data->[4];
-      $cur_row_ids{8} = $cur_row_data->[8];
+      ( @cur_row_ids{( 0, 2, 3, 4, 8 )} = @{$cur_row_data}[( 0, 2, 3, 4, 8 )] ),
 
       # cache expensive set of ops in a non-existent rowid slot
-      $cur_row_ids{10} = (
-        ( ( defined $cur_row_data->[0] ) && (join "\xFF", q{}, $cur_row_data->[0], q{} ))
+      ( $cur_row_ids{10} = (
+        ( ( defined $cur_row_data->[0] ) && (join "\xFF", q{}, $cur_row_ids{0}, q{} ))
           or
-        ( ( defined $cur_row_data->[2] ) && (join "\xFF", q{}, $cur_row_data->[2], q{} ))
+        ( ( defined $cur_row_data->[2] ) && (join "\xFF", q{}, $cur_row_ids{2}, q{} ))
           or
         "\0$rows_pos\0"
-      );
+      )),
 
       # a present cref in $_[1] implies lazy prefetch, implies a supplied stash in $_[2]
-      $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{10}} and (unshift @{$_[2]}, $cur_row_data) and last;
+      ( $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{10}} and (unshift @{$_[2]}, $cur_row_data) and last ),
 
-      $collapse_idx[0]{$cur_row_ids{10}} //= $_[0][$result_pos++] = { year => $$cur_row_data[1] };
+      ( $collapse_idx[0]{$cur_row_ids{10}} //= $_[0][$result_pos++] = { year => $$cur_row_data[1] } ),
 
-      (! defined $cur_row_data->[0] ) ? $collapse_idx[0]{$cur_row_ids{10}}{single_track} = undef : do {
+      ( (! defined $cur_row_data->[0] ) ? $collapse_idx[0]{$cur_row_ids{10}}{single_track} = undef : do {
 
-        $collapse_idx[0]{$cur_row_ids{10}}{single_track} //= ($collapse_idx[1]{$cur_row_ids{0}} = { trackid => $$cur_row_data[0] });
+        ( $collapse_idx[0]{$cur_row_ids{10}}{single_track} //= ($collapse_idx[1]{$cur_row_ids{0}} = { trackid => $$cur_row_data[0] }) ),
 
-        $collapse_idx[1]{$cur_row_ids{0}}{cd} //= $collapse_idx[2]{$cur_row_ids{0}};
+        ( $collapse_idx[1]{$cur_row_ids{0}}{cd} //= $collapse_idx[2]{$cur_row_ids{0}} ),
 
-        $collapse_idx[2]{$cur_row_ids{0}}{artist} //= ($collapse_idx[3]{$cur_row_ids{0}} = { artistid => $$cur_row_data[6] });
+        ( $collapse_idx[2]{$cur_row_ids{0}}{artist} //= ($collapse_idx[3]{$cur_row_ids{0}} = { artistid => $$cur_row_data[6] }) ),
 
-        (! defined $cur_row_data->[4] ) ? $collapse_idx[3]{$cur_row_ids{0}}{cds} = [] : do {
+        ( (! defined $cur_row_data->[4] ) ? $collapse_idx[3]{$cur_row_ids{0}}{cds} = [] : do {
 
-          (! $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}} )
-            and
-          push @{$collapse_idx[3]{$cur_row_ids{0}}{cds}}, (
-              $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}} = { cdid => $$cur_row_data[4], genreid => $$cur_row_data[7], year => $$cur_row_data[5] }
-          );
+          (
+            (! $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}} )
+              and
+            push @{$collapse_idx[3]{$cur_row_ids{0}}{cds}}, (
+                $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}} = { cdid => $$cur_row_data[4], genreid => $$cur_row_data[7], year => $$cur_row_data[5] }
+            )
+          ),
 
-          (! defined $cur_row_data->[8] ) ? $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}}{tracks} = [] : do {
+          ( (! defined $cur_row_data->[8] ) ? $collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}}{tracks} = [] : do {
 
             (! $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{4}}{$cur_row_ids{8}} )
               and
             push @{$collapse_idx[4]{$cur_row_ids{0}}{$cur_row_ids{4}}{tracks}}, (
                 $collapse_idx[5]{$cur_row_ids{0}}{$cur_row_ids{4}}{$cur_row_ids{8}} = { title => $$cur_row_data[8] }
-            );
-          };
-        };
-      };
-
-      (! defined $cur_row_data->[2] ) ? $collapse_idx[0]{$cur_row_ids{10}}{tracks} = [] : do {
-        (! $collapse_idx[6]{$cur_row_ids{2}}{$cur_row_ids{3}} )
-          and
-        push @{$collapse_idx[0]{$cur_row_ids{10}}{tracks}}, (
+            ),
+          } ),
+        } ),
+      } ),
+
+      ( (! defined $cur_row_data->[2] ) ? $collapse_idx[0]{$cur_row_ids{10}}{tracks} = [] : do {
+        (
+          (! $collapse_idx[6]{$cur_row_ids{2}}{$cur_row_ids{3}} )
+            and
+          push @{$collapse_idx[0]{$cur_row_ids{10}}{tracks}}, (
             $collapse_idx[6]{$cur_row_ids{2}}{$cur_row_ids{3}} = { cd => $$cur_row_data[2], title => $$cur_row_data[3] }
-        );
-      };
+          )
+        ),
+      } ),
     }
 
     $#{$_[0]} = $result_pos - 1;
@@ -746,21 +834,74 @@ is_same_src (
   'Multiple has_many on multiple branches with underdefined root, HRI-direct torture test',
 );
 
+is_same_src (
+  ($schema->source ('Owners')->_mk_row_parser({
+    inflate_map => [qw( books.title books.owner )],
+    collapse => 1,
+    prune_null_branches => 1,
+  }))[0],
+  ' my $rows_pos = 0;
+    my ($result_pos, @collapse_idx, $cur_row_data, %cur_row_ids );
+
+    while ($cur_row_data = (
+      (
+        $rows_pos >= 0
+          and
+        (
+          $_[0][$rows_pos++]
+            or
+          ( ($rows_pos = -1), undef )
+        )
+      )
+        or
+      ( $_[1] and $_[1]->() )
+    ) ) {
+
+      ( @cur_row_ids{0,1} = @{$cur_row_data}[0,1] ),
+
+      ( $cur_row_ids{3} = (
+        ( ( defined $cur_row_data->[1] ) && (join "\xFF", q{}, $cur_row_ids{1}, q{} ))
+          or
+        "\0${rows_pos}\0"
+      )),
+
+      ( $_[1] and $result_pos and ! $collapse_idx[0]{$cur_row_ids{3}} and (unshift @{$_[2]}, $cur_row_data) and last ),
+
+      # empty data for the root node
+      ( $collapse_idx[0]{$cur_row_ids{3}} //= $_[0][$result_pos++] = [] ),
+
+      ( ( ! defined $cur_row_data->[0] ) ? $collapse_idx[0]{$cur_row_ids{3}}[1]{"books"} = [] : do {
+        ( ! $collapse_idx[1]{$cur_row_ids{0}} )
+          and
+        push @{$collapse_idx[0]{$cur_row_ids{3}}[1]{books}},
+          $collapse_idx[1]{$cur_row_ids{0}} = [ { owner => $cur_row_data->[1], title => $cur_row_data->[0] } ]
+      } ),
+    }
+
+    $#{$_[0]} = $result_pos - 1; # truncate the passed in array to where we filled it with results
+  ',
+  'Non-premultiplied implicit collapse with missing join columns',
+);
+
 done_testing;
 
 my $deparser;
 sub is_same_src { SKIP: {
+
+  skip "Skipping comparison of unicode-posioned source", 1
+    if DBIx::Class::_ENV_::STRESSTEST_UTF8_UPGRADE_GENERATED_COLLAPSER_SOURCE;
+
   $deparser ||= B::Deparse->new;
   local $Test::Builder::Level = $Test::Builder::Level + 1;
 
   my ($got, $expect) = @_;
 
   skip "Not testing equality of source containing defined-or operator on this perl $]", 1
-    if ($] < 5.010 and$expect =~ m!\Q//=!);
+    if ( "$]" < 5.010 and $expect =~ m!\Q//=! );
 
-  $expect =~ s/__NBC__/B::perlstring($DBIx::Class::ResultSource::RowParser::Util::null_branch_class)/ge;
+  $expect =~ s/__NBC__/perlstring($DBIx::Class::ResultSource::RowParser::Util::null_branch_class)/ge;
 
-  $expect = "  { use strict; use warnings FATAL => 'all';\n$expect\n  }";
+  $expect = "  { use strict; use warnings FATAL => 'uninitialized';\n$expect\n  }";
 
   my @normalized = map {
     my $cref = eval "sub { $_ }" or do {
index 3314b88..30e3797 100644 (file)
@@ -5,6 +5,13 @@ use lib qw(t/lib);
 use Test::More;
 use Test::Exception;
 
+# MASSIVE FIXME - there is a hole in ::RSC / as_subselect_rs
+# losing the order. Needs a rework/extract of the realiaser,
+# and that's a whole another bag of dicks
+BEGIN { $ENV{DBIC_SHUFFLE_UNORDERED_RESULTSETS} = 0 }
+
+use DBIx::Class::_Util 'scope_guard';
+
 use DBICTest::Schema::CD;
 BEGIN {
   # the default scalarref table name will not work well for this test
@@ -12,16 +19,9 @@ BEGIN {
 }
 
 use DBICTest;
-use DBIC::DebugObj;
-use DBIC::SqlMakerTest;
 
 my $schema = DBICTest->init_schema;
 
-my ($sql, @bind);
-my $debugobj = DBIC::DebugObj->new (\$sql, \@bind);
-my $orig_debugobj = $schema->storage->debugobj;
-my $orig_debug = $schema->storage->debug;
-
 my $tkfks = $schema->resultset('FourKeys_to_TwoKeys');
 
 my ($fa, $fb, $fc) = $tkfks->related_resultset ('fourkeys')->populate ([
@@ -38,7 +38,7 @@ my ($fa, $fb, $fc) = $tkfks->related_resultset ('fourkeys')->populate ([
 #  [qw/2       2  /],
 #]);
 my ($ta, $tb) = $schema->resultset ('TwoKeys')
-                  ->search ( [ { artist => 1, cd => 1 }, { artist => 2, cd => 2 } ])
+                  ->search ( [ { artist => 1, cd => 1 }, { artist => 2, cd => 2 } ], { order_by => 'artist' })
                     ->all;
 
 my $tkfk_cnt = $tkfks->count;
@@ -64,64 +64,95 @@ my $fks = $schema->resultset ('FourKeys')->search (
 );
 
 is ($fks->count, 4, 'Joined FourKey count correct (2x2)');
-
-$schema->storage->debugobj ($debugobj);
-$schema->storage->debug (1);
-$fks->update ({ read_count => \ 'read_count + 1' });
-$schema->storage->debugobj ($orig_debugobj);
-$schema->storage->debug ($orig_debug);
-
-is_same_sql_bind (
-  $sql,
-  \@bind,
+$schema->is_executed_sql_bind( sub {
+  $fks->update ({ read_count => \ 'read_count + 1' })
+}, [[
   'UPDATE fourkeys
    SET read_count = read_count + 1
    WHERE ( ( ( bar = ? OR bar = ? ) AND ( foo = ? OR foo = ? ) AND ( goodbye = ? OR goodbye = ? ) AND ( hello = ? OR hello = ? ) AND sensors != ? ) )
   ',
-  [ ("'1'", "'2'") x 4, "'c'" ],
-  'Correct update-SQL with multijoin with pruning',
-);
+  (1, 2) x 4,
+  'c',
+]], 'Correct update-SQL with multijoin with pruning' );
 
 is ($fa->discard_changes->read_count, 11, 'Update ran only once on discard-join resultset');
 is ($fb->discard_changes->read_count, 21, 'Update ran only once on discard-join resultset');
 is ($fc->discard_changes->read_count, 30, 'Update did not touch outlier');
 
 # make the multi-join stick
-my $fks_multi = $fks->search({ 'fourkeys_to_twokeys.pilot_sequence' => { '!=' => 666 } });
-
-$schema->storage->debugobj ($debugobj);
-$schema->storage->debug (1);
-$fks_multi->update ({ read_count => \ 'read_count + 1' });
-$schema->storage->debugobj ($orig_debugobj);
-$schema->storage->debug ($orig_debug);
-
-is_same_sql_bind (
-  $sql,
-  \@bind,
-  'UPDATE fourkeys
-   SET read_count = read_count + 1
-   WHERE ( bar = ? AND foo = ? AND goodbye = ? AND hello = ? ) OR ( bar = ? AND foo = ? AND goodbye = ? AND hello = ? )',
-  [ map { "'$_'" } ( (1) x 4, (2) x 4 ) ],
-  'Correct update-SQL with multijoin without pruning',
+my $fks_multi = $fks->search(
+  { 'fourkeys_to_twokeys.pilot_sequence' => { '!=' => 666 } },
+  { order_by => [ $fks->result_source->primary_columns ] },
 );
+$schema->is_executed_sql_bind( sub {
+  $fks_multi->update ({ read_count => \ 'read_count + 1' })
+}, [
+  [ 'BEGIN' ],
+  [
+    'SELECT me.foo, me.bar, me.hello, me.goodbye
+      FROM fourkeys me
+      LEFT JOIN fourkeys_to_twokeys fourkeys_to_twokeys
+        ON fourkeys_to_twokeys.f_bar = me.bar AND fourkeys_to_twokeys.f_foo = me.foo AND fourkeys_to_twokeys.f_goodbye = me.goodbye AND fourkeys_to_twokeys.f_hello = me.hello
+      WHERE ( bar = ? OR bar = ? ) AND ( foo = ? OR foo = ? ) AND fourkeys_to_twokeys.pilot_sequence != ? AND ( goodbye = ? OR goodbye = ? ) AND ( hello = ? OR hello = ? ) AND sensors != ?
+      GROUP BY me.foo, me.bar, me.hello, me.goodbye
+      ORDER BY foo, bar, hello, goodbye
+    ',
+    (1, 2) x 2,
+    666,
+    (1, 2) x 2,
+    'c',
+  ],
+  [
+    'UPDATE fourkeys
+     SET read_count = read_count + 1
+     WHERE ( bar = ? AND foo = ? AND goodbye = ? AND hello = ? ) OR ( bar = ? AND foo = ? AND goodbye = ? AND hello = ? )
+    ',
+    ( (1) x 4, (2) x 4 ),
+  ],
+  [ 'COMMIT' ],
+], 'Correct update-SQL with multijoin without pruning' );
 
 is ($fa->discard_changes->read_count, 12, 'Update ran only once on joined resultset');
 is ($fb->discard_changes->read_count, 22, 'Update ran only once on joined resultset');
 is ($fc->discard_changes->read_count, 30, 'Update did not touch outlier');
 
+$schema->is_executed_sql_bind( sub {
+  my $res = $fks_multi->search (\' "blah" = "bleh" ')->delete;
+  ok ($res, 'operation is true');
+  cmp_ok ($res, '==', 0, 'zero rows affected');
+}, [
+  [ 'BEGIN' ],
+  [
+    'SELECT me.foo, me.bar, me.hello, me.goodbye
+      FROM fourkeys me
+      LEFT JOIN fourkeys_to_twokeys fourkeys_to_twokeys
+        ON fourkeys_to_twokeys.f_bar = me.bar AND fourkeys_to_twokeys.f_foo = me.foo AND fourkeys_to_twokeys.f_goodbye = me.goodbye AND fourkeys_to_twokeys.f_hello = me.hello
+      WHERE "blah" = "bleh" AND ( bar = ? OR bar = ? ) AND ( foo = ? OR foo = ? ) AND fourkeys_to_twokeys.pilot_sequence != ? AND ( goodbye = ? OR goodbye = ? ) AND ( hello = ? OR hello = ? ) AND sensors != ?
+      GROUP BY me.foo, me.bar, me.hello, me.goodbye
+      ORDER BY foo, bar, hello, goodbye
+    ',
+    (1, 2) x 2,
+    666,
+    (1, 2) x 2,
+    'c',
+  ],
+  [ 'COMMIT' ],
+], 'Correct null-delete-SQL with multijoin without pruning' );
+
+
 # try the same sql with forced multicolumn in
-$schema->storage->_use_multicolumn_in (1);
-$schema->storage->debugobj ($debugobj);
-$schema->storage->debug (1);
-throws_ok { $fks_multi->update ({ read_count => \ 'read_count + 1' }) } # this can't actually execute, we just need the "as_query"
-  qr/\QDBI Exception:/ or do { $sql = ''; @bind = () };
-$schema->storage->_use_multicolumn_in (undef);
-$schema->storage->debugobj ($orig_debugobj);
-$schema->storage->debug ($orig_debug);
-
-is_same_sql_bind (
-  $sql,
-  \@bind,
+$schema->is_executed_sql_bind( sub {
+
+  my $orig_umi = $schema->storage->_use_multicolumn_in;
+  my $sg = scope_guard {
+    $schema->storage->_use_multicolumn_in($orig_umi);
+  };
+
+  $schema->storage->_use_multicolumn_in(1);
+
+  # this can't actually execute on sqlite
+  eval { $fks_multi->update ({ read_count => \ 'read_count + 1' }) };
+}, [[
   'UPDATE fourkeys
     SET read_count = read_count + 1
     WHERE (
@@ -133,42 +164,49 @@ is_same_sql_bind (
             AND fourkeys_to_twokeys.f_foo = me.foo
             AND fourkeys_to_twokeys.f_goodbye = me.goodbye
             AND fourkeys_to_twokeys.f_hello = me.hello
-        WHERE fourkeys_to_twokeys.pilot_sequence != ? AND ( bar = ? OR bar = ? ) AND ( foo = ? OR foo = ? ) AND ( goodbye = ? OR goodbye = ? ) AND ( hello = ? OR hello = ? ) AND sensors != ?
+        WHERE ( bar = ? OR bar = ? ) AND ( foo = ? OR foo = ? ) AND fourkeys_to_twokeys.pilot_sequence != ? AND ( goodbye = ? OR goodbye = ? ) AND ( hello = ? OR hello = ? ) AND sensors != ?
+        ORDER BY foo, bar, hello, goodbye
       )
     )
   ',
+  ( 1, 2) x 2,
+  666,
+  ( 1, 2) x 2,
+  'c',
+]], 'Correct update-SQL with multicolumn in support' );
+
+$schema->is_executed_sql_bind( sub {
+  $fks->search({ 'twokeys.artist' => { '!=' => 666 } })->update({ read_count => \ 'read_count + 1' });
+}, [
+  [ 'BEGIN' ],
   [
-    "'666'",
-    ("'1'", "'2'") x 4,
-    "'c'",
+    'SELECT me.foo, me.bar, me.hello, me.goodbye
+      FROM fourkeys me
+      LEFT JOIN fourkeys_to_twokeys fourkeys_to_twokeys
+        ON fourkeys_to_twokeys.f_bar = me.bar AND fourkeys_to_twokeys.f_foo = me.foo AND fourkeys_to_twokeys.f_goodbye = me.goodbye AND fourkeys_to_twokeys.f_hello = me.hello
+      LEFT JOIN twokeys twokeys
+        ON twokeys.artist = fourkeys_to_twokeys.t_artist AND twokeys.cd = fourkeys_to_twokeys.t_cd
+      WHERE ( bar = ? OR bar = ? ) AND ( foo = ? OR foo = ? ) AND ( goodbye = ? OR goodbye = ? ) AND ( hello = ? OR hello = ? ) AND sensors != ? AND twokeys.artist != ?
+      GROUP BY me.foo, me.bar, me.hello, me.goodbye
+    ',
+    (1, 2) x 4,
+    'c',
+    666,
   ],
-  'Correct update-SQL with multicolumn in support',
-);
-
-# make a *premultiplied* join stick
-my $fks_premulti = $fks->search({ 'twokeys.artist' => { '!=' => 666 } });
-
-$schema->storage->debugobj ($debugobj);
-$schema->storage->debug (1);
-$fks_premulti->update ({ read_count => \ 'read_count + 1' });
-$schema->storage->debugobj ($orig_debugobj);
-$schema->storage->debug ($orig_debug);
-
-is_same_sql_bind (
-  $sql,
-  \@bind,
-  'UPDATE fourkeys
-   SET read_count = read_count + 1
-   WHERE ( bar = ? AND foo = ? AND goodbye = ? AND hello = ? ) OR ( bar = ? AND foo = ? AND goodbye = ? AND hello = ? )',
-  [ map { "'$_'" } ( (1) x 4, (2) x 4 ) ],
-  'Correct update-SQL with premultiplied restricting join without pruning',
-);
+  [
+    'UPDATE fourkeys
+     SET read_count = read_count + 1
+     WHERE ( bar = ? AND foo = ? AND goodbye = ? AND hello = ? ) OR ( bar = ? AND foo = ? AND goodbye = ? AND hello = ? )
+    ',
+    ( (1) x 4, (2) x 4 ),
+  ],
+  [ 'COMMIT' ],
+], 'Correct update-SQL with premultiplied restricting join without pruning' );
 
 is ($fa->discard_changes->read_count, 13, 'Update ran only once on joined resultset');
 is ($fb->discard_changes->read_count, 23, 'Update ran only once on joined resultset');
 is ($fc->discard_changes->read_count, 30, 'Update did not touch outlier');
 
-
 #
 # Make sure multicolumn in or the equivalent functions correctly
 #
@@ -250,45 +288,40 @@ cmp_ok ($tkfk_cnt, '>', 1, 'More than 1 row left');
 $tkfks->search ({}, { rows => 1 })->delete;
 is ($tkfks->count, $tkfk_cnt -= 1, 'Only one row deleted');
 
+throws_ok {
+  $tkfks->search ({}, { rows => 0 })->delete
+} qr/rows attribute must be a positive integer/;
+is ($tkfks->count, $tkfk_cnt, 'Nothing deleted');
 
 # check with sql-equality, as sqlite will accept most bad sql just fine
-$schema->storage->debugobj ($debugobj);
-$schema->storage->debug (1);
-
 {
   my $rs = $schema->resultset('CD')->search(
     { 'me.year' => { '!=' => 2010 } },
   );
 
-  $rs->search({}, { join => 'liner_notes' })->delete;
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    $rs->search({}, { join => 'liner_notes' })->delete;
+  }, [[
     'DELETE FROM cd WHERE ( year != ? )',
-    ["'2010'"],
-    'Non-restricting multijoins properly thrown out'
-  );
+    2010,
+  ]], 'Non-restricting multijoins properly thrown out' );
 
-  $rs->search({}, { prefetch => 'liner_notes' })->delete;
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    $rs->search({}, { prefetch => 'liner_notes' })->delete;
+  }, [[
     'DELETE FROM cd WHERE ( year != ? )',
-    ["'2010'"],
-    'Non-restricting multiprefetch thrown out'
-  );
+    2010,
+  ]], 'Non-restricting multiprefetch thrown out' );
 
-  $rs->search({}, { prefetch => 'artist' })->delete;
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    $rs->search({}, { prefetch => 'artist' })->delete;
+  }, [[
     'DELETE FROM cd WHERE ( cdid IN ( SELECT me.cdid FROM cd me JOIN artist artist ON artist.artistid = me.artist WHERE ( me.year != ? ) ) )',
-    ["'2010'"],
-    'Restricting prefetch left in, selector thrown out'
-  );
+    2010,
+  ]], 'Restricting prefetch left in, selector thrown out');
 
-  # switch artist and cd to fully qualified table names
-  # make sure nothing is stripped out
+### switch artist and cd to fully qualified table names
+### make sure nothing is stripped out
   my $cd_rsrc = $schema->source('CD');
   $cd_rsrc->name('main.cd');
   $cd_rsrc->relationship_info($_)->{attrs}{cascade_delete} = 0
@@ -299,85 +332,80 @@ $schema->storage->debug (1);
   $art_rsrc->relationship_info($_)->{attrs}{cascade_delete} = 0
     for $art_rsrc->relationships;
 
-  $rs->delete;
-  is_same_sql_bind (
-    $sql,
-    \@bind,
-    'DELETE FROM main.cd WHERE ( year != ? )',
-    ["'2010'"],
-    'delete with fully qualified table name'
-  );
+  $schema->is_executed_sql_bind( sub {
+    $rs->delete
+  }, [[
+    'DELETE FROM main.cd WHERE year != ?',
+    2010,
+  ]], 'delete with fully qualified table name' );
 
   $rs->create({ title => 'foo', artist => 1, year => 2000 });
-  $rs->delete_all;
-  is_same_sql_bind (
-    $sql,
-    \@bind,
-    'DELETE FROM main.cd WHERE ( cdid = ? )',
-    ["'1'"],
-    'delete_all with fully qualified table name'
-  );
-
-  $rs->create({ cdid => 42, title => 'foo', artist => 2, year => 2000 });
-  $rs->find(42)->delete;
-  is_same_sql_bind (
-    $sql,
-    \@bind,
-    'DELETE FROM main.cd WHERE ( cdid = ? )',
-    ["'42'"],
-    'delete of object from table with fully qualified name'
-  );
+  $schema->is_executed_sql_bind( sub {
+    $rs->delete_all
+  }, [
+    [ 'BEGIN' ],
+    [
+      'SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM main.cd me WHERE me.year != ?',
+      2010,
+    ],
+    [
+      'DELETE FROM main.cd WHERE ( cdid = ? )',
+      1,
+    ],
+    [ 'COMMIT' ],
+  ], 'delete_all with fully qualified table name' );
 
   $rs->create({ cdid => 42, title => 'foo', artist => 2, year => 2000 });
-  $rs->find(42)->related_resultset('artist')->delete;
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  my $cd42 = $rs->find(42);
+
+  $schema->is_executed_sql_bind( sub {
+    $cd42->delete
+  }, [[
+    'DELETE FROM main.cd WHERE cdid = ?',
+    42,
+  ]], 'delete of object from table with fully qualified name' );
+
+  $schema->is_executed_sql_bind( sub {
+    $cd42->related_resultset('artist')->delete
+  }, [[
     'DELETE FROM main.artist WHERE ( artistid IN ( SELECT me.artistid FROM main.artist me WHERE ( me.artistid = ? ) ) )',
-    ["'2'"],
-    'delete of related object from scalarref fully qualified named table',
-  );
+    2,
+  ]], 'delete of related object from scalarref fully qualified named table' );
 
-  $schema->resultset('Artist')->find(3)->related_resultset('cds')->delete;
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  my $art3 = $schema->resultset('Artist')->find(3);
+
+  $schema->is_executed_sql_bind( sub {
+    $art3->related_resultset('cds')->delete;
+  }, [[
     'DELETE FROM main.cd WHERE ( artist = ? )',
-    ["'3'"],
-    'delete of related object from fully qualified named table',
-  );
+    3,
+  ]], 'delete of related object from fully qualified named table' );
 
-  $schema->resultset('Artist')->find(3)->cds_unordered->delete;
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    $art3->cds_unordered->delete;
+  }, [[
     'DELETE FROM main.cd WHERE ( artist = ? )',
-    ["'3'"],
-    'delete of related object from fully qualified named table via relaccessor',
-  );
+    3,
+  ]], 'delete of related object from fully qualified named table via relaccessor' );
 
-  $rs->search({}, { prefetch => 'artist' })->delete;
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    $rs->search({}, { prefetch => 'artist' })->delete;
+  }, [[
     'DELETE FROM main.cd WHERE ( cdid IN ( SELECT me.cdid FROM main.cd me JOIN main.artist artist ON artist.artistid = me.artist WHERE ( me.year != ? ) ) )',
-    ["'2010'"],
-    'delete with fully qualified table name and subquery correct'
-  );
+    2010,
+  ]], 'delete with fully qualified table name and subquery correct' );
 
   # check that as_subselect_rs works ok
   # inner query is untouched, then a selector
   # and an IN condition
-  $schema->resultset('CD')->search({
-    'me.cdid' => 1,
-    'artist.name' => 'partytimecity',
-  }, {
-    join => 'artist',
-  })->as_subselect_rs->delete;
-
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    $schema->resultset('CD')->search({
+      'me.cdid' => 1,
+      'artist.name' => 'partytimecity',
+    }, {
+      join => 'artist',
+    })->as_subselect_rs->delete;
+  }, [[
     '
       DELETE FROM main.cd
       WHERE (
@@ -392,12 +420,9 @@ $schema->storage->debug (1);
         )
       )
     ',
-    ["'partytimecity'", "'1'"],
-    'Delete from as_subselect_rs works correctly'
-  );
+    'partytimecity',
+    1,
+  ]], 'Delete from as_subselect_rs works correctly' );
 }
 
-$schema->storage->debugobj ($orig_debugobj);
-$schema->storage->debug ($orig_debug);
-
 done_testing;
diff --git a/t/resultsource/bare_resultclass_exception.t b/t/resultsource/bare_resultclass_exception.t
new file mode 100644 (file)
index 0000000..6b8d72c
--- /dev/null
@@ -0,0 +1,17 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+
+use lib 't/lib';
+use DBICTest;
+
+{
+  package DBICTest::Foo;
+  use base "DBIx::Class::Core";
+}
+
+throws_ok { DBICTest::Foo->new("urgh") } qr/must be a hashref/;
+
+done_testing;
diff --git a/t/row/copy_with_extra_selection.t b/t/row/copy_with_extra_selection.t
new file mode 100644 (file)
index 0000000..c1e3df4
--- /dev/null
@@ -0,0 +1,31 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib qw(t/lib);
+use DBICTest;
+
+my $schema = DBICTest->init_schema();
+
+my $cd = $schema->resultset('CD')->search({}, {
+  '+columns' => { avg_year => $schema->resultset('CD')->get_column('year')->func_rs('avg')->as_query },
+  order_by => 'cdid',
+})->next;
+
+my $ccd = $cd->copy({ cdid => 5_000_000, artist => 2 });
+
+cmp_ok(
+  $ccd->id,
+  '!=',
+  $cd->id,
+  'IDs differ'
+);
+
+is(
+  $ccd->title,
+  $cd->title,
+  'Title same on copied object',
+);
+
+done_testing;
index 4720575..7823fa5 100644 (file)
@@ -8,65 +8,65 @@ use DBICTest;
 
 my $from_storage_ran = 0;
 my $to_storage_ran = 0;
-my $schema = DBICTest->init_schema();
+my $schema = DBICTest->init_schema( no_populate => 1 );
 DBICTest::Schema::Artist->load_components(qw(FilterColumn InflateColumn));
-DBICTest::Schema::Artist->filter_column(rank => {
-  filter_from_storage => sub { $from_storage_ran++; $_[1] * 2 },
-  filter_to_storage   => sub { $to_storage_ran++; $_[1] / 2 },
+DBICTest::Schema::Artist->filter_column(charfield => {
+  filter_from_storage => sub { $from_storage_ran++; defined $_[1] ? $_[1] * 2 : undef },
+  filter_to_storage   => sub { $to_storage_ran++; defined $_[1] ? $_[1] / 2 : undef },
 });
-Class::C3->reinitialize();
+Class::C3->reinitialize() if DBIx::Class::_ENV_::OLD_MRO;
 
-my $artist = $schema->resultset('Artist')->create( { rank => 20 } );
+my $artist = $schema->resultset('Artist')->create( { charfield => 20 } );
 
 # this should be using the cursor directly, no inflation/processing of any sort
-my ($raw_db_rank) = $schema->resultset('Artist')
+my ($raw_db_charfield) = $schema->resultset('Artist')
                              ->search ($artist->ident_condition)
-                               ->get_column('rank')
+                               ->get_column('charfield')
                                 ->_resultset
                                  ->cursor
                                   ->next;
 
-is ($raw_db_rank, 10, 'INSERT: correctly unfiltered on insertion');
+is ($raw_db_charfield, 10, 'INSERT: correctly unfiltered on insertion');
 
 for my $reloaded (0, 1) {
   my $test = $reloaded ? 'reloaded' : 'stored';
   $artist->discard_changes if $reloaded;
 
-  is( $artist->rank , 20, "got $test filtered rank" );
+  is( $artist->charfield , 20, "got $test filtered charfield" );
 }
 
 $artist->update;
 $artist->discard_changes;
-is( $artist->rank , 20, "got filtered rank" );
+is( $artist->charfield , 20, "got filtered charfield" );
 
-$artist->update ({ rank => 40 });
-($raw_db_rank) = $schema->resultset('Artist')
+$artist->update ({ charfield => 40 });
+($raw_db_charfield) = $schema->resultset('Artist')
                              ->search ($artist->ident_condition)
-                               ->get_column('rank')
+                               ->get_column('charfield')
                                 ->_resultset
                                  ->cursor
                                   ->next;
-is ($raw_db_rank, 20, 'UPDATE: correctly unflitered on update');
+is ($raw_db_charfield, 20, 'UPDATE: correctly unflitered on update');
 
 $artist->discard_changes;
-$artist->rank(40);
-ok( !$artist->is_column_changed('rank'), 'column is not dirty after setting the same value' );
+$artist->charfield(40);
+ok( !$artist->is_column_changed('charfield'), 'column is not dirty after setting the same value' );
 
 MC: {
    my $cd = $schema->resultset('CD')->create({
-      artist => { rank => 20 },
+      artist => { charfield => 20 },
       title => 'fun time city!',
       year => 'forevertime',
    });
-   ($raw_db_rank) = $schema->resultset('Artist')
+   ($raw_db_charfield) = $schema->resultset('Artist')
                                 ->search ($cd->artist->ident_condition)
-                                  ->get_column('rank')
+                                  ->get_column('charfield')
                                    ->_resultset
                                     ->cursor
                                      ->next;
 
-   is $raw_db_rank, 10, 'artist rank gets correctly unfiltered w/ MC';
-   is $cd->artist->rank, 20, 'artist rank gets correctly filtered w/ MC';
+   is $raw_db_charfield, 10, 'artist charfield gets correctly unfiltered w/ MC';
+   is $cd->artist->charfield, 20, 'artist charfield gets correctly filtered w/ MC';
 }
 
 CACHE_TEST: {
@@ -79,122 +79,210 @@ CACHE_TEST: {
   is $from_storage_ran, $expected_from, 'from has not run yet';
   is $to_storage_ran, $expected_to, 'to has not run yet';
 
-  $artist->rank;
+  $artist->charfield;
   cmp_ok (
-    $artist->get_filtered_column('rank'),
+    $artist->get_filtered_column('charfield'),
       '!=',
-    $artist->get_column('rank'),
+    $artist->get_column('charfield'),
     'filter/unfilter differ'
   );
   is $from_storage_ran, ++$expected_from, 'from ran once, therefor caches';
   is $to_storage_ran, $expected_to,  'to did not run';
 
-  $artist->rank(6);
+  $artist->charfield(6);
   is $from_storage_ran, $expected_from, 'from did not run';
   is $to_storage_ran, ++$expected_to,  'to ran once';
 
-  ok ($artist->is_column_changed ('rank'), 'Column marked as dirty');
+  ok ($artist->is_column_changed ('charfield'), 'Column marked as dirty');
 
-  $artist->rank;
+  $artist->charfield;
   is $from_storage_ran, $expected_from, 'from did not run';
   is $to_storage_ran, $expected_to,  'to did not run';
 
   $artist->update;
 
-  $artist->set_column(rank => 3);
-  ok (! $artist->is_column_changed ('rank'), 'Column not marked as dirty on same set_column value');
-  is ($artist->rank, '6', 'Column set properly (cache blown)');
+  $artist->set_column(charfield => 3);
+  ok (! $artist->is_column_changed ('charfield'), 'Column not marked as dirty on same set_column value');
+  is ($artist->charfield, '6', 'Column set properly (cache blown)');
   is $from_storage_ran, ++$expected_from, 'from ran once (set_column blew cache)';
   is $to_storage_ran, $expected_to,  'to did not run';
 
-  $artist->rank(6);
-  ok (! $artist->is_column_changed ('rank'), 'Column not marked as dirty on same accessor-set value');
-  is ($artist->rank, '6', 'Column set properly');
+  $artist->charfield(6);
+  ok (! $artist->is_column_changed ('charfield'), 'Column not marked as dirty on same accessor-set value');
+  is ($artist->charfield, '6', 'Column set properly');
   is $from_storage_ran, $expected_from, 'from did not run';
-  is $to_storage_ran, $expected_to,  'to did not run';
+  is $to_storage_ran, ++$expected_to,  'to did run once (call in to set_column)';
 
-  $artist->store_column(rank => 4);
-  ok (! $artist->is_column_changed ('rank'), 'Column not marked as dirty on differing store_column value');
-  is ($artist->rank, '8', 'Cache properly blown');
+  $artist->store_column(charfield => 4);
+  ok (! $artist->is_column_changed ('charfield'), 'Column not marked as dirty on differing store_column value');
+  is ($artist->charfield, '8', 'Cache properly blown');
   is $from_storage_ran, ++$expected_from, 'from did not run';
   is $to_storage_ran, $expected_to,  'to did not run';
+
+  $artist->update({ charfield => undef });
+  is $from_storage_ran, $expected_from, 'from did not run';
+  is $to_storage_ran, ++$expected_to,  'to did run';
+
+  $artist->discard_changes;
+  is ( $artist->get_column('charfield'), undef, 'Got back null' );
+  is ( $artist->charfield, undef, 'Got back null through filter' );
+
+  is $from_storage_ran, ++$expected_from, 'from did run';
+  is $to_storage_ran, $expected_to,  'to did not run';
+
+}
+
+# test in-memory operations
+for my $artist_maker (
+  sub { $schema->resultset('Artist')->new({ charfield => 42 }) },
+  sub { my $art = $schema->resultset('Artist')->new({}); $art->charfield(42); $art },
+) {
+
+  my $expected_from = $from_storage_ran;
+  my $expected_to   = $to_storage_ran;
+
+  my $artist = $artist_maker->();
+
+  is $from_storage_ran, $expected_from, 'from has not run yet';
+  is $to_storage_ran, $expected_to, 'to has not run yet';
+
+  ok( ! $artist->has_column_loaded('artistid'), 'pk not loaded' );
+  ok( $artist->has_column_loaded('charfield'), 'Filtered column marked as loaded under new' );
+  is( $artist->charfield, 42, 'Proper unfiltered value' );
+  is( $artist->get_column('charfield'), 21, 'Proper filtered value' );
+}
+
+# test literals
+for my $v ( \ '16', \[ '?', '16' ] ) {
+  my $rs = $schema->resultset('Artist');
+  $rs->delete;
+
+  my $art = $rs->new({ charfield => 10 });
+  $art->charfield($v);
+
+  is_deeply( $art->charfield, $v);
+  is_deeply( $art->get_filtered_column("charfield"), $v);
+  is_deeply( $art->get_column("charfield"), $v);
+
+  $art->insert;
+  $art->discard_changes;
+
+  is ($art->get_column("charfield"), 16, "Literal inserted into database properly");
+  is ($art->charfield, 32, "filtering still works");
+
+  $art->update({ charfield => $v });
+
+  is_deeply( $art->charfield, $v);
+  is_deeply( $art->get_filtered_column("charfield"), $v);
+  is_deeply( $art->get_column("charfield"), $v);
+
+  $art->discard_changes;
+
+  is ($art->get_column("charfield"), 16, "Literal inserted into database properly");
+  is ($art->charfield, 32, "filtering still works");
 }
 
 IC_DIE: {
-  dies_ok {
-     DBICTest::Schema::Artist->inflate_column(rank =>
+  throws_ok {
+     DBICTest::Schema::Artist->inflate_column(charfield =>
         { inflate => sub {}, deflate => sub {} }
      );
-  } q(Can't inflate column after filter column);
+  } qr/InflateColumn can not be used on a column with a declared FilterColumn filter/, q(Can't inflate column after filter column);
 
   DBICTest::Schema::Artist->inflate_column(name =>
      { inflate => sub {}, deflate => sub {} }
   );
 
-  dies_ok {
+  throws_ok {
      DBICTest::Schema::Artist->filter_column(name => {
         filter_to_storage => sub {},
         filter_from_storage => sub {}
      });
-  } q(Can't filter column after inflate column);
+  } qr/FilterColumn can not be used on a column with a declared InflateColumn inflator/, q(Can't filter column after inflate column);
 }
 
 # test when we do not set both filter_from_storage/filter_to_storage
-DBICTest::Schema::Artist->filter_column(rank => {
+DBICTest::Schema::Artist->filter_column(charfield => {
   filter_to_storage => sub { $to_storage_ran++; $_[1] },
 });
-Class::C3->reinitialize();
+Class::C3->reinitialize() if DBIx::Class::_ENV_::OLD_MRO;
 
 ASYMMETRIC_TO_TEST: {
   # initialise value
-  $artist->rank(20);
+  $artist->charfield(20);
   $artist->update;
 
   my $expected_from = $from_storage_ran;
   my $expected_to   = $to_storage_ran;
 
-  $artist->rank(10);
-  ok ($artist->is_column_changed ('rank'), 'Column marked as dirty on accessor-set value');
-  is ($artist->rank, '10', 'Column set properly');
+  $artist->charfield(10);
+  ok ($artist->is_column_changed ('charfield'), 'Column marked as dirty on accessor-set value');
+  is ($artist->charfield, '10', 'Column set properly');
   is $from_storage_ran, $expected_from, 'from did not run';
   is $to_storage_ran, ++$expected_to,  'to did run';
 
   $artist->discard_changes;
 
-  is ($artist->rank, '20', 'Column set properly');
+  is ($artist->charfield, '20', 'Column set properly');
   is $from_storage_ran, $expected_from, 'from did not run';
   is $to_storage_ran, $expected_to,  'to did not run';
 }
 
-DBICTest::Schema::Artist->filter_column(rank => {
+DBICTest::Schema::Artist->filter_column(charfield => {
   filter_from_storage => sub { $from_storage_ran++; $_[1] },
 });
-Class::C3->reinitialize();
+Class::C3->reinitialize() if DBIx::Class::_ENV_::OLD_MRO;
 
 ASYMMETRIC_FROM_TEST: {
   # initialise value
-  $artist->rank(23);
+  $artist->charfield(23);
   $artist->update;
 
   my $expected_from = $from_storage_ran;
   my $expected_to   = $to_storage_ran;
 
-  $artist->rank(13);
-  ok ($artist->is_column_changed ('rank'), 'Column marked as dirty on accessor-set value');
-  is ($artist->rank, '13', 'Column set properly');
+  $artist->charfield(13);
+  ok ($artist->is_column_changed ('charfield'), 'Column marked as dirty on accessor-set value');
+  is ($artist->charfield, '13', 'Column set properly');
   is $from_storage_ran, $expected_from, 'from did not run';
   is $to_storage_ran, $expected_to,  'to did not run';
 
   $artist->discard_changes;
 
-  is ($artist->rank, '23', 'Column set properly');
+  is ($artist->charfield, '23', 'Column set properly');
   is $from_storage_ran, ++$expected_from, 'from did run';
   is $to_storage_ran, $expected_to,  'to did not run';
 }
 
-throws_ok { DBICTest::Schema::Artist->filter_column( rank => {} ) }
+throws_ok { DBICTest::Schema::Artist->filter_column( charfield => {} ) }
   qr/\QAn invocation of filter_column() must specify either a filter_from_storage or filter_to_storage/,
   'Correctly throws exception for empty attributes'
 ;
 
+FC_ON_PK_TEST: {
+  # there are cases in the wild that autovivify stuff deep in the
+  # colinfo guts. While this is insane, there is no alternative
+  # so at leats make sure it keeps working...
+
+  $schema->source('Artist')->column_info('artistid')->{_filter_info} ||= {};
+
+  for my $key ('', 'primary') {
+    lives_ok {
+      $schema->resultset('Artist')->find_or_create({ artistid => 42 }, { $key ? ( key => $key ) : () });
+    };
+  }
+
+
+  DBICTest::Schema::Artist->filter_column(artistid => {
+    filter_to_storage => sub { $_[1] * 100 },
+    filter_from_storage => sub { $_[1] - 100 },
+  });
+
+  for my $key ('', 'primary') {
+    throws_ok {
+      $schema->resultset('Artist')->find_or_create({ artistid => 42 }, { $key ? ( key => $key ) : () });
+    } qr/\QUnable to satisfy requested constraint 'primary', FilterColumn values not usable for column(s): 'artistid'/;
+  }
+}
+
 done_testing;
index 5e1e953..ea7767f 100644 (file)
@@ -4,8 +4,6 @@ use warnings;
 use Test::More;
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::DebugObj;
-use DBIC::SqlMakerTest;
 
 my $schema = DBICTest->init_schema();
 
@@ -15,20 +13,20 @@ $schema->resultset('CD')->delete;
 my $artist  = $schema->resultset("Artist")->create({ artistid => 21, name => 'Michael Jackson', rank => 20 });
 my $cd = $artist->create_related('cds', { year => 1975, title => 'Compilation from 1975' });
 
-my ($sql, @bind);
-local $schema->storage->{debug} = 1;
-local $schema->storage->{debugobj} = DBIC::DebugObj->new(\$sql, \@bind);
-
-my $find_cd = $artist->find_related('cds',{title => 'Compilation from 1975'});
-
-s/^'//, s/'\z// for @bind; # why does DBIC::DebugObj not do this?
-
-is_same_sql_bind (
-  $sql,
-  \@bind,
-  'SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me WHERE ( ( me.artist = ? AND me.title = ? ) ) ORDER BY year ASC',
-  [21, 'Compilation from 1975'],
-  'find_related only uses foreign key condition once',
-);
+$schema->is_executed_sql_bind(sub {
+  my $find_cd = $artist->find_related('cds',{title => 'Compilation from 1975'});
+}, [
+  [
+    ' SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track
+        FROM cd me
+      WHERE me.artist = ? AND me.title = ?
+      ORDER BY year ASC
+    ',
+    [ { dbic_colname => "me.artist", sqlt_datatype => "integer" }
+      => 21 ],
+    [ { dbic_colname => "me.title",  sqlt_datatype => "varchar", sqlt_size => 100 }
+      => "Compilation from 1975" ],
+  ]
+], 'find_related only uses foreign key condition once' );
 
 done_testing;
diff --git a/t/row/set_extra_column.t b/t/row/set_extra_column.t
new file mode 100644 (file)
index 0000000..0debaaf
--- /dev/null
@@ -0,0 +1,32 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib qw(t/lib);
+use DBICTest;
+
+my $schema = DBICTest->init_schema();
+
+my $rs_with_avg = $schema->resultset('CD')->search({}, {
+  '+columns' => { avg_year => $schema->resultset('CD')->get_column('year')->func_rs('avg')->as_query },
+  order_by => 'cdid',
+});
+
+for my $in_storage (1, 0) {
+  my $cd = $rs_with_avg->first;
+
+  ok ! $cd->is_column_changed('avg_year'), 'no changes';
+
+  $cd->in_storage($in_storage);
+
+  ok ! $cd->is_column_changed('avg_year'), 'still no changes';
+
+  $cd->set_column( avg_year => 42 );
+  $cd->set_column( avg_year => 69 );
+
+  ok $cd->is_column_changed('avg_year'), 'changed';
+  is $cd->get_column('avg_year'), 69, 'correct value'
+}
+
+done_testing;
diff --git a/t/row/sourceless.t b/t/row/sourceless.t
new file mode 100644 (file)
index 0000000..85ae3ee
--- /dev/null
@@ -0,0 +1,37 @@
+use strict;
+use warnings;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest;
+
+my $row = DBICTest::Schema::CD->new({ title => 'foo' });
+
+my @values = qw( foo bar baz );
+for my $i ( 0 .. $#values ) {
+  {
+    local $TODO = 'This probably needs to always return 1, on virgin objects... same with get_dirty_columns'
+      unless $i;
+
+    ok ( $row->is_column_changed('title'), 'uninserted row properly reports "eternally changed" value' );
+    is_deeply (
+      { $row->get_dirty_columns },
+      { title => $values[$i-1] },
+      'uninserted row properly reports "eternally changed" dirty_columns()'
+    );
+  }
+
+  $row->title( $values[$i] );
+
+  ok( $row->is_column_changed('title'), 'uninserted row properly reports changed value' );
+  is( $row->title, $values[$i] , 'Expected value on sourceless row' );
+  for my $meth (qw( get_columns get_inflated_columns get_dirty_columns )) {
+    is_deeply(
+      { $row->$meth },
+      { title => $values[$i] },
+      "Expected '$meth' rv",
+    )
+  }
+}
+
+done_testing;
index 07ac209..4a80267 100644 (file)
@@ -4,8 +4,7 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
diff --git a/t/search/empty_attrs.t b/t/search/empty_attrs.t
new file mode 100644 (file)
index 0000000..3b52487
--- /dev/null
@@ -0,0 +1,51 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib qw(t/lib);
+use DBICTest ':DiffSQL';
+
+my $schema = DBICTest->init_schema();
+
+my $rs = $schema->resultset('Artist')->search(
+  [ -and => [ {}, [] ], -or => [ {}, [] ] ],
+  {
+    select => [],
+    columns => {},
+    '+columns' => 'artistid',
+    join => [ {}, [ [ {}, {} ] ], {} ],
+    prefetch => [ [ [ {}, [] ], {} ], {}, [ {} ] ],
+    order_by => [],
+    group_by => [],
+    offset => 0,
+  }
+);
+
+is_same_sql_bind(
+  $rs->as_query,
+  '(SELECT me.artistid FROM artist me)',
+  [],
+);
+
+is_same_sql_bind(
+  $rs->count_rs->as_query,
+  '(SELECT COUNT(*) FROM artist me)',
+  [],
+);
+
+is_same_sql_bind(
+  $rs->as_subselect_rs->search({}, { columns => 'artistid' })->as_query,
+  '(SELECT me.artistid FROM (SELECT me.artistid FROM artist me) me)',
+  [],
+);
+
+{
+  local $TODO = 'Stupid misdesigned as_subselect_rs';
+  is_same_sql_bind(
+    $rs->as_subselect_rs->as_query,
+    $rs->as_subselect_rs->search({}, { columns => 'artistid' })->as_query,
+  );
+}
+
+done_testing;
index cb9a306..9f6704f 100644 (file)
@@ -5,53 +5,50 @@ use Test::More;
 use Test::Exception;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
-use DBIC::DebugObj;
-
-use Storable qw/dclone/;
+use DBICTest ':DiffSQL';
+use DBIx::Class::_Util 'serialize';
 
 my $schema = DBICTest->init_schema();
 
 # 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);
+  my ($cd_rs, $preimage);
 
   # test a real-life case - rs is obtained by an implicit m2m join
   $cd_rs = $schema->resultset ('Producer')->first->cds;
-  $attrs = dclone( $cd_rs->{attrs} );
+  $preimage = serialize $cd_rs->{attrs};
 
   $cd_rs->search ({})->all;
-  is_deeply (dclone($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after a simple search');
+  is ( serialize $cd_rs->{attrs}, $preimage, 'Resultset attributes preserved after a simple search');
 
   lives_ok (sub {
     $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
-    is_deeply (dclone($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after search with prefetch');
+    is ( serialize $cd_rs->{attrs}, $preimage, 'Resultset attributes preserved after search with prefetch');
   }, 'first prefetching search ok');
 
   lives_ok (sub {
     $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
-    is_deeply (dclone($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after another search with prefetch')
+    is ( serialize $cd_rs->{attrs}, $preimage, '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 = dclone( $cd_rs->{attrs} );
+  $preimage = serialize $cd_rs->{attrs};
 
   $cd_rs->search ({})->all;
-  is_deeply (dclone($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after a simple search');
+  is ( serialize $cd_rs->{attrs}, $preimage, 'Resultset attributes preserved after a simple search');
 
   lives_ok (sub {
     $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
-    is_deeply (dclone($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after search with prefetch');
+    is ( serialize $cd_rs->{attrs}, $preimage, 'Resultset attributes preserved after search with prefetch');
   }, 'first prefetching search ok');
 
   lives_ok (sub {
     $cd_rs->search ({'artist.artistid' => 1}, { prefetch => 'artist' })->all;
-    is_deeply (dclone($cd_rs->{attrs}), $attrs, 'Resultset attributes preserved after another search with prefetch')
+    is ( serialize $cd_rs->{attrs}, $preimage, 'Resultset attributes preserved after another search with prefetch')
   }, 'second prefetching search ok');
 }
 
index 1834d53..91b1fb7 100644 (file)
@@ -5,7 +5,6 @@ use Test::More;
 
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::SqlMakerTest;
 
 my $schema = DBICTest->init_schema();
 
index 76336b1..5e34fe9 100644 (file)
@@ -4,8 +4,7 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 
 my $ROWS = DBIx::Class::SQLMaker::LimitDialects->__rows_bindtype;
index 257b0b3..ed8f23b 100644 (file)
@@ -4,8 +4,7 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
index d742fd6..63de73c 100644 (file)
@@ -5,8 +5,7 @@ use Test::More;
 use Test::Exception;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
diff --git a/t/search/stack_cond.t b/t/search/stack_cond.t
new file mode 100644 (file)
index 0000000..9a0e806
--- /dev/null
@@ -0,0 +1,92 @@
+use strict;
+use warnings;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest ':DiffSQL';
+use SQL::Abstract qw(is_plain_value is_literal_value);
+use List::Util 'shuffle';
+use Data::Dumper;
+$Data::Dumper::Terse = 1;
+$Data::Dumper::Useqq = 1;
+$Data::Dumper::Indent = 0;
+
+my $schema = DBICTest->init_schema();
+
+for my $c (
+  { cond => undef, sql => 'IS NULL' },
+  { cond => { -value => undef }, sql => 'IS NULL' },
+  { cond => \'foo', sql => '= foo' },
+  { cond => 'foo', sql => '= ?', bind => [
+    [ { dbic_colname => "title", sqlt_datatype => "varchar", sqlt_size => 100 } => 'foo' ],
+    [ { dbic_colname => "year", sqlt_datatype => "varchar", sqlt_size => 100 } => 'foo' ],
+  ]},
+  { cond => { -value => 'foo' }, sql => '= ?', bind => [
+    [ { dbic_colname => "title", sqlt_datatype => "varchar", sqlt_size => 100 } => 'foo' ],
+    [ { dbic_colname => "year", sqlt_datatype => "varchar", sqlt_size => 100 } => 'foo' ],
+  ]},
+  { cond => \[ '?', "foo" ], sql => '= ?', bind => [
+    [ {} => 'foo' ],
+    [ {} => 'foo' ],
+  ]},
+) {
+  my $rs = $schema->resultset('CD')->search({}, { columns => 'title' });
+
+  my $bare_cond = is_literal_value($c->{cond}) ? { '=', $c->{cond} } : $c->{cond};
+
+  my @query_steps = (
+    # these are monkey-wrenches, always there
+    { title => { '!=', [ -and => \'bar' ] }, year => { '!=', [ -and => 'bar' ] } },
+    { -or => [ genreid => undef, genreid => { '!=' => \42 } ] },
+    { -or => [ genreid => undef, genreid => { '!=' => \42 } ] },
+
+    { title => $bare_cond, year => { '=', $c->{cond} } },
+    { -and => [ year => $bare_cond, { title => { '=', $c->{cond} } } ] },
+    [ year => $bare_cond ],
+    [ title => $bare_cond ],
+    { -and => [ { year => { '=', $c->{cond} } }, { title => { '=', $c->{cond} } } ] },
+    { -and => { -or => { year => { '=', $c->{cond} } } }, -or => { title => $bare_cond } },
+  );
+
+  if (my $v = is_plain_value($c->{cond})) {
+    push @query_steps,
+      { year => $$v },
+      { title => $$v },
+      { -and => [ year => $$v, title => $$v ] },
+    ;
+  }
+
+  @query_steps = shuffle @query_steps;
+
+  $rs = $rs->search($_) for @query_steps;
+
+  my @bind = @{$c->{bind} || []};
+  {
+    no warnings 'misc';
+    splice @bind, 1, 0, [ { dbic_colname => "year", sqlt_datatype => "varchar", sqlt_size => 100 } => 'bar' ];
+  }
+
+  is_same_sql_bind (
+    $rs->as_query,
+    "(
+      SELECT me.title
+        FROM cd me
+      WHERE
+        ( genreid != 42 OR genreid IS NULL )
+          AND
+        ( genreid != 42 OR genreid IS NULL )
+          AND
+        title != bar
+          AND
+        title $c->{sql}
+          AND
+        year != ?
+          AND
+        year $c->{sql}
+    )",
+    \@bind,
+    'Double condition correctly collapsed for steps' . Dumper \@query_steps,
+  );
+}
+
+done_testing;
index a281fe9..8c3fcf7 100644 (file)
@@ -4,9 +4,9 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
+use DBIx::Class::_Util 'sigwarn_silencer';
 
 my $ROWS = DBIx::Class::SQLMaker::LimitDialects->__rows_bindtype;
 
@@ -165,6 +165,8 @@ my @tests = (
 for my $i (0 .. $#tests) {
   my $t = $tests[$i];
   for my $p (1, 2) {  # repeat everything twice, make sure we do not clobber search arguments
+    local $SIG{__WARN__} = sigwarn_silencer( qr/\Q{from} structures with conditions not conforming to the SQL::Abstract syntax are deprecated/ );
+
     is_same_sql_bind (
       $t->{rs}->search ($t->{search}, $t->{attrs})->as_query,
       $t->{sqlbind},
index bafe8e9..3097191 100644 (file)
@@ -6,8 +6,7 @@ use Test::Exception;
 use Math::BigInt;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 
 my ($ROWS, $OFFSET) = (
@@ -104,45 +103,30 @@ shorthand_check(
   'stringifyable $object === [ {}, $object ]',
 );
 
-throws_ok {
-  shorthand_check(
+shorthand_check(
     [ 2 ],
-    [],
-  )
-} qr !You must supply a datatype/bindtype .+ for non-scalar value  \Q[ 2 ]!,
-  'exception on bare array bindvalue';
+    [ {} => [ 2 ] ],
+);
 
-throws_ok {
-  shorthand_check(
+shorthand_check(
     [ {} => [ 2 ] ],
-    [],
-  )
-} qr !You must supply a datatype/bindtype .+ for non-scalar value  \Q[ 2 ]!,
-  'exception on untyped array bindvalue';
+    [ {} => [ 2 ] ],
+);
 
-throws_ok {
-  shorthand_check(
+shorthand_check(
     [ {}, 2, 3 ],
-    [],
-  )
-} qr !You must supply a datatype/bindtype .+ for non-scalar value  \[ 'HASH\(\w+\)', 2, 3 \]!,
-  'exception on bare multielement array bindvalue';
+    [ {} => [ {}, 2, 3 ] ],
+);
 
-throws_ok {
-  shorthand_check(
+shorthand_check(
     bless( {}, 'Foo'),
-    [],
-  )
-} qr !You must supply a datatype/bindtype .+ for non-scalar value  \Qbless( {}, 'Foo' )!,
-  'exception on bare object';
+    [ {} => bless( {}, 'Foo') ],
+);
 
-throws_ok {
-  shorthand_check(
+shorthand_check(
+    [ {}, bless( {}, 'Foo') ],
     [ {}, bless( {}, 'Foo') ],
-    [],
-  )
-} qr !You must supply a datatype/bindtype .+ for non-scalar value  \Qbless( {}, 'Foo' )!,
-  'exception on untyped object';
+);
 
 
 sub shorthand_check {
index 57cf480..1c2a1c3 100644 (file)
@@ -5,8 +5,7 @@ use Test::More;
 use Test::Exception;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema(no_deploy => 1);
 
index 53ce03b..8e45566 100644 (file)
@@ -4,8 +4,7 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
@@ -25,7 +24,7 @@ my ($sql, @bind) = $sql_maker->select(
                 '-join_type' => ''
               },
               {
-                'artist.artistid' => 'me.artist'
+                'artist.artistid' => { -ident => 'me.artist' },
               }
             ],
             [
@@ -34,7 +33,7 @@ my ($sql, @bind) = $sql_maker->select(
                 '-join_type' => 'left'
               },
               {
-                'tracks.cd' => 'me.cdid'
+                'tracks.cd' => { -ident => 'me.cdid' },
               }
             ],
           ],
@@ -308,7 +307,7 @@ $sql_maker->quote_char([qw/[ ]/]);
                 '-join_type' => ''
               },
               {
-                'artist.artistid' => 'me.artist'
+                'artist.artistid' => { -ident => 'me.artist' }
               }
             ]
           ],
diff --git a/t/sqlmaker/dbihacks_internals.t b/t/sqlmaker/dbihacks_internals.t
new file mode 100644 (file)
index 0000000..ca81737
--- /dev/null
@@ -0,0 +1,685 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Warn;
+use Test::Exception;
+
+use lib qw(t/lib);
+use DBICTest ':DiffSQL';
+use DBIx::Class::_Util 'UNRESOLVABLE_CONDITION';
+
+use Data::Dumper;
+BEGIN {
+  if ( eval { require Test::Differences } ) {
+    no warnings 'redefine';
+    *is_deeply = \&Test::Differences::eq_or_diff;
+  }
+}
+
+my $schema = DBICTest->init_schema( no_deploy => 1);
+my $sm = $schema->storage->sql_maker;
+
+{
+  package # hideee
+    DBICTest::SillyInt;
+
+  use overload
+    fallback => 1,
+    '0+' => sub { ${$_[0]} },
+  ;
+}
+my $num = bless( \do { my $foo = 69 }, 'DBICTest::SillyInt' );
+
+is($num, 69, 'test overloaded object is "sane"');
+is("$num", 69, 'test overloaded object is "sane"');
+
+my @tests = (
+  {
+    where => { artistid => 1, charfield => undef },
+    cc_result => { artistid => 1, charfield => undef },
+    sql => 'WHERE artistid = ? AND charfield IS NULL',
+    efcc_result => { artistid => 1 },
+    efcc_n_result => { artistid => 1, charfield => undef },
+  },
+  {
+    where => { -and => [ artistid => 1, charfield => undef, { rank => 13 } ] },
+    cc_result => { artistid => 1, charfield => undef, rank => 13 },
+    sql => 'WHERE artistid = ?  AND charfield IS NULL AND rank = ?',
+    efcc_result => { artistid => 1, rank => 13 },
+    efcc_n_result => { artistid => 1, charfield => undef, rank => 13 },
+  },
+  {
+    where => { -and => [ { artistid => 1, charfield => undef}, { rank => 13 } ] },
+    cc_result => { artistid => 1, charfield => undef, rank => 13 },
+    sql => 'WHERE artistid = ?  AND charfield IS NULL AND rank = ?',
+    efcc_result => { artistid => 1, rank => 13 },
+    efcc_n_result => { artistid => 1, charfield => undef, rank => 13 },
+  },
+  {
+    where => { -and => [ -or => { name => 'Caterwauler McCrae' }, 'rank' ] },
+    cc_result => { name => 'Caterwauler McCrae', rank => undef },
+    sql => 'WHERE name = ? AND rank IS NULL',
+    efcc_result => { name => 'Caterwauler McCrae' },
+    efcc_n_result => { name => 'Caterwauler McCrae', rank => undef },
+  },
+  {
+    where => { -and => [ [ [ artist => {'=' => \'foo' } ] ], { name => \[ '= ?', 'bar' ] } ] },
+    cc_result => { artist => {'=' => \'foo' }, name => \[ '= ?', 'bar' ] },
+    sql => 'WHERE artist = foo AND name = ?',
+    efcc_result => { artist => \'foo' },
+  },
+  {
+    where => { -and => [ -or => { name => 'Caterwauler McCrae', artistid => 2 } ] },
+    cc_result => { -or => [ artistid => 2, name => 'Caterwauler McCrae' ] },
+    sql => 'WHERE artistid = ? OR name = ?',
+    efcc_result => {},
+  },
+  {
+    where => { -or => { name => 'Caterwauler McCrae', artistid => 2 } },
+    cc_result => { -or => [ artistid => 2, name => 'Caterwauler McCrae' ] },
+    sql => 'WHERE artistid = ? OR name = ?',
+    efcc_result => {},
+  },
+  {
+    where => { -and => [ \'foo=bar',  [ { artistid => { '=', $num } } ], { name => 'Caterwauler McCrae'} ] },
+    cc_result => { -and => [ \'foo=bar' ], name => 'Caterwauler McCrae', artistid => $num },
+    sql => 'WHERE foo=bar AND artistid = ? AND name = ?',
+    efcc_result => { name => 'Caterwauler McCrae', artistid => $num },
+  },
+  {
+    where => { -and => [ \'foo=bar',  [ { artistid => { '=', $num } } ], { name => 'Caterwauler McCrae'}, \'buzz=bozz' ] },
+    cc_result => { -and => [ \'foo=bar', \'buzz=bozz' ], name => 'Caterwauler McCrae', artistid => $num },
+    sql => 'WHERE foo=bar AND artistid = ? AND name = ? AND buzz=bozz',
+    collapsed_sql => 'WHERE foo=bar AND buzz=bozz AND artistid = ? AND name = ?',
+    efcc_result => { name => 'Caterwauler McCrae', artistid => $num },
+  },
+  {
+    where => { artistid => [ $num ], rank => [ 13, 2, 3 ], charfield => [ undef ] },
+    cc_result => { artistid => $num, charfield => undef, rank => [13, 2, 3] },
+    sql => 'WHERE artistid = ? AND charfield IS NULL AND ( rank = ? OR rank = ? OR rank = ? )',
+    efcc_result => { artistid => $num },
+    efcc_n_result => { artistid => $num, charfield => undef },
+  },
+  {
+    where => { artistid => { '=' => 1 }, rank => { '>' => 12 }, charfield => { '=' => undef } },
+    cc_result => { artistid => 1, charfield => undef, rank => { '>' => 12 } },
+    sql => 'WHERE artistid = ? AND charfield IS NULL AND rank > ?',
+    efcc_result => { artistid => 1 },
+    efcc_n_result => { artistid => 1, charfield => undef },
+  },
+  {
+    where => { artistid => { '=' => [ 1 ], }, charfield => { '=' => [ -AND => \'1', \['?',2] ] }, rank => { '=' => [ -OR => $num, $num ] } },
+    cc_result => { artistid => 1, charfield => [-and => { '=' => \['?',2] }, { '=' => \'1' } ], rank => { '=' => [$num, $num] } },
+    sql => 'WHERE artistid = ? AND charfield = 1 AND charfield = ? AND ( rank = ? OR rank = ? )',
+    collapsed_sql => 'WHERE artistid = ? AND charfield = ? AND charfield = 1 AND ( rank = ? OR rank = ? )',
+    efcc_result => { artistid => 1, charfield => UNRESOLVABLE_CONDITION },
+  },
+  {
+    where => { -and => [ artistid => 1, artistid => 2 ], name => [ -and => { '!=', 1 }, 2 ], charfield => [ -or => { '=', 2 } ], rank => [-and => undef, { '=', undef }, { '!=', 2 } ] },
+    cc_result => { artistid => [ -and => 1, 2 ], name => [ -and => { '!=', 1 }, 2 ], charfield => 2, rank => [ -and => { '!=', 2 }, undef ] },
+    sql => 'WHERE artistid = ? AND artistid = ? AND charfield = ? AND name != ? AND name = ? AND rank IS NULL AND rank IS NULL AND rank != ?',
+    collapsed_sql => 'WHERE artistid = ? AND artistid = ? AND charfield = ? AND name != ? AND name = ? AND rank != ? AND rank IS NULL',
+    efcc_result => {
+      artistid => UNRESOLVABLE_CONDITION,
+      name => 2,
+      charfield => 2,
+    },
+    efcc_n_result => {
+      artistid => UNRESOLVABLE_CONDITION,
+      name => 2,
+      charfield => 2,
+      rank => undef,
+    },
+  },
+  (map { {
+    where => $_,
+    sql => 'WHERE (rank = 13 OR charfield IS NULL OR artistid = ?) AND (artistid = ? OR charfield IS NULL OR rank != 42)',
+    collapsed_sql => 'WHERE (artistid = ? OR charfield IS NULL OR rank = 13) AND (artistid = ? OR charfield IS NULL OR rank != 42)',
+    cc_result => { -and => [
+      { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] },
+      { -or => [ artistid => 1, charfield => undef, rank => { '!=' => \42 } ] },
+    ] },
+    efcc_result => {},
+    efcc_n_result => {},
+  } } (
+
+    { -and => [
+      -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ],
+      -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '!=' => \42 } },
+    ] },
+
+    {
+      -OR => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ],
+      -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '!=' => \42 } },
+    },
+
+  ) ),
+  {
+    where => { -or => [
+      -and => [ foo => { '!=', { -value => undef } }, bar => { -in => [ 69, 42 ] } ],
+      foo => { '=', { -value => undef } },
+      baz => { '!=' => { -ident => 'bozz' } },
+      baz => { -ident => 'buzz' },
+    ] },
+    sql => 'WHERE ( foo IS NOT NULL AND bar IN ( ?, ? ) ) OR foo IS NULL OR baz != bozz OR baz = buzz',
+    collapsed_sql => 'WHERE baz != bozz OR baz = buzz OR foo IS NULL OR ( bar IN ( ?, ? ) AND foo IS NOT NULL )',
+    cc_result => { -or => [
+      baz => { '!=' => { -ident => 'bozz' } },
+      baz => { '=' => { -ident => 'buzz' } },
+      foo => undef,
+      { bar => { -in => [ 69, 42 ] }, foo => { '!=', undef } }
+    ] },
+    efcc_result => {},
+  },
+  {
+    where => { -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => { '=' => 1 }, genreid => { '=' => \['?', 2] } ] },
+    sql => 'WHERE rank = 13 OR charfield IS NULL OR artistid = ? OR genreid = ?',
+    collapsed_sql => 'WHERE artistid = ? OR charfield IS NULL OR genreid = ? OR rank = 13',
+    cc_result => { -or => [ artistid => 1, charfield => undef, genreid => { '=' => \['?', 2] }, rank => { '=' => \13 } ] },
+    efcc_result => {},
+    efcc_n_result => {},
+  },
+  {
+    where => { -and => [
+      -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ],
+      -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '=' => \13 } },
+    ] },
+    cc_result => { -and => [
+      { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] },
+      { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] },
+    ] },
+    sql => 'WHERE (rank = 13 OR charfield IS NULL OR artistid = ?) AND (artistid = ? OR charfield IS NULL OR rank = 13)',
+    collapsed_sql => 'WHERE (artistid = ? OR charfield IS NULL OR rank = 13) AND (artistid = ? OR charfield IS NULL OR rank = 13)',
+    efcc_result => {},
+    efcc_n_result => {},
+  },
+  {
+    where => { -and => [
+      -or => [ rank => { '=' => \13 }, charfield => { '=' => undef }, artistid => 1 ],
+      -or => { artistid => { '=' => 1 }, charfield => undef, rank => { '!=' => \42 } },
+      -and => [ foo => { '=' => \1 }, bar => 2 ],
+      -and => [ foo => 3, bar => { '=' => \4 } ],
+      -exists => \'(SELECT 1)',
+      -exists => \'(SELECT 2)',
+      -not => { foo => 69 },
+      -not => { foo => 42 },
+    ]},
+    sql => 'WHERE
+          ( rank = 13 OR charfield IS NULL OR artistid = ? )
+      AND ( artistid = ? OR charfield IS NULL OR rank != 42 )
+      AND foo = 1
+      AND bar = ?
+      AND foo = ?
+      AND bar = 4
+      AND (EXISTS (SELECT 1))
+      AND (EXISTS (SELECT 2))
+      AND NOT foo = ?
+      AND NOT foo = ?
+    ',
+    collapsed_sql => 'WHERE
+          ( artistid = ? OR charfield IS NULL OR rank = 13 )
+      AND ( artistid = ? OR charfield IS NULL OR rank != 42 )
+      AND (EXISTS (SELECT 1))
+      AND (EXISTS (SELECT 2))
+      AND NOT foo = ?
+      AND NOT foo = ?
+      AND bar = 4
+      AND bar = ?
+      AND foo = 1
+      AND foo = ?
+    ',
+    cc_result => {
+      -and => [
+        { -or => [ artistid => 1, charfield => undef, rank => { '=' => \13 } ] },
+        { -or => [ artistid => 1, charfield => undef, rank => { '!=' => \42 } ] },
+        { -exists => \'(SELECT 1)' },
+        { -exists => \'(SELECT 2)' },
+        { -not => { foo => 69 } },
+        { -not => { foo => 42 } },
+      ],
+      foo => [ -and => { '=' => \1 }, 3 ],
+      bar => [ -and => { '=' => \4 }, 2 ],
+    },
+    efcc_result => {
+      foo => UNRESOLVABLE_CONDITION,
+      bar => UNRESOLVABLE_CONDITION,
+    },
+    efcc_n_result => {
+      foo => UNRESOLVABLE_CONDITION,
+      bar => UNRESOLVABLE_CONDITION,
+    },
+  },
+  {
+    where => { -and => [
+      [ '_macro.to' => { -like => '%correct%' }, '_wc_macros.to' => { -like => '%correct%' } ],
+      { -and => [ { 'group.is_active' => 1 }, { 'me.is_active' => 1 } ] }
+    ] },
+    cc_result => {
+      'group.is_active' => 1,
+      'me.is_active' => 1,
+      -or => [
+        '_macro.to' => { -like => '%correct%' },
+        '_wc_macros.to' => { -like => '%correct%' },
+      ],
+    },
+    sql => 'WHERE ( _macro.to LIKE ? OR _wc_macros.to LIKE ? ) AND group.is_active = ? AND me.is_active = ?',
+    efcc_result => { 'group.is_active' => 1, 'me.is_active' => 1 },
+  },
+
+  {
+    where => { -and => [
+      artistid => { -value => [1] },
+      charfield => { -ident => 'foo' },
+      name => { '=' => { -value => undef } },
+      rank => { '=' => { -ident => 'bar' } },
+    ] },
+    sql => 'WHERE artistid = ? AND charfield = foo AND name IS NULL AND rank = bar',
+    cc_result => {
+      artistid => { -value => [1] },
+      name => undef,
+      charfield => { '=', { -ident => 'foo' } },
+      rank => { '=' => { -ident => 'bar' } },
+    },
+    efcc_result => {
+      artistid => [1],
+      charfield => { -ident => 'foo' },
+      rank => { -ident => 'bar' },
+    },
+    efcc_n_result => {
+      artistid => [1],
+      name => undef,
+      charfield => { -ident => 'foo' },
+      rank => { -ident => 'bar' },
+    },
+  },
+
+  {
+    where => { artistid => [] },
+    cc_result => { artistid => [] },
+    efcc_result => {},
+  },
+  (map {
+    {
+      where => { -and => $_ },
+      cc_result => undef,
+      efcc_result => {},
+      sql => '',
+    },
+    {
+      where => { -or => $_ },
+      cc_result => undef,
+      efcc_result => {},
+      sql => '',
+    },
+    {
+      where => { -or => [ foo => 1, $_ ] },
+      cc_result => { foo => 1 },
+      efcc_result => { foo => 1 },
+      sql => 'WHERE foo = ?',
+    },
+    {
+      where => { -or => [ $_, foo => 1 ] },
+      cc_result => { foo => 1 },
+      efcc_result => { foo => 1 },
+      sql => 'WHERE foo = ?',
+    },
+    {
+      where => { -and => [ fuu => 2, $_, foo => 1 ] },
+      sql => 'WHERE fuu = ? AND foo = ?',
+      collapsed_sql => 'WHERE foo = ? AND fuu = ?',
+      cc_result => { foo => 1, fuu => 2 },
+      efcc_result => { foo => 1, fuu => 2 },
+    },
+  } (
+    # bare
+    [], {},
+    # singles
+    [ {} ], [ [] ],
+    # doubles
+    [ [], [] ], [ {}, {} ], [ [], {} ], [ {}, [] ],
+    # tripples
+    [ {}, [], {} ], [ [], {}, [] ]
+  )),
+
+  # FIXME legacy compat crap, possibly worth undef/dieing in SQLMaker
+  { where => { artistid => {} }, sql => '', cc_result => undef, efcc_result => {}, efcc_n_result => {} },
+
+  # batshit insanity, just to be thorough
+  {
+    where => { -and => [ [ 'artistid' ], [ -and => [ artistid => { '!=', 69 }, artistid => undef, artistid => { '=' => 200 } ]], artistid => [], { -or => [] }, { -and => [] }, [ 'charfield' ], { name => [] }, 'rank' ] },
+    cc_result => { artistid => [ -and => [], { '!=', 69 }, undef, 200  ], charfield => undef, name => [], rank => undef },
+    sql => 'WHERE artistid IS NULL AND artistid != ? AND artistid IS NULL AND artistid = ? AND 0=1 AND charfield IS NULL AND 0=1 AND rank IS NULL',
+    collapsed_sql => 'WHERE 0=1 AND artistid != ? AND artistid IS NULL AND artistid = ? AND charfield IS NULL AND 0=1 AND rank IS NULL',
+    efcc_result => { artistid => UNRESOLVABLE_CONDITION },
+    efcc_n_result => { artistid => UNRESOLVABLE_CONDITION, charfield => undef, rank => undef },
+  },
+
+  # original test from RT#93244
+  {
+    where => {
+      -and => [
+        \[
+          "LOWER(me.title) LIKE ?",
+          '%spoon%',
+        ],
+        [ { 'me.title' => 'Spoonful of bees' } ],
+    ]},
+    cc_result => {
+      -and => [ \[
+        "LOWER(me.title) LIKE ?",
+        '%spoon%',
+      ]],
+      'me.title' => 'Spoonful of bees',
+    },
+    sql => 'WHERE LOWER(me.title) LIKE ? AND me.title = ?',
+    efcc_result => { 'me.title' => 'Spoonful of bees' },
+  },
+
+  # crazy literals
+  {
+    where => {
+      -or => [
+        \'foo = bar',
+      ],
+    },
+    sql => 'WHERE foo = bar',
+    cc_result => {
+      -and => [
+        \'foo = bar',
+      ],
+    },
+    efcc_result => {},
+  },
+  {
+    where => {
+      -or => [
+        \'foo = bar',
+        \'baz = ber',
+      ],
+    },
+    sql => 'WHERE foo = bar OR baz = ber',
+    collapsed_sql => 'WHERE baz = ber OR foo = bar',
+    cc_result => {
+      -or => [
+        \'baz = ber',
+        \'foo = bar',
+      ],
+    },
+    efcc_result => {},
+  },
+  {
+    where => {
+      -and => [
+        \'foo = bar',
+        \'baz = ber',
+      ],
+    },
+    sql => 'WHERE foo = bar AND baz = ber',
+    cc_result => {
+      -and => [
+        \'foo = bar',
+        \'baz = ber',
+      ],
+    },
+    efcc_result => {},
+  },
+  {
+    where => {
+      -and => [
+        \'foo = bar',
+        \'baz = ber',
+        x => { -ident => 'y' },
+      ],
+    },
+    sql => 'WHERE foo = bar AND baz = ber AND x = y',
+    cc_result => {
+      -and => [
+        \'foo = bar',
+        \'baz = ber',
+      ],
+      x => { '=' => { -ident => 'y' } }
+    },
+    efcc_result => { x => { -ident => 'y' } },
+  },
+);
+
+# these die as of SQLA 1.80 - make sure we do not transform them
+# into something usable instead
+for my $lhs (undef, '', { -ident => 'foo' }, { -value => 'foo' } ) {
+  no warnings 'uninitialized';
+
+  for my $w (
+    ( map { { -or => $_ }, (ref $lhs ? () : { @$_ } ) }
+      [ $lhs => "foo" ],
+      [ $lhs => { "=" => "bozz" } ],
+      [ $lhs => { "=" => \"bozz" } ],
+      [ $lhs => { -max => \"bizz" } ],
+    ),
+
+    (ref $lhs) ? () : (
+      { -or => [ -and => { $lhs => "baz" }, bizz => "buzz" ] },
+      { -or => [ foo => "bar", { $lhs => "baz" }, bizz => "buzz" ] },
+      { foo => "bar", -or => { $lhs => "baz" } },
+      { foo => "bar", -or => { $lhs => \"baz" }, bizz => "buzz" },
+    ),
+
+    { foo => "bar", -and => [ $lhs => \"baz" ], bizz => "buzz" },
+    { foo => "bar", -or => [ $lhs => \"baz" ], bizz => "buzz" },
+
+    { -or => [ foo => "bar", [ $lhs => \"baz" ], bizz => "buzz" ] },
+    { -or => [ foo => "bar", $lhs => \"baz", bizz => "buzz" ] },
+    { -or => [ foo => "bar", $lhs => \["baz"], bizz => "buzz" ] },
+    { -or => [ $lhs => \"baz" ] },
+    { -or => [ $lhs => \["baz"] ] },
+
+  ) {
+    push @tests, {
+      where => $w,
+      throw => qr/
+        \QSupplying an empty left hand side argument is not supported in \E(?:array|hash)-pairs
+          |
+        \QIllegal use of top-level '-\E(?:value|ident)'
+      /x,
+    }
+  }
+}
+
+# these are deprecated as of SQLA 1.79 - make sure we do not transform
+# them without losing the warning
+for my $lhs (undef, '') {
+  for my $rhs ( \"baz", \[ "baz" ] ) {
+    no warnings 'uninitialized';
+
+    my $expected_warning = qr/\QHash-pairs consisting of an empty string with a literal are deprecated/;
+
+    push @tests, {
+      where => { $lhs => $rhs },
+      cc_result => { -and => [ $rhs ] },
+      efcc_result => {},
+      sql => 'WHERE baz',
+      warn => $expected_warning,
+    };
+
+    for my $w (
+      { foo => "bar", -and => { $lhs => $rhs }, bizz => "buzz" },
+      { foo => "bar", $lhs => $rhs, bizz => "buzz" },
+    ) {
+      push @tests, {
+        where => $w,
+        cc_result => {
+          -and => [ $rhs ],
+          bizz => "buzz",
+          foo => "bar",
+        },
+        efcc_result => {
+          foo => "bar",
+          bizz => "buzz",
+        },
+        sql => 'WHERE baz AND bizz = ? AND foo = ?',
+        warn => $expected_warning,
+      };
+    }
+  }
+}
+
+# lots of extra silly tests with a false column
+for my $eq (
+  \"= baz",
+  \[ "= baz" ],
+  { '=' => { -ident => 'baz' } },
+  { '=' => \'baz' },
+) {
+  for my $where (
+    { foo => "bar", -and => [ 0 => $eq ], bizz => "buzz" },
+    { foo => "bar", -or => [ 0 => $eq ], bizz => "buzz" },
+    { foo => "bar", -and => { 0 => $eq }, bizz => "buzz" },
+    { foo => "bar", -or => { 0 => $eq }, bizz => "buzz" },
+    { foo => "bar", 0 => $eq, bizz => "buzz" },
+  ) {
+    push @tests, {
+      where => $where,
+      cc_result => {
+        0 => $eq,
+        foo => 'bar',
+        bizz => 'buzz',
+      },
+      efcc_result => {
+        foo => 'bar',
+        bizz => 'buzz',
+        ( ref $eq eq 'HASH' ? ( 0 => $eq->{'='} ) : () ),
+      },
+      sql => 'WHERE 0 = baz AND bizz = ? AND foo = ?',
+    };
+
+    push @tests, {
+      where => { -or => $where },
+      cc_result => { -or => [
+        "0" => $eq,
+        bizz => 'buzz',
+        foo => 'bar',
+      ]},
+      efcc_result => {},
+      sql => 'WHERE 0 = baz OR bizz = ? OR foo = ?',
+    }
+
+  }
+
+  for my $where (
+    [ foo => "bar", -and => [ 0 => $eq ], bizz => "buzz" ],
+    [ foo => "bar", -or => [ 0 => $eq ], bizz => "buzz" ],
+    [ foo => "bar", -and => { 0 => $eq }, bizz => "buzz" ],
+    [ foo => "bar", -or => { 0 => $eq }, bizz => "buzz" ],
+    [ foo => "bar", 0 => $eq, bizz => "buzz" ],
+  ) {
+    push @tests, {
+      where => { -or => $where },
+      cc_result => { -or => [
+        "0" => $eq,
+        bizz => 'buzz',
+        foo => 'bar',
+      ]},
+      efcc_result => {},
+      sql => 'WHERE foo = ? OR 0 = baz OR bizz = ?',
+      collapsed_sql => 'WHERE 0 = baz OR bizz = ? OR foo = ?',
+    }
+  }
+
+  for my $where (
+    [ {foo => "bar"}, -and => { 0 => "baz" }, bizz => "buzz" ],
+    [ -or => [ foo => "bar", -or => { 0 => "baz" }, bizz => "buzz" ] ],
+  ) {
+    push @tests, {
+      where => { -or => $where },
+      cc_result => { -or => [
+        "0" => 'baz',
+        bizz => 'buzz',
+        foo => 'bar',
+      ]},
+      efcc_result => {},
+      sql => 'WHERE foo = ? OR 0 = ? OR bizz = ?',
+      collapsed_sql => 'WHERE 0 = ? OR bizz = ? OR foo = ?',
+    };
+  }
+
+};
+
+for my $t (@tests) {
+  for my $w (
+    $t->{where},
+    $t->{where},  # do it twice, make sure we didn't destory the condition
+    [ -and => $t->{where} ],
+    [ -AND => $t->{where} ],
+    { -OR => [ -AND => $t->{where} ] },
+    ( ( keys %{$t->{where}} == 1 and length( (keys %{$t->{where}})[0] ) )
+      ? [ %{$t->{where}} ]
+      : ()
+    ),
+    ( (keys %{$t->{where}} == 1 and $t->{where}{-or})
+      ? ( ref $t->{where}{-or} eq 'HASH'
+        ? [ map { $_ => $t->{where}{-or}{$_} } sort keys %{$t->{where}{-or}} ]
+        : $t->{where}{-or}
+      )
+      : ()
+    ),
+  ) {
+    die unless Test::Builder->new->is_passing;
+
+    my $name = do { local ($Data::Dumper::Indent, $Data::Dumper::Terse, $Data::Dumper::Sortkeys) = (0, 1, 1); Dumper $w };
+
+    my ($collapsed_cond, $collapsed_cond_as_sql);
+
+    if ($t->{throw}) {
+      throws_ok {
+        $collapsed_cond = $schema->storage->_collapse_cond($w);
+        ($collapsed_cond_as_sql) = $sm->where($collapsed_cond);
+      } $t->{throw}, "Exception on attempted collapse/render of $name"
+        and
+      next;
+    }
+
+    warnings_exist {
+      $collapsed_cond = $schema->storage->_collapse_cond($w);
+      ($collapsed_cond_as_sql) = $sm->where($collapsed_cond);
+    } $t->{warn} || [], "Expected warning when collapsing/rendering $name";
+
+    is_deeply(
+      $collapsed_cond,
+      $t->{cc_result},
+      "Expected collapsed condition produced on $name",
+    );
+
+    my ($original_sql) = do {
+      local $SIG{__WARN__} = sub {};
+      $sm->where($w);
+    };
+
+    is_same_sql ( $original_sql, $t->{sql}, "Expected original SQL from $name" )
+      if exists $t->{sql};
+
+    is_same_sql(
+      $collapsed_cond_as_sql,
+      ( $t->{collapsed_sql} || $t->{sql} || $original_sql ),
+      "Collapse did not alter *the semantics* of the final SQL based on $name",
+    );
+
+    is_deeply(
+      $schema->storage->_extract_fixed_condition_columns($collapsed_cond),
+      $t->{efcc_result},
+      "Expected fixed_condition produced on $name",
+    );
+
+    is_deeply(
+      $schema->storage->_extract_fixed_condition_columns($collapsed_cond, 'consider_nulls'),
+      $t->{efcc_n_result},
+      "Expected fixed_condition including NULLs produced on $name",
+    ) if $t->{efcc_n_result};
+
+    is_deeply(
+      $collapsed_cond,
+      $t->{cc_result},
+      "Collapsed condition result unaltered by fixed condition extractor",
+    );
+  }
+}
+
+done_testing;
index 1283140..3495e85 100644 (file)
@@ -1,13 +1,11 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'id_shortener';
+
 use strict;
 use warnings;
 
 use Test::More;
-use lib qw(t/lib);
-
-use DBIx::Class::Optional::Dependencies;
-plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('id_shortener')
-  unless DBIx::Class::Optional::Dependencies->req_ok_for ('id_shortener');
 
+use lib qw(t/lib);
 use DBICTest::Schema::Artist;
 BEGIN {
   DBICTest::Schema::Artist->add_column('parentid');
@@ -23,9 +21,7 @@ BEGIN {
   );
 }
 
-use DBICTest;
-use DBICTest::Schema;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 use DBIx::Class::SQLMaker::LimitDialects;
 my $ROWS = DBIx::Class::SQLMaker::LimitDialects->__rows_bindtype;
diff --git a/t/sqlmaker/legacy_joins.t b/t/sqlmaker/legacy_joins.t
new file mode 100644 (file)
index 0000000..1c93c35
--- /dev/null
@@ -0,0 +1,100 @@
+use strict;
+use warnings;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest ':DiffSQL';
+use DBIx::Class::_Util 'sigwarn_silencer';
+
+use DBIx::Class::SQLMaker;
+my $sa = DBIx::Class::SQLMaker->new;
+
+$SIG{__WARN__} = sigwarn_silencer( qr/\Q{from} structures with conditions not conforming to the SQL::Abstract syntax are deprecated/ );
+
+my @j = (
+    { child => 'person' },
+    [ { father => 'person' }, { 'father.person_id' => 'child.father_id' }, ],
+    [ { mother => 'person' }, { 'mother.person_id' => 'child.mother_id' } ],
+);
+my $match = 'person child JOIN person father ON ( father.person_id = '
+          . 'child.father_id ) JOIN person mother ON ( mother.person_id '
+          . '= child.mother_id )'
+          ;
+is_same_sql(
+  $sa->_recurse_from(@j),
+  $match,
+  'join 1 ok'
+);
+
+my @j2 = (
+    { mother => 'person' },
+    [   [   { child => 'person' },
+            [   { father             => 'person' },
+                { 'father.person_id' => 'child.father_id' }
+            ]
+        ],
+        { 'mother.person_id' => 'child.mother_id' }
+    ],
+);
+$match = 'person mother JOIN (person child JOIN person father ON ('
+       . ' father.person_id = child.father_id )) ON ( mother.person_id = '
+       . 'child.mother_id )'
+       ;
+is_same_sql(
+  $sa->_recurse_from(@j2),
+  $match,
+  'join 2 ok'
+);
+
+my @j3 = (
+    { child => 'person' },
+    [ { father => 'person', -join_type => 'inner' }, { 'father.person_id' => 'child.father_id' }, ],
+    [ { mother => 'person', -join_type => 'inner'  }, { 'mother.person_id' => 'child.mother_id' } ],
+);
+$match = 'person child INNER JOIN person father ON ( father.person_id = '
+          . 'child.father_id ) INNER JOIN person mother ON ( mother.person_id '
+          . '= child.mother_id )'
+          ;
+
+is_same_sql(
+  $sa->_recurse_from(@j3),
+  $match,
+  'join 3 (inner join) ok'
+);
+
+my @j4 = (
+    { mother => 'person' },
+    [   [   { child => 'person', -join_type => 'left' },
+            [   { father             => 'person', -join_type => 'right' },
+                { 'father.person_id' => 'child.father_id' }
+            ]
+        ],
+        { 'mother.person_id' => 'child.mother_id' }
+    ],
+);
+$match = 'person mother LEFT JOIN (person child RIGHT JOIN person father ON ('
+       . ' father.person_id = child.father_id )) ON ( mother.person_id = '
+       . 'child.mother_id )'
+       ;
+is_same_sql(
+  $sa->_recurse_from(@j4),
+  $match,
+  'join 4 (nested joins + join types) ok'
+);
+
+my @j5 = (
+    { child => 'person' },
+    [ { father => 'person' }, { 'father.person_id' => \'!= child.father_id' }, ],
+    [ { mother => 'person' }, { 'mother.person_id' => 'child.mother_id' } ],
+);
+$match = 'person child JOIN person father ON ( father.person_id != '
+          . 'child.father_id ) JOIN person mother ON ( mother.person_id '
+          . '= child.mother_id )'
+          ;
+is_same_sql(
+  $sa->_recurse_from(@j5),
+  $match,
+  'join 5 (SCALAR reference for ON statement) ok'
+);
+
+done_testing;
index c5e61c6..89c4788 100644 (file)
@@ -5,9 +5,7 @@ use Test::More;
 use Test::Warn;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBICTest::Schema;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 # This is legacy stuff from SQL::Absract::Limit
 # Keep it around just in case someone is using it
index 10d3e60..ab3e170 100644 (file)
@@ -3,8 +3,7 @@ use warnings;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema;
 
@@ -114,6 +113,15 @@ for my $ord_set (
     exselect_outer => 'ORDER__BY__001, ORDER__BY__002, ORDER__BY__003',
     exselect_inner => 'title AS ORDER__BY__001, bar AS ORDER__BY__002, sensors AS ORDER__BY__003',
   },
+
+  {
+    order_by => [
+      'name',
+    ],
+    order_inner => 'name',
+    order_outer => 'name DESC',
+    order_req => 'name',
+  },
 ) {
   my $o_sel = $ord_set->{exselect_outer}
     ? ', ' . $ord_set->{exselect_outer}
@@ -124,8 +132,13 @@ for my $ord_set (
     : ''
   ;
 
+  my $rs = $books_45_and_owners->search ({}, {order_by => $ord_set->{order_by}});
+
+  # query actually works
+  ok( defined $rs->count, 'Query actually works' );
+
   is_same_sql_bind(
-    $books_45_and_owners->search ({}, {order_by => $ord_set->{order_by}})->as_query,
+    $rs->as_query,
     "(SELECT me.id, me.source, me.owner, me.price, owner__id, owner__name
         FROM (
           SELECT me.id, me.source, me.owner, me.price, owner__id, owner__name$o_sel
@@ -145,6 +158,7 @@ for my $ord_set (
     [ [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'source' }
         => 'Library' ] ],
   );
+
 }
 
 # with groupby
index 539855c..acaf770 100644 (file)
@@ -3,8 +3,7 @@ use warnings;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 
 my ($LIMIT, $OFFSET) = (
index ef899ff..2d4beda 100644 (file)
@@ -4,8 +4,7 @@ use warnings;
 use Test::More;
 use lib qw(t/lib);
 use List::Util 'min';
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 my ($ROWS, $TOTAL, $OFFSET) = (
    DBIx::Class::SQLMaker::LimitDialects->__rows_bindtype,
index 7806dfb..e452953 100644 (file)
@@ -2,8 +2,7 @@ use strict;
 use warnings;
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 my $OFFSET = DBIx::Class::SQLMaker::LimitDialects->__offset_bindtype;
 my $TOTAL  = DBIx::Class::SQLMaker::LimitDialects->__total_bindtype;
index 32f67c5..b317792 100644 (file)
@@ -3,8 +3,7 @@ use warnings;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 
 my ($TOTAL, $OFFSET) = (
index b01790f..806bba4 100644 (file)
@@ -4,8 +4,7 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 
 my ($TOTAL, $OFFSET, $ROWS) = (
@@ -42,7 +41,7 @@ for my $test_set (
     sql => '(
       SELECT id, artist__id, bleh
       FROM (
-        SELECT id, artist__id, bleh, ROWNUM rownum__index
+        SELECT id, artist__id, bleh, ROWNUM AS rownum__index
         FROM (
           SELECT foo.id AS id, bar.id AS artist__id, TO_CHAR (foo.womble, "blah") AS bleh
             FROM cd me
@@ -70,7 +69,7 @@ for my $test_set (
     sql => '(
       SELECT id, artist__id, bleh
       FROM (
-        SELECT id, artist__id, bleh, ROWNUM rownum__index
+        SELECT id, artist__id, bleh, ROWNUM AS rownum__index
         FROM (
           SELECT foo.id AS id, bar.id AS artist__id, TO_CHAR(foo.womble, "blah") AS bleh
             FROM cd me
@@ -102,7 +101,7 @@ for my $test_set (
     sql => '(
       SELECT id, artist__id, bleh
       FROM (
-        SELECT id, artist__id, bleh, ROWNUM rownum__index
+        SELECT id, artist__id, bleh, ROWNUM AS rownum__index
         FROM (
           SELECT foo.id AS id, bar.id AS artist__id, TO_CHAR(foo.womble, "blah") AS bleh
             FROM cd me
@@ -130,7 +129,7 @@ for my $test_set (
     sql => '(
       SELECT id, ends_with_me__id
       FROM (
-        SELECT id, ends_with_me__id, ROWNUM rownum__index
+        SELECT id, ends_with_me__id, ROWNUM AS rownum__index
         FROM (
           SELECT foo.id AS id, ends_with_me.id AS ends_with_me__id
             FROM cd me
@@ -157,7 +156,7 @@ for my $test_set (
     sql => '(
       SELECT id, ends_with_me__id
       FROM (
-        SELECT id, ends_with_me__id, ROWNUM rownum__index
+        SELECT id, ends_with_me__id, ROWNUM AS rownum__index
         FROM (
           SELECT foo.id AS id, ends_with_me.id AS ends_with_me__id
             FROM cd me
@@ -202,7 +201,7 @@ is_same_sql_bind(
   '(
     SELECT owner_name, owner_books
       FROM (
-        SELECT owner_name, owner_books, ROWNUM rownum__index
+        SELECT owner_name, owner_books, ROWNUM AS rownum__index
           FROM (
             SELECT  owner.name AS owner_name,
               ( SELECT COUNT( * ) FROM owners owner WHERE (count.id = owner.id)) AS owner_books
index ba2d8cf..a87b95e 100644 (file)
@@ -3,8 +3,7 @@ use warnings;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::LimitDialects;
 
 my ($LIMIT, $OFFSET) = (
index 88c99a6..3fb03d9 100644 (file)
@@ -3,8 +3,7 @@ use warnings;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema;
 
index f4e7d1d..4dac672 100644 (file)
@@ -3,17 +3,18 @@ use warnings;
 
 use Test::More;
 use Test::Exception;
-use Storable 'dclone';
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
+use DBIx::Class::_Util 'deep_clone';
 
 my $schema = DBICTest->init_schema;
 my $native_limit_dialect = $schema->storage->sql_maker->{limit_dialect};
 
+my $where_string = 'me.title = ? AND source != ? AND source = ?';
+
 my @where_bind = (
-  [ {} => 'Study' ],
   [ {} => 'kama sutra' ],
+  [ {} => 'Study' ],
   [ { sqlt_datatype => 'varchar', sqlt_size => 100, dbic_colname => 'source' } => 'Library' ],
 );
 my @select_bind = (
@@ -36,17 +37,23 @@ my @order_bind = (
 my $tests = {
 
   LimitOffset => {
+    limit_plain => [
+      "( SELECT me.artistid FROM artist me LIMIT ? )",
+      [
+        [ { sqlt_datatype => 'integer' } => 5 ]
+      ],
+    ],
     limit => [
-      '(
+      "(
         SELECT me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
         LIMIT ?
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -56,17 +63,17 @@ my $tests = {
       ],
     ],
     limit_offset => [
-      '(
+      "(
         SELECT me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
         LIMIT ?
         OFFSET ?
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -77,17 +84,17 @@ my $tests = {
       ],
     ],
     ordered_limit => [
-      '(
+      "(
         SELECT me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
         ORDER BY ? / ?, ?
         LIMIT ?
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -98,18 +105,18 @@ my $tests = {
       ]
     ],
     ordered_limit_offset => [
-      '(
+      "(
         SELECT me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
         ORDER BY ? / ?, ?
         LIMIT ?
         OFFSET ?
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -121,7 +128,7 @@ my $tests = {
       ],
     ],
     limit_offset_prefetch => [
-      '(
+      "(
         SELECT me.name, books.id, books.source, books.owner, books.title, books.price
           FROM (
             SELECT me.name, me.id
@@ -130,7 +137,7 @@ my $tests = {
           ) me
           LEFT JOIN books books
             ON books.owner = me.id
-      )',
+      )",
       [
         [ { sqlt_datatype => 'integer' } => 3 ],
         [ { sqlt_datatype => 'integer' } => 1 ],
@@ -139,18 +146,24 @@ my $tests = {
   },
 
   LimitXY => {
+    limit_plain => [
+      "( SELECT me.artistid FROM artist me LIMIT ? )",
+      [
+        [ { sqlt_datatype => 'integer' } => 5 ]
+      ],
+    ],
     ordered_limit_offset => [
-      '(
+      "(
         SELECT me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
         ORDER BY ? / ?, ?
         LIMIT ?, ?
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -162,7 +175,7 @@ my $tests = {
       ],
     ],
     limit_offset_prefetch => [
-      '(
+      "(
         SELECT me.name, books.id, books.source, books.owner, books.title, books.price
           FROM (
             SELECT me.name, me.id
@@ -171,7 +184,7 @@ my $tests = {
           ) me
           LEFT JOIN books books
             ON books.owner = me.id
-      )',
+      )",
       [
         [ { sqlt_datatype => 'integer' } => 1 ],
         [ { sqlt_datatype => 'integer' } => 3 ],
@@ -180,17 +193,23 @@ my $tests = {
   },
 
   SkipFirst => {
+    limit_plain => [
+      "( SELECT FIRST ? me.artistid FROM artist me )",
+      [
+        [ { sqlt_datatype => 'integer' } => 5 ]
+      ],
+    ],
     ordered_limit_offset => [
-      '(
+      "(
         SELECT SKIP ? FIRST ? me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
         ORDER BY ? / ?, ?
-      )',
+      )",
       [
         [ { sqlt_datatype => 'integer' } => 3 ],
         [ { sqlt_datatype => 'integer' } => 4 ],
@@ -202,7 +221,7 @@ my $tests = {
       ],
     ],
     limit_offset_prefetch => [
-      '(
+      "(
         SELECT me.name, books.id, books.source, books.owner, books.title, books.price
           FROM (
             SELECT SKIP ? FIRST ? me.name, me.id
@@ -210,7 +229,7 @@ my $tests = {
           ) me
           LEFT JOIN books books
             ON books.owner = me.id
-      )',
+      )",
       [
         [ { sqlt_datatype => 'integer' } => 1 ],
         [ { sqlt_datatype => 'integer' } => 3 ],
@@ -219,17 +238,23 @@ my $tests = {
   },
 
   FirstSkip => {
+    limit_plain => [
+      "( SELECT FIRST ? me.artistid FROM artist me )",
+      [
+        [ { sqlt_datatype => 'integer' } => 5 ]
+      ],
+    ],
     ordered_limit_offset => [
-      '(
+      "(
         SELECT FIRST ? SKIP ? me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
         ORDER BY ? / ?, ?
-      )',
+      )",
       [
         [ { sqlt_datatype => 'integer' } => 4 ],
         [ { sqlt_datatype => 'integer' } => 3 ],
@@ -241,7 +266,7 @@ my $tests = {
       ],
     ],
     limit_offset_prefetch => [
-      '(
+      "(
         SELECT me.name, books.id, books.source, books.owner, books.title, books.price
           FROM (
             SELECT FIRST ? SKIP ? me.name, me.id
@@ -249,7 +274,7 @@ my $tests = {
           ) me
           LEFT JOIN books books
             ON books.owner = me.id
-      )',
+      )",
       [
         [ { sqlt_datatype => 'integer' } => 3 ],
         [ { sqlt_datatype => 'integer' } => 1 ],
@@ -258,7 +283,7 @@ my $tests = {
   },
 
   RowNumberOver => do {
-    my $unordered_sql = '(
+    my $unordered_sql = "(
       SELECT me.id, owner__id, owner__name, bar, baz
         FROM (
           SELECT me.id, owner__id, owner__name, bar, baz, ROW_NUMBER() OVER() AS rno__row__index
@@ -267,15 +292,15 @@ my $tests = {
                 FROM books me
                 JOIN owners owner
                   ON owner.id = me.owner
-              WHERE source != ? AND me.title = ? AND source = ?
+              WHERE $where_string
               GROUP BY (me.id / ?), owner.id
               HAVING ?
             ) me
       ) me
       WHERE rno__row__index >= ? AND rno__row__index <= ?
-    )';
+    )";
 
-    my $ordered_sql = '(
+    my $ordered_sql = "(
       SELECT me.id, owner__id, owner__name, bar, baz
         FROM (
           SELECT me.id, owner__id, owner__name, bar, baz, ROW_NUMBER() OVER( ORDER BY ORDER__BY__001, ORDER__BY__002 ) AS rno__row__index
@@ -285,15 +310,32 @@ my $tests = {
                 FROM books me
                 JOIN owners owner
                   ON owner.id = me.owner
-              WHERE source != ? AND me.title = ? AND source = ?
+              WHERE $where_string
               GROUP BY (me.id / ?), owner.id
               HAVING ?
             ) me
       ) me
       WHERE rno__row__index >= ? AND rno__row__index <= ?
-    )';
+    )";
 
     {
+      limit_plain => [
+        "(
+          SELECT me.artistid
+            FROM (
+              SELECT me.artistid, ROW_NUMBER() OVER(  ) AS rno__row__index
+                FROM (
+                  SELECT me.artistid
+                    FROM artist me
+                ) me
+            ) me
+          WHERE rno__row__index >= ? AND rno__row__index <= ?
+        )",
+        [
+          [ { sqlt_datatype => 'integer' } => 1 ],
+          [ { sqlt_datatype => 'integer' } => 5 ],
+        ],
+      ],
       limit => [$unordered_sql,
         [
           @select_bind,
@@ -337,7 +379,7 @@ my $tests = {
         ],
       ],
       limit_offset_prefetch => [
-        '(
+        "(
           SELECT me.name, books.id, books.source, books.owner, books.title, books.price
             FROM (
               SELECT me.name, me.id
@@ -351,7 +393,7 @@ my $tests = {
             ) me
             LEFT JOIN books books
               ON books.owner = me.id
-        )',
+        )",
         [
           [ { sqlt_datatype => 'integer' } => 2 ],
           [ { sqlt_datatype => 'integer' } => 4 ],
@@ -362,23 +404,36 @@ my $tests = {
 
   RowNum => do {
     my $limit_sql = sub {
-      sprintf '(
+      sprintf "(
         SELECT me.id, owner__id, owner__name, bar, baz
           FROM (
             SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz
               FROM books me
               JOIN owners owner
                 ON owner.id = me.owner
-            WHERE source != ? AND me.title = ? AND source = ?
+            WHERE $where_string
             GROUP BY (me.id / ?), owner.id
             HAVING ?
             %s
           ) me
         WHERE ROWNUM <= ?
-      )', $_[0] || '';
+      )", $_[0] || '';
     };
 
     {
+      limit_plain => [
+        "(
+          SELECT me.artistid
+            FROM (
+              SELECT me.artistid
+                FROM artist me
+            ) me
+          WHERE ROWNUM <= ?
+        )",
+        [
+          [ { sqlt_datatype => 'integer' } => 5 ],
+        ],
+      ],
       limit => [ $limit_sql->(),
         [
           @select_bind,
@@ -389,22 +444,22 @@ my $tests = {
         ],
       ],
       limit_offset => [
-        '(
+        "(
           SELECT me.id, owner__id, owner__name, bar, baz
             FROM (
-              SELECT me.id, owner__id, owner__name, bar, baz, ROWNUM rownum__index
+              SELECT me.id, owner__id, owner__name, bar, baz, ROWNUM AS rownum__index
                 FROM (
                   SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz
                     FROM books me
                     JOIN owners owner
                       ON owner.id = me.owner
-                  WHERE source != ? AND me.title = ? AND source = ?
+                  WHERE $where_string
                   GROUP BY (me.id / ?), owner.id
                   HAVING ?
                 ) me
             ) me
           WHERE rownum__index BETWEEN ? AND ?
-        )',
+        )",
         [
           @select_bind,
           @where_bind,
@@ -425,16 +480,16 @@ my $tests = {
         ],
       ],
       ordered_limit_offset => [
-        '(
+        "(
           SELECT me.id, owner__id, owner__name, bar, baz
             FROM (
-              SELECT me.id, owner__id, owner__name, bar, baz, ROWNUM rownum__index
+              SELECT me.id, owner__id, owner__name, bar, baz, ROWNUM AS rownum__index
                 FROM (
                   SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz
                     FROM books me
                     JOIN owners owner
                       ON owner.id = me.owner
-                  WHERE source != ? AND me.title = ? AND source = ?
+                  WHERE $where_string
                   GROUP BY (me.id / ?), owner.id
                   HAVING ?
                   ORDER BY ? / ?, ?
@@ -442,7 +497,7 @@ my $tests = {
               WHERE ROWNUM <= ?
             ) me
           WHERE rownum__index >= ?
-        )',
+        )",
         [
           @select_bind,
           @where_bind,
@@ -454,12 +509,12 @@ my $tests = {
         ],
       ],
       limit_offset_prefetch => [
-        '(
+        "(
           SELECT me.name, books.id, books.source, books.owner, books.title, books.price
             FROM (
               SELECT me.name, me.id
                 FROM (
-                  SELECT me.name, me.id, ROWNUM rownum__index
+                  SELECT me.name, me.id, ROWNUM AS rownum__index
                     FROM (
                       SELECT me.name, me.id
                         FROM owners me
@@ -468,7 +523,7 @@ my $tests = {
             ) me
             LEFT JOIN books books
               ON books.owner = me.id
-        )',
+        )",
         [
           [ { sqlt_datatype => 'integer' } => 2 ],
           [ { sqlt_datatype => 'integer' } => 4 ],
@@ -478,17 +533,21 @@ my $tests = {
   },
 
   FetchFirst => {
+    limit_plain => [
+      "( SELECT me.artistid FROM artist me FETCH FIRST 5 ROWS ONLY )",
+      [],
+    ],
     limit => [
-      '(
+      "(
         SELECT me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
         FETCH FIRST 4 ROWS ONLY
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -497,14 +556,14 @@ my $tests = {
       ],
     ],
     limit_offset => [
-      '(
+      "(
         SELECT me.id, owner__id, owner__name, bar, baz
           FROM (
             SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz
               FROM books me
               JOIN owners owner
                 ON owner.id = me.owner
-            WHERE source != ? AND me.title = ? AND source = ?
+            WHERE $where_string
             GROUP BY (me.id / ?), owner.id
             HAVING ?
             ORDER BY me.id
@@ -512,7 +571,7 @@ my $tests = {
           ) me
         ORDER BY me.id DESC
         FETCH FIRST 4 ROWS ONLY
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -521,17 +580,17 @@ my $tests = {
       ],
     ],
     ordered_limit => [
-      '(
+      "(
         SELECT me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
         ORDER BY ? / ?, ?
         FETCH FIRST 4 ROWS ONLY
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -541,7 +600,7 @@ my $tests = {
       ],
     ],
     ordered_limit_offset => [
-      '(
+      "(
         SELECT me.id, owner__id, owner__name, bar, baz
           FROM (
             SELECT me.id, owner__id, owner__name, bar, baz, ORDER__BY__001, ORDER__BY__002
@@ -550,7 +609,7 @@ my $tests = {
                   FROM books me
                   JOIN owners owner
                     ON owner.id = me.owner
-                WHERE source != ? AND me.title = ? AND source = ?
+                WHERE $where_string
                 GROUP BY (me.id / ?), owner.id
                 HAVING ?
                 ORDER BY ? / ?, ?
@@ -560,18 +619,18 @@ my $tests = {
             FETCH FIRST 4 ROWS ONLY
           ) me
         ORDER BY ORDER__BY__001, ORDER__BY__002
-      )',
+      )",
       [
         @select_bind,
         @order_bind,
         @where_bind,
         @group_bind,
         @having_bind,
-        @{ dclone \@order_bind },  # without this is_deeply throws a fit
+        @{ deep_clone \@order_bind },  # without this is_deeply throws a fit
       ],
     ],
     limit_offset_prefetch => [
-      '(
+      "(
         SELECT me.name, books.id, books.source, books.owner, books.title, books.price
           FROM (
             SELECT me.name, me.id
@@ -586,22 +645,26 @@ my $tests = {
           ) me
           LEFT JOIN books books
             ON books.owner = me.id
-      )',
+      )",
       [],
     ],
   },
 
   Top => {
+    limit_plain => [
+      "( SELECT TOP 5 me.artistid FROM artist me )",
+      [],
+    ],
     limit => [
-      '(
+      "(
         SELECT TOP 4 me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -610,20 +673,20 @@ my $tests = {
       ],
     ],
     limit_offset => [
-      '(
+      "(
         SELECT TOP 4 me.id, owner__id, owner__name, bar, baz
           FROM (
             SELECT TOP 7 me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz
               FROM books me
               JOIN owners owner
                 ON owner.id = me.owner
-            WHERE source != ? AND me.title = ? AND source = ?
+            WHERE $where_string
             GROUP BY (me.id / ?), owner.id
             HAVING ?
             ORDER BY me.id
           ) me
         ORDER BY me.id DESC
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -632,16 +695,16 @@ my $tests = {
       ],
     ],
     ordered_limit => [
-      '(
+      "(
         SELECT TOP 4 me.id, owner.id, owner.name, ? * ?, ?
           FROM books me
           JOIN owners owner
             ON owner.id = me.owner
-        WHERE source != ? AND me.title = ? AND source = ?
+        WHERE $where_string
         GROUP BY (me.id / ?), owner.id
         HAVING ?
         ORDER BY ? / ?, ?
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -651,7 +714,7 @@ my $tests = {
       ],
     ],
     ordered_limit_offset => [
-      '(
+      "(
         SELECT me.id, owner__id, owner__name, bar, baz
           FROM (
             SELECT TOP 4 me.id, owner__id, owner__name, bar, baz, ORDER__BY__001, ORDER__BY__002
@@ -660,7 +723,7 @@ my $tests = {
                   FROM books me
                   JOIN owners owner
                     ON owner.id = me.owner
-                WHERE source != ? AND me.title = ? AND source = ?
+                WHERE $where_string
                 GROUP BY (me.id / ?), owner.id
                 HAVING ?
                 ORDER BY ? / ?, ?
@@ -668,18 +731,18 @@ my $tests = {
             ORDER BY ORDER__BY__001 DESC, ORDER__BY__002 DESC
           ) me
         ORDER BY ORDER__BY__001, ORDER__BY__002
-      )',
+      )",
       [
         @select_bind,
         @order_bind,
         @where_bind,
         @group_bind,
         @having_bind,
-        @{ dclone \@order_bind },  # without this is_deeply throws a fit
+        @{ deep_clone \@order_bind },  # without this is_deeply throws a fit
       ],
     ],
     limit_offset_prefetch => [
-      '(
+      "(
         SELECT me.name, books.id, books.source, books.owner, books.title, books.price
           FROM (
             SELECT TOP 3 me.name, me.id
@@ -692,21 +755,40 @@ my $tests = {
           ) me
           LEFT JOIN books books
             ON books.owner = me.id
-      )',
+      )",
       [],
     ],
   },
 
   GenericSubQ => {
+    limit_plain => [
+      "(
+        SELECT me.artistid
+          FROM (
+            SELECT me.artistid
+              FROM artist me
+          ) me
+        WHERE
+          (
+            SELECT COUNT(*)
+              FROM artist rownum__emulation
+            WHERE rownum__emulation.artistid < me.artistid
+          ) < ?
+        ORDER BY me.artistid ASC
+      )",
+      [
+        [ { sqlt_datatype => 'integer' } => 5 ]
+      ],
+    ],
     ordered_limit => [
-      '(
+      "(
         SELECT me.id, owner__id, owner__name, bar, baz
           FROM (
             SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz, me.price
               FROM books me
               JOIN owners owner
                 ON owner.id = me.owner
-            WHERE source != ? AND me.title = ? AND source = ?
+            WHERE $where_string
             GROUP BY (me.id / ?), owner.id
             HAVING ?
           ) me
@@ -735,7 +817,7 @@ my $tests = {
             )
           ) < ?
         ORDER BY me.price DESC, me.id ASC
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -745,14 +827,14 @@ my $tests = {
       ],
     ],
     ordered_limit_offset => [
-      '(
+      "(
         SELECT me.id, owner__id, owner__name, bar, baz
           FROM (
             SELECT me.id, owner.id AS owner__id, owner.name AS owner__name, ? * ? AS bar, ? AS baz, me.price
               FROM books me
               JOIN owners owner
                 ON owner.id = me.owner
-            WHERE source != ? AND me.title = ? AND source = ?
+            WHERE $where_string
             GROUP BY (me.id / ?), owner.id
             HAVING ?
           ) me
@@ -781,7 +863,7 @@ my $tests = {
             )
           ) BETWEEN ? AND ?
         ORDER BY me.price DESC, me.id ASC
-      )',
+      )",
       [
         @select_bind,
         @where_bind,
@@ -792,7 +874,7 @@ my $tests = {
       ],
     ],
     limit_offset_prefetch => [
-      '(
+      "(
         SELECT me.name, books.id, books.source, books.owner, books.title, books.price
           FROM (
             SELECT me.name, me.id
@@ -819,7 +901,7 @@ my $tests = {
           LEFT JOIN books books
             ON books.owner = me.id
         ORDER BY me.name ASC, me.id DESC
-      )',
+      )",
       [
         [ { sqlt_datatype => 'integer' } => 1 ],
         [ { sqlt_datatype => 'integer' } => 3 ],
@@ -835,7 +917,25 @@ for my $limtype (sort keys %$tests) {
   delete $schema->storage->_sql_maker->{_cached_syntax};
   $schema->storage->_sql_maker->limit_dialect ($limtype);
 
-  my $can_run = ($limtype eq $native_limit_dialect or $limtype eq 'GenericSubQ');
+  # do the simplest thing possible first
+  if ($tests->{$limtype}{limit_plain}) {
+    is_same_sql_bind(
+      $schema->resultset('Artist')->search(
+        [ -and => [ {}, [] ], -or => [ {}, [] ] ],
+        {
+          columns => 'artistid',
+          join => [ {}, [ [ {}, {} ] ], {} ],
+          prefetch => [ [ [ {}, [] ], {} ], {}, [ {} ] ],
+          order_by => ( $limtype eq 'GenericSubQ' ? 'artistid' : [] ),
+          group_by => [],
+          rows => 5,
+          offset => 0,
+        }
+      )->as_query,
+      @{$tests->{$limtype}{limit_plain}},
+      "$limtype: Plain unordered ungrouped select with limit and no offset",
+    )
+  }
 
   # chained search is necessary to exercise the recursive {where} parser
   my $rs = $schema->resultset('BooksInLibrary')->search(
@@ -855,6 +955,7 @@ for my $limtype (sort keys %$tests) {
   #
   # not all tests run on all dialects (somewhere impossible, somewhere makes no sense)
   #
+  my $can_run = ($limtype eq $native_limit_dialect or $limtype eq 'GenericSubQ');
 
   # only limit, no offset, no order
   if ($tests->{$limtype}{limit}) {
index 2805d03..179b3f3 100644 (file)
@@ -2,8 +2,7 @@ use strict;
 use warnings;
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 # the entire point of the subclass is that parenthesis have to be
 # just right for ACCESS to be happy
@@ -87,7 +86,7 @@ my ($sql, @bind) = $sa->select(
         { me => "cd" },
         [
             { "-join_type" => "LEFT", artist => "artist" },
-            { "artist.artistid" => "me.artist" },
+            { "artist.artistid" => { -ident => "me.artist" } },
         ],
     ],
     [ 'cd.cdid', 'cd.artist', 'cd.title', 'cd.year', 'artist.artistid', 'artist.name' ],
@@ -105,11 +104,11 @@ is_same_sql_bind(
         { me => "cd" },
         [
             { "-join_type" => "LEFT", track => "track" },
-            { "track.cd" => "me.cdid" },
+            { "track.cd" => { -ident => "me.cdid" } },
         ],
         [
             { artist => "artist" },
-            { "artist.artistid" => "me.artist" },
+            { "artist.artistid" => { -ident => "me.artist" } },
         ],
     ],
     [ 'track.title', 'cd.cdid', 'cd.artist', 'cd.title', 'cd.year', 'artist.artistid', 'artist.name' ],
index 2755a3d..0e2ad29 100644 (file)
@@ -4,66 +4,49 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBICTest::Schema;
-use DBIC::SqlMakerTest;
-use DBIC::DebugObj;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest::Schema->connect (DBICTest->_database, { quote_char => '`' });
 # cheat
 require DBIx::Class::Storage::DBI::mysql;
+*DBIx::Class::Storage::DBI::mysql::_get_server_version = sub { 5 };
 bless ( $schema->storage, 'DBIx::Class::Storage::DBI::mysql' );
 
 # check that double-subqueries are properly wrapped
 {
-  my ($sql, @bind);
-  my $debugobj = DBIC::DebugObj->new (\$sql, \@bind);
-  my $orig_debugobj = $schema->storage->debugobj;
-  my $orig_debug = $schema->storage->debug;
-
-  $schema->storage->debugobj ($debugobj);
-  $schema->storage->debug (1);
-
   # the expected SQL may seem wastefully nonsensical - this is due to
   # CD's tablename being \'cd', which triggers the "this can be anything"
   # mode, and forces a subquery. This in turn forces *another* subquery
   # because mysql is being mysql
   # Also we know it will fail - never deployed. All we care about is the
-  # SQL to compare
-  eval { $schema->resultset ('CD')->update({ genreid => undef }) };
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  # SQL to compare, hence the eval
+  $schema->is_executed_sql_bind( sub {
+    eval { $schema->resultset ('CD')->update({ genreid => undef }) }
+  },[[
     'UPDATE cd SET `genreid` = ? WHERE `cdid` IN ( SELECT * FROM ( SELECT `me`.`cdid` FROM cd `me` ) `_forced_double_subquery` )',
-    [ 'NULL' ],
-    'Correct update-SQL with double-wrapped subquery',
-  );
+    [ { dbic_colname => "genreid", sqlt_datatype => "integer" }  => undef ],
+  ]], 'Correct update-SQL with double-wrapped subquery' );
 
   # same comment as above
-  eval { $schema->resultset ('CD')->delete };
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    eval { $schema->resultset ('CD')->delete }
+  }, [[
     'DELETE FROM cd WHERE `cdid` IN ( SELECT * FROM ( SELECT `me`.`cdid` FROM cd `me` ) `_forced_double_subquery` )',
-    [],
-    'Correct delete-SQL with double-wrapped subquery',
-  );
+  ]], 'Correct delete-SQL with double-wrapped subquery' );
 
   # and a couple of really contrived examples (we test them live in t/71mysql.t)
   my $rs = $schema->resultset('Artist')->search({ name => { -like => 'baby_%' } });
   my ($count_sql, @count_bind) = @${$rs->count_rs->as_query};
-  eval {
-    $schema->resultset('Artist')->search(
-      { artistid => {
-        -in => $rs->get_column('artistid')
-                    ->as_query
-      } },
-    )->update({ name => \[ "CONCAT( `name`, '_bell_out_of_', $count_sql )", @count_bind ] });
-  };
-
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    eval {
+      $schema->resultset('Artist')->search(
+        { artistid => {
+          -in => $rs->get_column('artistid')
+                      ->as_query
+        } },
+      )->update({ name => \[ "CONCAT( `name`, '_bell_out_of_', $count_sql )", @count_bind ] });
+    }
+  }, [[
     q(
       UPDATE `artist`
         SET `name` = CONCAT(`name`, '_bell_out_of_', (
@@ -83,18 +66,18 @@ bless ( $schema->storage, 'DBIx::Class::Storage::DBI::mysql' );
               WHERE `name` LIKE ?
             ) `_forced_double_subquery` )
     ),
-    [ ("'baby_%'") x 2 ],
-  );
+    ( [ { dbic_colname => "name", sqlt_datatype => "varchar", sqlt_size => 100 }
+        => 'baby_%' ]
+    ) x 2
+  ]]);
 
-  eval {
-    $schema->resultset('CD')->search_related('artist',
-      { 'artist.name' => { -like => 'baby_with_%' } }
-    )->delete
-  };
-
-  is_same_sql_bind (
-    $sql,
-    \@bind,
+  $schema->is_executed_sql_bind( sub {
+    eval {
+      $schema->resultset('CD')->search_related('artist',
+        { 'artist.name' => { -like => 'baby_with_%' } }
+      )->delete
+    }
+  }, [[
     q(
       DELETE FROM `artist`
       WHERE `artistid` IN (
@@ -102,17 +85,15 @@ bless ( $schema->storage, 'DBIx::Class::Storage::DBI::mysql' );
           FROM (
             SELECT `artist`.`artistid`
               FROM cd `me`
-              INNER JOIN `artist` `artist`
+              JOIN `artist` `artist`
                 ON `artist`.`artistid` = `me`.`artist`
             WHERE `artist`.`name` LIKE ?
           ) `_forced_double_subquery`
       )
     ),
-    [ "'baby_with_%'" ],
-  );
-
-  $schema->storage->debugobj ($orig_debugobj);
-  $schema->storage->debug ($orig_debug);
+    [ { dbic_colname => "artist.name", sqlt_datatype => "varchar", sqlt_size => 100 }
+        => 'baby_with_%' ],
+  ]] );
 }
 
 # Test support for straight joins
@@ -138,4 +119,37 @@ bless ( $schema->storage, 'DBIx::Class::Storage::DBI::mysql' );
   );
 }
 
+# Test support for inner joins on mysql v3
+for (
+  [ 3 => 'INNER JOIN' ],
+  [ 4 => 'JOIN' ],
+) {
+  my ($ver, $join_op) = @$_;
+
+  # we do not care at this point if data is available, just do a reconnect cycle
+  # to clear the server version cache and then get a new maker
+  {
+    $schema->storage->disconnect;
+    $schema->storage->_sql_maker(undef);
+
+    no warnings 'redefine';
+    local *DBIx::Class::Storage::DBI::mysql::_get_server_version = sub { $ver };
+
+    $schema->storage->ensure_connected;
+    $schema->storage->sql_maker;
+  }
+
+  is_same_sql_bind (
+    $schema->resultset('CD')->search ({}, { prefetch => 'artist' })->as_query,
+    "(
+      SELECT `me`.`cdid`, `me`.`artist`, `me`.`title`, `me`.`year`, `me`.`genreid`, `me`.`single_track`,
+             `artist`.`artistid`, `artist`.`name`, `artist`.`rank`, `artist`.`charfield`
+        FROM cd `me`
+        $join_op `artist` `artist` ON `artist`.`artistid` = `me`.`artist`
+    )",
+    [],
+    "default join type works for version $ver",
+  );
+}
+
 done_testing;
index f00443a..a6edeee 100644 (file)
@@ -5,8 +5,7 @@ use Test::More;
 use Test::Warn;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
index 69234f9..cd3e629 100644 (file)
@@ -1,18 +1,13 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'id_shortener';
+
 use strict;
 use warnings;
-use Test::More;
-
-BEGIN {
-  require DBIx::Class::Optional::Dependencies;
-  plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('id_shortener')
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('id_shortener');
-}
 
+use Test::More;
 use Test::Exception;
 use Data::Dumper::Concise;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::Oracle;
 
 #
index 3ba82ab..11298b0 100644 (file)
@@ -1,28 +1,25 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'id_shortener';
+
 use strict;
 use warnings;
 
 use Test::More;
 
-BEGIN {
-  require DBIx::Class::Optional::Dependencies;
-  plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('id_shortener')
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('id_shortener');
-}
-
 use lib qw(t/lib);
-use DBICTest;
+use DBICTest ':DiffSQL';
 use DBIx::Class::SQLMaker::OracleJoins;
-use DBIC::SqlMakerTest;
 
 my $sa = DBIx::Class::SQLMaker::OracleJoins->new;
 
+for my $rhs ( "me.artist", { -ident => "me.artist" } ) {
+
 #  my ($self, $table, $fields, $where, $order, @rest) = @_;
 my ($sql, @bind) = $sa->select(
     [
         { me => "cd" },
         [
             { "-join_type" => "LEFT", artist => "artist" },
-            { "artist.artistid" => "me.artist" },
+            { "artist.artistid" => $rhs },
         ],
     ],
     [ 'cd.cdid', 'cd.artist', 'cd.title', 'cd.year', 'artist.artistid', 'artist.name' ],
@@ -40,7 +37,7 @@ is_same_sql_bind(
         { me => "cd" },
         [
             { "-join_type" => "", artist => "artist" },
-            { "artist.artistid" => "me.artist" },
+            { "artist.artistid" => $rhs },
         ],
     ],
     [ 'cd.cdid', 'cd.artist', 'cd.title', 'cd.year', 'artist.artistid', 'artist.name' ],
@@ -57,8 +54,26 @@ is_same_sql_bind(
     [
         { me => "cd" },
         [
+            { "-join_type" => "right", artist => "artist" },
+            { "artist.artistid" => $rhs },
+        ],
+    ],
+    [ 'cd.cdid', 'cd.artist', 'cd.title', 'cd.year', 'artist.artistid', 'artist.name' ],
+    { 'artist.artistid' => 3 },
+    undef
+);
+is_same_sql_bind(
+  $sql, \@bind,
+  'SELECT cd.cdid, cd.artist, cd.title, cd.year, artist.artistid, artist.name FROM cd me, artist artist WHERE ( ( ( artist.artistid = me.artist(+) ) AND ( artist.artistid = ? ) ) )', [3],
+  'WhereJoins search with where clause'
+);
+
+($sql, @bind) = $sa->select(
+    [
+        { me => "cd" },
+        [
             { "-join_type" => "LEFT", artist => "artist" },
-            { "artist.artistid" => "me.artist" },
+            { "artist.artistid" => $rhs },
         ],
     ],
     [ 'cd.cdid', 'cd.artist', 'cd.title', 'cd.year', 'artist.artistid', 'artist.name' ],
@@ -71,5 +86,7 @@ is_same_sql_bind(
   'WhereJoins search with or in where clause'
 );
 
+}
+
 done_testing;
 
index b612375..24da80e 100644 (file)
@@ -5,8 +5,7 @@ use Test::More;
 use Test::Exception;
 use Data::Dumper::Concise;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 sub test_order {
     my $rs = shift;
index 51968ed..9609219 100644 (file)
@@ -3,8 +3,7 @@ use warnings;
 use Test::More;
 
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema();
 
diff --git a/t/sqlmaker/quotes.t b/t/sqlmaker/quotes.t
new file mode 100644 (file)
index 0000000..4a5357b
--- /dev/null
@@ -0,0 +1,67 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib qw(t/lib);
+use DBICTest ':DiffSQL';
+
+my $schema = DBICTest->init_schema( no_deploy => 1 );
+
+$schema->connection(
+  @{ $schema->storage->_dbi_connect_info },
+  { AutoCommit => 1, quote_char => [qw/[ ]/] }
+);
+
+my $rs =  $schema->resultset('CD')->search(
+  { 'me.year' => 2001, 'artist.name' => 'Caterwauler McCrae' },
+  { join => 'artist' }
+)->count_rs;
+
+my $expected_bind = [
+  [ { dbic_colname => "artist.name", sqlt_datatype => "varchar", sqlt_size => 100 }
+    => 'Caterwauler McCrae' ],
+  [ { dbic_colname => "me.year", sqlt_datatype => "varchar", sqlt_size => 100 }
+    => 2001 ],
+];
+
+is_same_sql_bind(
+  $rs->as_query,
+  "(SELECT COUNT( * ) FROM cd [me] JOIN [artist] [artist] ON [artist].[artistid] = [me].[artist] WHERE ( [artist].[name] = ? AND [me].[year] = ? ))",
+  $expected_bind,
+  'got correct SQL for count query with bracket quoting'
+);
+
+$schema->storage->sql_maker->quote_char('`');
+$schema->storage->sql_maker->name_sep('.');
+
+is_same_sql_bind (
+  $rs->as_query,
+  "(SELECT COUNT( * ) FROM cd `me`  JOIN `artist` `artist` ON ( `artist`.`artistid` = `me`.`artist` ) WHERE ( `artist`.`name` = ? AND `me`.`year` = ? ))",
+  $expected_bind,
+  'got correct SQL for count query with mysql quoting'
+);
+
+# !!! talk to ribasushi *explicitly* before modfying these tests !!!
+{
+  is_same_sql_bind(
+    $schema->resultset('CD')->search({}, { order_by => 'year DESC', columns => 'cdid' })->as_query,
+    '(SELECT `me`.`cdid` FROM cd `me` ORDER BY `year DESC`)',
+    [],
+    'quoted ORDER BY with DESC (should use a scalarref anyway)'
+  );
+
+  is_same_sql_bind(
+    $schema->resultset('CD')->search({}, { order_by => \'year DESC', columns => 'cdid' })->as_query,
+    '(SELECT `me`.`cdid` FROM cd `me` ORDER BY year DESC)',
+    [],
+    'did not quote ORDER BY with scalarref',
+  );
+}
+
+is_same_sql(
+  scalar $schema->storage->sql_maker->update('group', { order => 12, name => 'Bill' }),
+  'UPDATE `group` SET `name` = ?, `order` = ?',
+  'quoted table names for UPDATE' );
+
+done_testing;
diff --git a/t/sqlmaker/quotes/quotes.t b/t/sqlmaker/quotes/quotes.t
deleted file mode 100644 (file)
index 3fbc94c..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-use strict;
-use warnings;
-
-use Test::More;
-
-use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
-use DBIC::DebugObj;
-
-my $schema = DBICTest->init_schema();
-
-$schema->storage->sql_maker->quote_char('`');
-$schema->storage->sql_maker->name_sep('.');
-
-my ($sql, @bind);
-$schema->storage->debugobj(DBIC::DebugObj->new(\$sql, \@bind));
-$schema->storage->debug(1);
-
-my $rs;
-
-$rs = $schema->resultset('CD')->search(
-           { 'me.year' => 2001, 'artist.name' => 'Caterwauler McCrae' },
-           { join => 'artist' });
-eval { $rs->count };
-is_same_sql_bind(
-  $sql, \@bind,
-  "SELECT COUNT( * ) FROM cd `me`  JOIN `artist` `artist` ON ( `artist`.`artistid` = `me`.`artist` ) WHERE ( `artist`.`name` = ? AND `me`.`year` = ? )", ["'Caterwauler McCrae'", "'2001'"],
-  'got correct SQL for count query with quoting'
-);
-
-my $order = 'year DESC';
-$rs = $schema->resultset('CD')->search({},
-            { 'order_by' => $order });
-eval { $rs->first };
-like($sql, qr/ORDER BY `\Q${order}\E`/, 'quoted ORDER BY with DESC (should use a scalarref anyway)');
-
-$rs = $schema->resultset('CD')->search({},
-            { 'order_by' => \$order });
-eval { $rs->first };
-like($sql, qr/ORDER BY \Q${order}\E/, 'did not quote ORDER BY with scalarref');
-
-$schema->storage->sql_maker->quote_char([qw/[ ]/]);
-$schema->storage->sql_maker->name_sep('.');
-
-$rs = $schema->resultset('CD')->search(
-           { 'me.year' => 2001, 'artist.name' => 'Caterwauler McCrae' },
-           { join => 'artist' });
-eval { $rs->count };
-is_same_sql_bind(
-  $sql, \@bind,
-  "SELECT COUNT( * ) FROM cd [me]  JOIN [artist] [artist] ON ( [artist].[artistid] = [me].[artist] ) WHERE ( [artist].[name] = ? AND [me].[year] = ? )", ["'Caterwauler McCrae'", "'2001'"],
-  'got correct SQL for count query with bracket quoting'
-);
-
-my %data = (
-       name => 'Bill',
-       order => '12'
-);
-
-$schema->storage->sql_maker->quote_char('`');
-$schema->storage->sql_maker->name_sep('.');
-
-is($schema->storage->sql_maker->update('group', \%data), 'UPDATE `group` SET `name` = ?, `order` = ?', 'quoted table names for UPDATE');
-
-done_testing;
diff --git a/t/sqlmaker/quotes/quotes_newstyle.t b/t/sqlmaker/quotes/quotes_newstyle.t
deleted file mode 100644 (file)
index 900a68a..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-use strict;
-use warnings;
-
-use Test::More;
-
-use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
-use DBIC::DebugObj;
-
-my $schema = DBICTest->init_schema();
-
-my $dsn = $schema->storage->_dbi_connect_info->[0];
-$schema->connection(
-  $dsn,
-  undef,
-  undef,
-  { AutoCommit => 1 },
-  { quote_char => '`', name_sep => '.' },
-);
-
-my ($sql, @bind);
-$schema->storage->debugobj(DBIC::DebugObj->new(\$sql, \@bind)),
-$schema->storage->debug(1);
-
-my $rs;
-
-$rs = $schema->resultset('CD')->search(
-           { 'me.year' => 2001, 'artist.name' => 'Caterwauler McCrae' },
-           { join => 'artist' });
-eval { $rs->count };
-is_same_sql_bind(
-  $sql, \@bind,
-  "SELECT COUNT( * ) FROM cd `me`  JOIN `artist` `artist` ON ( `artist`.`artistid` = `me`.`artist` ) WHERE ( `artist`.`name` = ? AND `me`.`year` = ? )", ["'Caterwauler McCrae'", "'2001'"],
-  'got correct SQL for count query with quoting'
-);
-
-my $order = 'year DESC';
-$rs = $schema->resultset('CD')->search({},
-            { 'order_by' => $order });
-eval { $rs->first };
-like($sql, qr/ORDER BY `\Q${order}\E`/, 'quoted ORDER BY with DESC (should use a scalarref anyway)');
-
-$rs = $schema->resultset('CD')->search({},
-            { 'order_by' => \$order });
-eval { $rs->first };
-like($sql, qr/ORDER BY \Q${order}\E/, 'did not quote ORDER BY with scalarref');
-
-$schema->connection(
-  $dsn,
-  undef,
-  undef,
-  { AutoCommit => 1, quote_char => [qw/[ ]/], name_sep => '.' }
-);
-
-$schema->storage->debugobj(DBIC::DebugObj->new(\$sql, \@bind)),
-$schema->storage->debug(1);
-
-$rs = $schema->resultset('CD')->search(
-           { 'me.year' => 2001, 'artist.name' => 'Caterwauler McCrae' },
-           { join => 'artist' });
-eval { $rs->count };
-is_same_sql_bind(
-  $sql, \@bind,
-  "SELECT COUNT( * ) FROM cd [me]  JOIN [artist] [artist] ON ( [artist].[artistid] = [me].[artist] ) WHERE ( [artist].[name] = ? AND [me].[year] = ? )", ["'Caterwauler McCrae'", "'2001'"],
-  'got correct SQL for count query with bracket quoting'
-);
-
-my %data = (
-       name => 'Bill',
-       order => '12'
-);
-
-$schema->connection(
-  $dsn,
-  undef,
-  undef,
-  { AutoCommit => 1, quote_char => '`', name_sep => '.' }
-);
-
-is($schema->storage->sql_maker->update('group', \%data), 'UPDATE `group` SET `name` = ?, `order` = ?', 'quoted table names for UPDATE');
-
-done_testing;
index 86fcc82..9c0b904 100644 (file)
@@ -3,8 +3,7 @@ use warnings;
 
 use Test::More;
 use lib qw(t/lib);
-use DBICTest;
-use DBIC::SqlMakerTest;
+use DBICTest ':DiffSQL';
 
 my $schema = DBICTest->init_schema;
 
index df3641e..b7650a8 100644 (file)
@@ -10,11 +10,13 @@ use Data::Dumper;
 
 my $schema = DBICTest->init_schema( sqlite_use_file => 1 );
 
-is( ref($schema->storage), 'DBIx::Class::Storage::DBI::SQLite',
-    'Storage reblessed correctly into DBIx::Class::Storage::DBI::SQLite' );
-
 my $storage = $schema->storage;
-$storage->ensure_connected;
+
+is(
+  ref($storage),
+  'DBIx::Class::Storage::DBI::SQLite',
+  'Storage reblessed correctly into DBIx::Class::Storage::DBI::SQLite'
+) unless $ENV{DBICTEST_VIA_REPLICATED};
 
 throws_ok {
     $schema->storage->throw_exception('test_exception_42');
@@ -25,6 +27,34 @@ throws_ok {
 } qr/prepare_cached failed/, 'exception via DBI->HandleError, etc';
 
 
+# make sure repeated disconnection works
+{
+  my $fn = DBICTest->_sqlite_dbfilename;
+
+  lives_ok {
+    $schema->storage->ensure_connected;
+    my $dbh = $schema->storage->dbh;
+    $schema->storage->disconnect for 1,2;
+    unlink $fn;
+    $dbh->disconnect;
+  };
+
+  lives_ok {
+    $schema->storage->ensure_connected;
+    $schema->storage->disconnect for 1,2;
+    unlink $fn;
+    $schema->storage->disconnect for 1,2;
+  };
+
+  lives_ok {
+    $schema->storage->ensure_connected;
+    $schema->storage->_dbh->disconnect;
+    unlink $fn;
+    $schema->storage->disconnect for 1,2;
+  };
+}
+
+
 # testing various invocations of connect_info ([ ... ])
 
 my $coderef = sub { 42 };
index e6c0ba7..ce0be84 100644 (file)
@@ -13,9 +13,9 @@ lives_ok {
   is($schema->resultset("Artist")->search(), 3, "Three artists returned");
 } 'Custom cursor autoloaded';
 
+# test component_class reentrancy
 SKIP: {
-  eval { require Class::Unload }
-    or skip 'component_class reentrancy test requires Class::Unload', 1;
+  DBIx::Class::Optional::Dependencies->skip_without( 'Class::Unload>=0.07' );
 
   Class::Unload->unload('DBICTest::Cursor');
 
index 0beb858..727c245 100644 (file)
@@ -9,6 +9,10 @@ use DBICTest;
 my $schema = DBICTest->init_schema();
 my $storage = $schema->storage;
 
+$storage = $storage->master
+  if $ENV{DBICTEST_VIA_REPLICATED};
+
+
 # test (re)connection
 for my $disconnect (0, 1) {
   $schema->storage->_dbh->disconnect if $disconnect;
@@ -43,7 +47,7 @@ is_deeply (
 
 # test nested aliasing
 my $res = 'original';
-$storage->dbh_do (sub {
+$schema->storage->dbh_do (sub {
   shift->dbh_do(sub { $_[3] = 'changed' }, @_)
 }, $res);
 
index 6a698ef..1a1c32e 100644 (file)
@@ -1,15 +1,12 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_prettydebug';
+
 use strict;
 use warnings;
+
 use lib qw(t/lib);
 use DBICTest;
 use Test::More;
 
-BEGIN {
-    require DBIx::Class;
-    plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_prettydebug')
-      unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_prettydebug');
-}
-
 BEGIN { delete @ENV{qw(DBIC_TRACE_PROFILE)} }
 
 {
@@ -27,7 +24,9 @@ BEGIN { delete @ENV{qw(DBIC_TRACE_PROFILE)} }
    is($schema->storage->debugobj->_sqlat->indent_string, ' ', 'indent string set correctly from console profile');
 }
 
-{
+SKIP:{
+   DBIx::Class::Optional::Dependencies->skip_without('config_file_reader' );
+
    local $ENV{DBIC_TRACE_PROFILE} = './t/lib/awesome.json';
 
    my $schema = DBICTest->init_schema;
index 6d8e94c..3f5d399 100644 (file)
@@ -2,16 +2,23 @@ use strict;
 use warnings;
 no warnings 'once';
 
+BEGIN {
+  delete @ENV{qw(
+    DBIC_TRACE
+    DBIC_TRACE_PROFILE
+    DBICTEST_SQLITE_USE_FILE
+    DBICTEST_VIA_REPLICATED
+  )};
+}
+
 use Test::More;
 use Test::Exception;
+use Try::Tiny;
+use File::Spec;
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::DebugObj;
-use DBIC::SqlMakerTest;
 use Path::Class qw/file/;
 
-BEGIN { delete @ENV{qw(DBIC_TRACE DBIC_TRACE_PROFILE DBICTEST_SQLITE_USE_FILE)} }
-
 my $schema = DBICTest->init_schema();
 
 my $lfn = file("t/var/sql-$$.log");
@@ -19,6 +26,7 @@ unlink $lfn or die $!
   if -e $lfn;
 
 # make sure we are testing the vanilla debugger and not ::PrettyPrint
+require DBIx::Class::Storage::Statistics;
 $schema->storage->debugobj(DBIx::Class::Storage::Statistics->new);
 
 ok ( $schema->storage->debug(1), 'debug' );
@@ -54,32 +62,122 @@ END {
 }
 
 open(STDERRCOPY, '>&STDERR');
-close(STDERR);
-dies_ok {
+
+my $exception_line_number;
+# STDERR will be closed, no T::B diag in blocks
+my $exception = try {
+  close(STDERR);
+  $exception_line_number = __LINE__ + 1;  # important for test, do not reformat
   $schema->resultset('CD')->search({})->count;
-} 'Died on closed FH';
+} catch {
+  $_
+} finally {
+  # restore STDERR
+  open(STDERR, '>&STDERRCOPY');
+};
+
+ok $exception =~ /
+  \QDuplication of STDERR for debug output failed (perhaps your STDERR is closed?)\E
+    .+
+  \Qat @{[__FILE__]} line $exception_line_number\E$
+/xms
+  or diag "Unexpected exception text:\n\n$exception\n";
+
+my @warnings;
+$exception = try {
+  local $SIG{__WARN__} = sub { push @warnings, @_ if $_[0] =~ /character/i };
+  close STDERR;
+  open(STDERR, '>', File::Spec->devnull) or die $!;
+  $schema->resultset('CD')->search({ title => "\x{1f4a9}" })->count;
+  '';
+} catch {
+  $_;
+} finally {
+  # restore STDERR
+  close STDERR;
+  open(STDERR, '>&STDERRCOPY');
+};
+
+die "How did that fail... $exception"
+  if $exception;
+
+is_deeply(\@warnings, [], 'No warnings with unicode on STDERR');
+
+# test debugcb and debugobj protocol
+{
+  my $rs = $schema->resultset('CD')->search( {
+    artist => 1,
+    cdid => { -between => [ 1, 3 ] },
+    title => { '!=' => \[ '?', undef ] }
+  });
+
+  my $sql_trace = 'SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track FROM cd me WHERE ( ( artist = ? AND ( cdid BETWEEN ? AND ? ) AND title != ? ) )';
+  my @bind_trace = qw( '1' '1' '3' NULL );  # quotes are in fact part of the trace </facepalm>
+
+
+  my @args;
+  $schema->storage->debugcb(sub { push @args, @_ } );
 
-open(STDERR, '>&STDERRCOPY');
+  $rs->all;
+
+  is_deeply( \@args, [
+    "SELECT",
+    sprintf( "%s: %s\n", $sql_trace, join ', ', @bind_trace ),
+  ]);
+
+  {
+    package DBICTest::DebugObj;
+    our @ISA = 'DBIx::Class::Storage::Statistics';
+
+    sub query_start {
+      my $self = shift;
+      ( $self->{_traced_sql}, @{$self->{_traced_bind}} ) = @_;
+    }
+  }
+
+  my $do = $schema->storage->debugobj(DBICTest::DebugObj->new);
+
+  $rs->all;
+
+  is( $do->{_traced_sql}, $sql_trace );
+
+  is_deeply ( $do->{_traced_bind}, \@bind_trace );
+}
 
-# test trace output correctness for bind params
+# recreate test as seen in DBIx::Class::QueryLog
+# the rationale is that if someone uses a non-IO::Handle object
+# on CPAN, many are *bound* to use one on darkpan. Thus this
+# test to ensure there is no future silent breakage
 {
-    my ($sql, @bind);
-    $schema->storage->debugobj(DBIC::DebugObj->new(\$sql, \@bind));
-
-    my @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 ?) )",
-        [qw/'1' '1' '3'/],
-        'got correct SQL with all bind parameters (debugcb)'
-    );
-
-    @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'"],
-        'got correct SQL with all bind parameters (debugobj)'
-    );
+  my $output = "";
+
+  {
+    package DBICTest::_Printable;
+
+    sub print {
+      my ($self, @args) = @_;
+      $output .= join('', @args);
+    }
+  }
+
+  $schema->storage->debugobj(undef);
+  $schema->storage->debug(1);
+  $schema->storage->debugfh( bless {}, "DBICTest::_Printable" );
+  $schema->storage->txn_do( sub { $schema->resultset('Artist')->count } );
+
+  like (
+    $output,
+    qr/
+      \A
+      ^ \QBEGIN WORK\E \s*?
+      ^ \QSELECT COUNT( * ) FROM artist me:\E \s*?
+      ^ \QCOMMIT\E \s*?
+      \z
+    /xm
+  );
+
+  $schema->storage->debug(0);
+  $schema->storage->debugfh(undef);
 }
 
 done_testing;
index 433f58e..3a1f66f 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'deploy';
+
 use strict;
 use warnings;
 
@@ -8,13 +10,6 @@ use Path::Class qw/dir/;
 use lib qw(t/lib);
 use DBICTest;
 
-BEGIN {
-  require DBIx::Class;
-  plan skip_all =>
-      'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('deploy')
-    unless DBIx::Class::Optional::Dependencies->req_ok_for ('deploy')
-}
-
 local $ENV{DBI_DSN};
 
 # this is how maint/gen_schema did it (connect() to force a storage
@@ -22,7 +17,7 @@ local $ENV{DBI_DSN};
 # there ought to be more code like this in the wild
 like(
   DBICTest::Schema->connect->deployment_statements('SQLite'),
-  qr/\bCREATE TABLE\b/i
+  qr/\bCREATE TABLE artist\b/i  # ensure quoting *is* disabled
 );
 
 lives_ok( sub {
@@ -31,18 +26,42 @@ lives_ok( sub {
     $parse_schema->resultset("Artist")->all();
 }, 'artist table deployed correctly' );
 
-my $schema = DBICTest->init_schema();
+my $schema = DBICTest->init_schema( quote_names => 1 );
 
 my $var = dir ("t/var/ddl_dir-$$");
 $var->mkpath unless -d $var;
 
 my $test_dir_1 = $var->subdir ('test1', 'foo', 'bar' );
 $test_dir_1->rmtree if -d $test_dir_1;
-$schema->create_ddl_dir( undef, undef, $test_dir_1 );
+$schema->create_ddl_dir( [qw(SQLite MySQL)], 1, $test_dir_1 );
 
 ok( -d $test_dir_1, 'create_ddl_dir did a make_path on its target dir' );
 ok( scalar( glob $test_dir_1.'/*.sql' ), 'there are sql files in there' );
 
+my $less = $schema->clone;
+$less->unregister_source('BindType');
+$less->create_ddl_dir( [qw(SQLite MySQL)], 2, $test_dir_1, 1 );
+
+for (
+  [ SQLite => '"' ],
+  [ MySQL => '`' ],
+) {
+  my $type = $_->[0];
+  my $q = quotemeta($_->[1]);
+
+  for my $f (map { $test_dir_1->file("DBICTest-Schema-${_}-$type.sql") } qw(1 2) ) {
+    like scalar $f->slurp, qr/CREATE TABLE ${q}track${q}/, "Proper quoting in $f";
+  }
+
+  {
+    local $TODO = 'SQLT::Producer::MySQL has no knowledge of the mythical beast of quoting...'
+      if $type eq 'MySQL';
+
+    my $f = $test_dir_1->file("DBICTest-Schema-1-2-$type.sql");
+    like scalar $f->slurp, qr/DROP TABLE ${q}bindtype_test${q}/, "Proper quoting in diff $f";
+  }
+}
+
 {
   local $TODO = 'we should probably add some tests here for actual deployability of the DDL?';
   ok( 0 );
index d6dcc03..494780d 100644 (file)
@@ -1,6 +1,8 @@
 use strict;
 use warnings;
 
+BEGIN { $ENV{DBICTEST_VIA_REPLICATED} = 0 }
+
 use Test::More;
 use lib qw(t/lib);
 use DBICTest;
index 61d6782..e01da70 100644 (file)
@@ -8,6 +8,61 @@ use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
 
+for my $conn_args (
+  [ on_connect_do   => "_NOPE_" ],
+  [ on_connect_call => sub { shift->_dbh->do("_NOPE_") } ],
+  [ on_connect_call => "_NOPE_" ],
+) {
+  for my $method (qw( ensure_connected _server_info _get_server_version _get_dbh )) {
+
+    my $s = DBICTest->init_schema(
+      no_deploy => 1,
+      on_disconnect_call => sub { fail 'Disconnector should not be invoked' },
+      @$conn_args
+    );
+
+    my $storage = $s->storage;
+    $storage = $storage->master if $ENV{DBICTEST_VIA_REPLICATED};
+
+    ok( ! $storage->connected, 'Starting unconnected' );
+
+    my $desc = "calling $method with broken on_connect action @{[ explain $conn_args ]}";
+
+    throws_ok { $storage->$method }
+      qr/ _NOPE_ \b/x,
+      "Throwing correctly when $desc";
+
+    ok( ! $storage->connected, "Still not connected after $desc" );
+
+    # this checks that the on_disconect_call FAIL won't trigger
+    $storage->disconnect;
+  }
+}
+
+for my $conn_args (
+  [ on_disconnect_do   => "_NOPE_" ],
+  [ on_disconnect_call => sub { shift->_dbh->do("_NOPE_") } ],
+  [ on_disconnect_call => "_NOPE_" ],
+) {
+  my $s = DBICTest->init_schema( no_deploy => 1, @$conn_args );
+
+  my $storage = $s->storage;
+  $storage = $storage->master if $ENV{DBICTEST_VIA_REPLICATED};
+
+  my $desc = "broken on_disconnect action @{[ explain $conn_args ]}";
+
+  # connect + ping
+  my $dbh = $storage->dbh;
+
+  ok ($dbh->FETCH('Active'), 'Freshly connected DBI handle is healthy');
+
+  warnings_exist { eval { $storage->disconnect } } [
+    qr/\QDisconnect action failed\E .+ _NOPE_ \b/x
+  ], "Found warning of failed $desc";
+
+  ok (! $dbh->FETCH('Active'), "Actual DBI disconnect was not prevented by $desc" );
+}
+
 my $schema = DBICTest->init_schema;
 
 warnings_are ( sub {
@@ -38,7 +93,7 @@ throws_ok (
 # exception fallback:
 
 SKIP: {
-  if (DBIx::Class::_ENV_::PEEPEENESS) {
+  if ( !!DBIx::Class::_ENV_::PEEPEENESS ) {
     skip "Your perl version $] appears to leak like a sieve - skipping garbage collected \$schema test", 1;
   }
 
index 4fb49cb..6bddfd7 100644 (file)
@@ -3,6 +3,9 @@ use warnings;
 
 use Test::More;
 
+# so we can see the retry exceptions (if any)
+BEGIN { $ENV{DBIC_STORAGE_RETRY_DEBUG} = 1 }
+
 use DBIx::Class::Optional::Dependencies ();
 
 use lib qw(t/lib);
@@ -35,9 +38,6 @@ for my $type (qw/PG MYSQL SQLite/) {
     # to induce out-of-order destruction
     $DBICTest::FakeSchemaFactory::schema = $schema;
 
-    # so we can see the retry exceptions (if any)
-    $ENV{DBIC_DBIRETRY_DEBUG} = 1;
-
     ok (!$schema->storage->connected, "$type: start disconnected");
 
     $schema->txn_do (sub {
index d2dd840..b229756 100644 (file)
@@ -4,17 +4,14 @@ use warnings;
 use Test::More;
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::DebugObj;
-use DBIC::SqlMakerTest;
-use DBI::Const::GetInfoType;
 
 { # Fake storage driver for SQLite + no bind variables
   package DBICTest::SQLite::NoBindVars;
-    use Class::C3;
-    use base qw/
-        DBIx::Class::Storage::DBI::NoBindVars
-        DBIx::Class::Storage::DBI::SQLite
-    /;
+  use base qw(
+    DBIx::Class::Storage::DBI::NoBindVars
+    DBIx::Class::Storage::DBI::SQLite
+  );
+  use mro 'c3';
 }
 
 my $schema = DBICTest->init_schema (storage_type => 'DBICTest::SQLite::NoBindVars', no_populate => 1);
@@ -35,26 +32,13 @@ my $it = $schema->resultset('Artist')->search( {},
 
 is( $it->count, 3, "LIMIT count ok" );  # ask for 3 rows out of 7 artists
 
-my ($sql, @bind);
-my $orig_debugobj = $schema->storage->debugobj;
-my $orig_debug = $schema->storage->debug;
-$schema->storage->debugobj (DBIC::DebugObj->new (\$sql, \@bind) );
-$schema->storage->debug (1);
-
-is( $it->next->name, "Artist 2", "iterator->next ok" );
-$it->next;
-$it->next;
-is( $it->next, undef, "next past end of resultset ok" );
-
-$schema->storage->debugobj ($orig_debugobj);
-$schema->storage->debug ($orig_debug);
-
-is_same_sql_bind (
-  $sql,
-  \@bind,
-  'SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me ORDER BY artistid LIMIT 3 OFFSET 2',
-  [],
-  'Correctly interpolated SQL'
-);
+$schema->is_executed_sql_bind( sub {
+  is( $it->next->name, "Artist 2", "iterator->next ok" );
+  $it->next;
+  $it->next;
+  is( $it->next, undef, "next past end of resultset ok" );
+}, [
+  [ 'SELECT me.artistid, me.name, me.rank, me.charfield FROM artist me ORDER BY artistid LIMIT 3 OFFSET 2' ],
+], 'Correctly interpolated SQL' );
 
 done_testing;
index 115fadb..6fccbb1 100644 (file)
@@ -1,6 +1,8 @@
 use strict;
 use warnings;
 
+BEGIN { $ENV{DBICTEST_VIA_REPLICATED} = 0 }
+
 # !!! do not replace this with done_testing - tests reside in the callbacks
 # !!! number of calls is important
 use Test::More tests => 13;
index a17c382..28af647 100644 (file)
@@ -4,7 +4,6 @@ use warnings;
 use Test::More;
 use lib qw(t/lib);
 use DBICTest;
-use DBIC::SqlMakerTest;
 
 my $ping_count = 0;
 
diff --git a/t/storage/prefer_stringification.t b/t/storage/prefer_stringification.t
new file mode 100644 (file)
index 0000000..ffb292a
--- /dev/null
@@ -0,0 +1,33 @@
+use warnings;
+use strict;
+
+use Test::More;
+
+use lib qw(t/lib);
+use DBICTest;
+
+{
+  package # hideee
+    DBICTest::CrazyInt;
+
+  use overload
+    '0+' => sub { 666 },
+    '""' => sub { 999 },
+    fallback => 1,
+  ;
+}
+
+# check DBI behavior when fed a stringifiable/nummifiable value
+{
+  my $crazynum = bless {}, 'DBICTest::CrazyInt';
+  cmp_ok( $crazynum, '==', 666 );
+  cmp_ok( $crazynum, 'eq', 999 );
+
+  my $schema = DBICTest->init_schema( no_populate => 1 );
+  $schema->storage->dbh_do(sub {
+    $_[1]->do('INSERT INTO artist (name) VALUES (?)', {}, $crazynum );
+  });
+
+  is( $schema->resultset('Artist')->next->name, 999, 'DBI preferred stringified version' );
+}
+done_testing;
index 6492f25..ac65fa0 100644 (file)
@@ -89,7 +89,9 @@ my %dbs = (
 );
 
 # lie that we already locked stuff - the tests below do not touch anything
-$ENV{DBICTEST_LOCK_HOLDER} = -1;
+# unless we are under travis, where the OOM killers reign and things are rough
+$ENV{DBICTEST_LOCK_HOLDER} = -1
+  unless DBICTest::RunMode->is_ci;
 
 # Make sure oracle is tried last - some clients (e.g. 10.2) have symbol
 # clashes with libssl, and will segfault everything coming after them
@@ -104,12 +106,12 @@ for my $db (sort {
 
   my $schema;
 
-  try {
+  my $sql_maker = try {
     $schema = DBICTest::Schema->connect($dsn, $user, $pass, {
       quote_names => 1
     });
     $schema->storage->ensure_connected;
-    1;
+    $schema->storage->sql_maker;
   } || next;
 
   my ($exp_quote_char, $exp_name_sep) =
@@ -118,19 +120,23 @@ for my $db (sort {
   my ($quote_char_text, $name_sep_text) = map { dumper($_) }
     ($exp_quote_char, $exp_name_sep);
 
-  is_deeply $schema->storage->sql_maker->quote_char,
+  is_deeply $sql_maker->quote_char,
     $exp_quote_char,
     "$db quote_char with quote_names => 1 is $quote_char_text";
 
 
-  is $schema->storage->sql_maker->name_sep,
+  is $sql_maker->name_sep,
     $exp_name_sep,
     "$db name_sep with quote_names => 1 is $name_sep_text";
 
   # if something was produced - it better be quoted
-  if ( my $ddl = try { $schema->deployment_statements } ) {
-
-    my $quoted_artist = $schema->storage->sql_maker->_quote('artist');
+  if (
+    # the SQLT producer has no idea what quotes are :/
+    ! grep { $db eq $_ } qw( SYBASE DB2 )
+      and
+    my $ddl = try { $schema->deployment_statements }
+  ) {
+    my $quoted_artist = $sql_maker->_quote('artist');
 
     like ($ddl, qr/^CREATE\s+TABLE\s+\Q$quoted_artist/msi, "$db DDL contains expected quoted table name");
   }
index 557bff8..fc97ebd 100644 (file)
@@ -2,7 +2,9 @@ use strict;
 use warnings;
 
 use FindBin;
+use B::Deparse;
 use File::Copy 'move';
+use Scalar::Util 'weaken';
 use Test::More;
 use Test::Exception;
 use lib qw(t/lib);
@@ -14,6 +16,12 @@ my $db_tmp  = "$db_orig.tmp";
 # Set up the "usual" sqlite for DBICTest
 my $schema = DBICTest->init_schema( sqlite_use_file => 1 );
 
+my $exception_action_count;
+$schema->exception_action(sub {
+  $exception_action_count++;
+  die @_;
+});
+
 # Make sure we're connected by doing something
 my @art = $schema->resultset("Artist")->search({ }, { order_by => { -desc => 'name' }});
 cmp_ok(@art, '==', 3, "Three artists returned");
@@ -49,7 +57,7 @@ close $db_file;
   # Catch the DBI connection error
   local $SIG{__WARN__} = sub {};
   throws_ok {
-    my @art_three = $schema->resultset("Artist")->search( {}, { order_by => { -desc => 'name' } } );
+    $schema->resultset("Artist")->create({ name => 'not gonna happen' });
   }  qr/not a database/, 'The operation failed';
 }
 
@@ -90,6 +98,8 @@ for my $ctx (keys %$ctx_map) {
 
   # start disconnected and then connected
   $schema->storage->disconnect;
+  $exception_action_count = 0;
+
   for (1, 2) {
     my $disarmed;
 
@@ -104,6 +114,86 @@ for my $ctx (keys %$ctx_map) {
       isa_ok ($schema->resultset('Artist')->next, 'DBICTest::Artist');
     }, @$args) });
   }
+
+  is( $exception_action_count, 0, 'exception_action never called' );
 };
 
+# make sure RT#110429 does not recur on manual DBI-side disconnect
+for my $cref (
+  sub {
+    my $schema = shift;
+
+    my $g = $schema->txn_scope_guard;
+
+    is( $schema->storage->transaction_depth, 1, "Expected txn depth" );
+
+    $schema->storage->_dbh->disconnect;
+
+    $schema->storage->dbh_do(sub { $_[1]->do('SELECT 1') } );
+  },
+  sub {
+    my $schema = shift;
+    $schema->txn_do(sub {
+      $schema->storage->_dbh->disconnect
+    } );
+  },
+  sub {
+    my $schema = shift;
+    $schema->txn_do(sub {
+      $schema->storage->disconnect;
+      die "VIOLENCE";
+    } );
+  },
+) {
+
+  note( "Testing with " . B::Deparse->new->coderef2text($cref) );
+
+  $schema->storage->disconnect;
+  $exception_action_count = 0;
+
+  ok( !$schema->storage->connected, 'Not connected' );
+
+  is( $schema->storage->transaction_depth, undef, "Start with unknown txn depth" );
+
+  # messages vary depending on version and whether txn or do, whatever
+  dies_ok {
+    $cref->($schema)
+  } 'Threw *something*';
+
+  ok( !$schema->storage->connected, 'Not connected as a result of failed rollback' );
+
+  is( $schema->storage->transaction_depth, undef, "Depth expectedly unknown after failed rollbacks" );
+
+  is( $exception_action_count, 1, "exception_action called only once" );
+}
+
+# check exception_action under tenacious disconnect
+{
+  $schema->storage->disconnect;
+  $exception_action_count = 0;
+
+  throws_ok { $schema->txn_do(sub {
+    $schema->storage->_dbh->disconnect;
+
+    $schema->resultset('Artist')->next;
+  })} qr/prepare on inactive database handle/;
+
+  is( $exception_action_count, 1, "exception_action called only once" );
+}
+
+# check that things aren't crazy with a non-violent disconnect
+{
+  my $schema = DBICTest->init_schema( sqlite_use_file => 0, no_deploy => 1 );
+  weaken( my $ws = $schema );
+
+  $schema->is_executed_sql_bind( sub {
+    $ws->txn_do(sub { $ws->storage->disconnect } );
+  }, [ [ 'BEGIN' ] ], 'Only one BEGIN statement' );
+
+  $schema->is_executed_sql_bind( sub {
+    my $g = $ws->txn_scope_guard;
+    $ws->storage->disconnect;
+  }, [ [ 'BEGIN' ] ], 'Only one BEGIN statement' );
+}
+
 done_testing;
index 509b3e6..82c809d 100644 (file)
@@ -1,22 +1,19 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_replicated';
+
 use strict;
 use warnings;
 
 use Test::More;
-
+use DBIx::Class::_Util 'modver_gt_or_eq_and_lt';
 use lib qw(t/lib);
 use DBICTest;
 
 BEGIN {
-    require DBIx::Class;
-    plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_replicated')
-      unless DBIx::Class::Optional::Dependencies->req_ok_for ('test_replicated');
-
-    if (DBICTest::RunMode->is_smoker) {
-      my $mver = Moose->VERSION;
-      plan skip_all => "A trial version $mver of Moose detected known to break replication - skipping test known to fail"
-        if ($mver >= 1.99 and $mver <= 1.9902);
-    }
-
+  plan skip_all => "A trial version of Moose detected known to break replication - skipping test known to fail" if (
+    DBICTest::RunMode->is_smoker
+      and
+    modver_gt_or_eq_and_lt( 'Moose', '1.99', '1.9903' )
+  )
 }
 
 use Test::Moose;
@@ -24,7 +21,6 @@ use Test::Exception;
 use List::Util 'first';
 use Scalar::Util 'reftype';
 use File::Spec;
-use IO::Handle;
 use Moose();
 use MooseX::Types();
 note "Using Moose version $Moose::VERSION and MooseX::Types version $MooseX::Types::VERSION";
diff --git a/t/storage/savepoints.t b/t/storage/savepoints.t
new file mode 100644 (file)
index 0000000..b0f3858
--- /dev/null
@@ -0,0 +1,262 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+use DBIx::Class::_Util qw(modver_gt_or_eq sigwarn_silencer scope_guard);
+
+use lib qw(t/lib);
+use DBICTest;
+
+{
+  package # moar hide
+    DBICTest::SVPTracerObj;
+
+  use base 'DBIx::Class::Storage::Statistics';
+
+  sub query_start { 'do notning'}
+  sub callback { 'dummy '}
+
+  for my $svpcall (map { "svp_$_" } qw(begin rollback release)) {
+    no strict 'refs';
+    *$svpcall = sub { $_[0]{uc $svpcall}++ };
+  }
+}
+
+my $env2optdep = {
+  DBICTEST_PG => 'test_rdbms_pg',
+  DBICTEST_MYSQL => 'test_rdbms_mysql',
+};
+
+my $schema;
+
+for ('', keys %$env2optdep) { SKIP: {
+
+  my $prefix;
+
+  if ($prefix = $_) {
+    my ($dsn, $user, $pass) = map { $ENV{"${prefix}_$_"} } qw/DSN USER PASS/;
+
+    skip ("Skipping tests with $prefix: set \$ENV{${prefix}_DSN} _USER and _PASS", 1)
+      unless $dsn;
+
+    skip ("Testing with ${prefix}_DSN needs " . DBIx::Class::Optional::Dependencies->req_missing_for( $env2optdep->{$prefix} ), 1)
+      unless  DBIx::Class::Optional::Dependencies->req_ok_for($env2optdep->{$prefix});
+
+    $schema = DBICTest::Schema->connect ($dsn,$user,$pass,{ auto_savepoint => 1 });
+
+    my $create_sql;
+    $schema->storage->ensure_connected;
+    if ($schema->storage->isa('DBIx::Class::Storage::DBI::Pg')) {
+      $create_sql = "CREATE TABLE artist (artistid serial PRIMARY KEY, name VARCHAR(100), rank INTEGER NOT NULL DEFAULT '13', charfield CHAR(10))";
+      $schema->storage->dbh->do('SET client_min_messages=WARNING');
+    }
+    elsif ($schema->storage->isa('DBIx::Class::Storage::DBI::mysql')) {
+      $create_sql = "CREATE TABLE artist (artistid INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), rank INTEGER NOT NULL DEFAULT '13', charfield CHAR(10)) ENGINE=InnoDB";
+    }
+    else {
+      skip( 'Untested driver ' . $schema->storage, 1 );
+    }
+
+    $schema->storage->dbh_do (sub {
+      $_[1]->do('DROP TABLE IF EXISTS artist');
+      $_[1]->do($create_sql);
+    });
+  }
+  else {
+    $prefix = 'SQLite Internal DB';
+    $schema = DBICTest->init_schema( no_populate => 1, auto_savepoint => 1 );
+  }
+
+  note "Testing $prefix";
+
+  # can not use local() due to an unknown number of storages
+  # (think replicated)
+  my $orig_states = { map
+    { $_ => $schema->storage->$_ }
+    qw(debugcb debugobj debug)
+  };
+  my $sg = scope_guard {
+    $schema->storage->$_ ( $orig_states->{$_} ) for keys %$orig_states;
+  };
+  $schema->storage->debugobj (my $stats = DBICTest::SVPTracerObj->new);
+  $schema->storage->debug (1);
+
+  $schema->resultset('Artist')->create({ name => 'foo' });
+
+  $schema->txn_begin;
+
+  my $arty = $schema->resultset('Artist')->find(1);
+
+  my $name = $arty->name;
+
+  # First off, test a generated savepoint name
+  $schema->svp_begin;
+
+  cmp_ok($stats->{'SVP_BEGIN'}, '==', 1, 'Statistics svp_begin tickled');
+
+  $arty->update({ name => 'Jheephizzy' });
+
+  $arty->discard_changes;
+
+  cmp_ok($arty->name, 'eq', 'Jheephizzy', 'Name changed');
+
+  # Rollback the generated name
+  # Active: 0
+  $schema->svp_rollback;
+
+  cmp_ok($stats->{'SVP_ROLLBACK'}, '==', 1, 'Statistics svp_rollback tickled');
+
+  $arty->discard_changes;
+
+  cmp_ok($arty->name, 'eq', $name, 'Name rolled back');
+
+  $arty->update({ name => 'Jheephizzy'});
+
+  # Active: 0 1
+  $schema->svp_begin('testing1');
+
+  $arty->update({ name => 'yourmom' });
+
+  # Active: 0 1 2
+  $schema->svp_begin('testing2');
+
+  $arty->update({ name => 'gphat' });
+  $arty->discard_changes;
+  cmp_ok($arty->name, 'eq', 'gphat', 'name changed');
+
+  # Active: 0 1 2
+  # Rollback doesn't DESTROY the savepoint, it just rolls back to the value
+  # at its conception
+  $schema->svp_rollback('testing2');
+  $arty->discard_changes;
+  cmp_ok($arty->name, 'eq', 'yourmom', 'testing2 reverted');
+
+  # Active: 0 1 2 3
+  $schema->svp_begin('testing3');
+  $arty->update({ name => 'coryg' });
+
+  # Active: 0 1 2 3 4
+  $schema->svp_begin('testing4');
+  $arty->update({ name => 'watson' });
+
+  # Release 3, which implicitly releases 4
+  # Active: 0 1 2
+  $schema->svp_release('testing3');
+
+  $arty->discard_changes;
+  cmp_ok($arty->name, 'eq', 'watson', 'release left data');
+
+  # This rolls back savepoint 2
+  # Active: 0 1 2
+  $schema->svp_rollback;
+
+  $arty->discard_changes;
+  cmp_ok($arty->name, 'eq', 'yourmom', 'rolled back to 2');
+
+  # Rollback the original savepoint, taking us back to the beginning, implicitly
+  # rolling back savepoint 1 and 2
+  $schema->svp_rollback('savepoint_0');
+  $arty->discard_changes;
+  cmp_ok($arty->name, 'eq', 'foo', 'rolled back to start');
+
+  $schema->txn_commit;
+
+  is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
+
+  # And now to see if txn_do will behave correctly
+  $schema->txn_do (sub {
+    my $artycp = $arty;
+
+    $schema->txn_do (sub {
+      $artycp->name ('Muff');
+      $artycp->update;
+    });
+
+    eval {
+      $schema->txn_do (sub {
+        $artycp->name ('Moff');
+        $artycp->update;
+        $artycp->discard_changes;
+        is($artycp->name,'Moff','Value updated in nested transaction');
+        $schema->storage->dbh->do ("GUARANTEED TO PHAIL");
+      });
+    };
+
+    ok ($@,'Nested transaction failed (good)');
+
+    $arty->discard_changes;
+
+    is($arty->name,'Muff','auto_savepoint rollback worked');
+
+    $arty->name ('Miff');
+
+    $arty->update;
+  });
+
+  is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
+
+  $arty->discard_changes;
+
+  is($arty->name,'Miff','auto_savepoint worked');
+
+  cmp_ok($stats->{'SVP_BEGIN'},'==',7,'Correct number of savepoints created');
+
+  cmp_ok($stats->{'SVP_RELEASE'},'==',3,'Correct number of savepoints released');
+
+  cmp_ok($stats->{'SVP_ROLLBACK'},'==',5,'Correct number of savepoint rollbacks');
+
+### test originally written for SQLite exclusively (git blame -w -C -M)
+  # test two-phase commit and inner transaction rollback from nested transactions
+  my $ars = $schema->resultset('Artist');
+
+  $schema->txn_do(sub {
+    $ars->create({ name => 'in_outer_transaction' });
+    $schema->txn_do(sub {
+      $ars->create({ name => 'in_inner_transaction' });
+    });
+    ok($ars->search({ name => 'in_inner_transaction' })->first,
+      'commit from inner transaction visible in outer transaction');
+    throws_ok {
+      $schema->txn_do(sub {
+        $ars->create({ name => 'in_inner_transaction_rolling_back' });
+        die 'rolling back inner transaction';
+      });
+    } qr/rolling back inner transaction/, 'inner transaction rollback executed';
+    $ars->create({ name => 'in_outer_transaction2' });
+  });
+
+  is_deeply( $schema->storage->savepoints, [], 'All savepoints forgotten' );
+
+SKIP: {
+  skip "Reading inexplicably fails on very old replicated DBD::SQLite<1.33", 1 if (
+    $ENV{DBICTEST_VIA_REPLICATED}
+      and
+    $prefix eq 'SQLite Internal DB'
+      and
+    ! modver_gt_or_eq('DBD::SQLite', '1.33')
+  );
+
+  ok($ars->search({ name => 'in_outer_transaction' })->first,
+    'commit from outer transaction');
+  ok($ars->search({ name => 'in_outer_transaction2' })->first,
+    'second commit from outer transaction');
+  ok($ars->search({ name => 'in_inner_transaction' })->first,
+    'commit from inner transaction');
+  is $ars->search({ name => 'in_inner_transaction_rolling_back' })->first,
+    undef,
+    'rollback from inner transaction';
+}
+
+### cleanupz
+  $schema->storage->dbh_do(sub { $_[1]->do("DROP TABLE artist") });
+}}
+
+done_testing;
+
+END {
+  local $SIG{__WARN__} = sigwarn_silencer( qr/Internal transaction state of handle/ )
+    unless modver_gt_or_eq('DBD::SQLite', '1.33');
+  eval { $schema->storage->dbh_do(sub { $_[1]->do("DROP TABLE artist") }) } if defined $schema;
+  undef $schema;
+}
index efe3641..f8e1b35 100644 (file)
@@ -36,7 +36,7 @@ my $code = sub {
 for my $want (0,1) {
   my $schema = DBICTest->init_schema;
 
-  is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
+  is( $schema->storage->transaction_depth, 0, 'txn depth starts at 0');
 
   my @titles = map {'txn_do test CD ' . $_} (1..5);
   my $artist = $schema->resultset('Artist')->find(1);
@@ -56,7 +56,7 @@ for my $want (0,1) {
     title => "txn_do test CD $_",
   })->first->year, 2006, "new CD $_ year correct") for (1..5);
 
-  is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
+  is( $schema->storage->transaction_depth, 0, 'txn depth has been reset');
 }
 
 # Test txn_do() @_ aliasing support
@@ -72,7 +72,7 @@ for my $want (0,1) {
 {
   my $schema = DBICTest->init_schema;
 
-  is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
+  is( $schema->storage->transaction_depth, 0, 'txn depth starts at 0');
 
   my $nested_code = sub {
     my ($schema, $artist, $code) = @_;
@@ -96,7 +96,7 @@ for my $want (0,1) {
   })->first->year, 2006, qq{nested txn_do CD$_ year ok}) for (1..10);
   is($artist->cds->count, $count_before+10, 'nested txn_do added all CDs');
 
-  is( $schema->storage->{transaction_depth}, 0, 'txn depth has been reset');
+  is( $schema->storage->transaction_depth, 0, 'txn depth has been reset');
 }
 
 # test nested txn_begin on fresh connection
@@ -107,6 +107,7 @@ for my $want (0,1) {
   is ($schema->storage->transaction_depth, 0, 'Start outside txn');
 
   my @pids;
+  SKIP:
   for my $action (
     sub {
       my $s = shift;
@@ -129,8 +130,13 @@ for my $want (0,1) {
     },
   ) {
     my $pid = fork();
-    die "Unable to fork: $!\n"
-      if ! defined $pid;
+
+    if( ! defined $pid ) {
+      skip "EAGAIN encountered, your system is likely bogged down: skipping forking test", 1
+        if $! == Errno::EAGAIN();
+
+      die "Unable to fork: $!"
+    }
 
     if ($pid) {
       push @pids, $pid;
@@ -206,8 +212,13 @@ sub _test_forking_action {
       if $^O eq 'MSWin32';
 
     my $pid = fork();
-    die "Unable to fork: $!\n"
-      if ! defined $pid;
+    if( ! defined $pid ) {
+
+      skip "EAGAIN encountered, your system is likely bogged down: skipping forking test", 1
+        if $! == Errno::EAGAIN();
+
+      die "Unable to fork: $!"
+    }
 
     if ($pid) {
       push @pids, $pid;
@@ -260,7 +271,7 @@ my $fail_code = sub {
   # Test failed txn_do()
   for my $pass (1,2) {
 
-    is( $schema->storage->{transaction_depth}, 0, "txn depth starts at 0 (pass $pass)");
+    is( $schema->storage->transaction_depth, 0, "txn depth starts at 0 (pass $pass)");
 
     my $artist = $schema->resultset('Artist')->find(3);
 
@@ -274,13 +285,13 @@ my $fail_code = sub {
     })->first;
     ok(!defined($cd), qq{failed txn_do didn't change the cds table (pass $pass)});
 
-    is( $schema->storage->{transaction_depth}, 0, "txn depth has been reset (pass $pass)");
+    is( $schema->storage->transaction_depth, 0, "txn depth has been reset (pass $pass)");
   }
 
 
   # Test failed txn_do() with failed rollback
   {
-    is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
+    is( $schema->storage->transaction_depth, 0, 'txn depth starts at 0');
 
     my $artist = $schema->resultset('Artist')->find(3);
 
@@ -322,7 +333,7 @@ my $fail_code = sub {
 {
   my $schema = DBICTest->init_schema();
 
-  is( $schema->storage->{transaction_depth}, 0, 'txn depth starts at 0');
+  is( $schema->storage->transaction_depth, 0, 'txn depth starts at 0');
 
   my $nested_fail_code = sub {
     my ($schema, $artist, $code1, $code2) = @_;
@@ -407,4 +418,52 @@ warnings_are {
 
 } [], 'No warnings on AutoCommit => 0 with txn_do';
 
+
+# make sure we are not fucking up the stacktrace on broken overloads
+{
+  package DBICTest::BrokenOverload;
+
+  use overload '""' => sub { $_[0] };
+}
+
+{
+  my @w;
+  local $SIG{__WARN__} = sub {
+    $_[0] =~ /\QExternal exception class DBICTest::BrokenOverload implements partial (broken) overloading preventing its instances from being used in simple (\E\$x eq \$y\Q) comparisons/
+      ? push @w, @_
+      : warn @_
+  };
+
+  my $s = DBICTest->init_schema(no_deploy => 1);
+  $s->stacktrace(0);
+  my $g = $s->storage->txn_scope_guard;
+  my $broken_exception = bless {}, 'DBICTest::BrokenOverload';
+
+  # FIXME - investigate what confuses the regex engine below
+
+  # do not reformat - line-num part of the test
+  my $ln = __LINE__ + 6;
+  throws_ok {
+    $s->txn_do( sub {
+      $s->txn_do( sub {
+        $s->storage->_dbh->disconnect;
+        die $broken_exception
+      });
+    })
+  } qr/\QTransaction aborted: $broken_exception. Rollback failed: DBIx::Class::Storage::DBI::txn_rollback(): lost connection to storage at @{[__FILE__]} line $ln\E\n/;  # FIXME wtf - ...\E$/m doesn't work here
+
+  is @w, 1, 'One matching warning only';
+
+  # try the same broken exception object, but have exception_action inject it
+  $s->exception_action(sub { die $broken_exception });
+  eval {
+    $s->txn_do( sub {
+      die "some string masked away";
+    });
+  };
+  isa_ok $@, 'DBICTest::BrokenOverload', 'Deficient exception properly propagated';
+
+  is @w, 2, 'The warning was emitted a second time';
+}
+
 done_testing;
index 4a2c14b..6c6d1df 100644 (file)
@@ -4,6 +4,10 @@ use warnings;
 use Test::More;
 use Test::Warn;
 use Test::Exception;
+
+use List::Util 'shuffle';
+use DBIx::Class::_Util 'sigwarn_silencer';
+
 use lib qw(t/lib);
 use DBICTest;
 
@@ -90,10 +94,8 @@ use DBICTest;
 {
   my $schema = DBICTest->init_schema;
 
-  no strict 'refs';
   no warnings 'redefine';
-
-  local *{DBIx::Class::Storage::DBI::txn_rollback} = sub { die 'die die my darling' };
+  local *DBIx::Class::Storage::DBI::txn_rollback = sub { die 'die die my darling' };
   Class::C3->reinitialize() if DBIx::Class::_ENV_::OLD_MRO;
 
   throws_ok (sub {
@@ -106,7 +108,7 @@ use DBICTest;
     #$schema->storage->_dbh( $schema->storage->_dbh->clone );
 
     die 'Deliberate exception';
-  }, ($] >= 5.013008 )
+  }, ( "$]" >= 5.013008 )
     ? qr/Deliberate exception/s # temporary until we get the generic exception wrapper rolling
     : qr/Deliberate exception.+Rollback failed/s
   );
@@ -117,37 +119,9 @@ use DBICTest;
 
 # make sure it warns *big* on failed rollbacks
 # test with and without a poisoned $@
-for my $pre_poison (0,1) {
-for my $post_poison (0,1) {
-
-  my $schema = DBICTest->init_schema(no_populate => 1);
-
-  no strict 'refs';
-  no warnings 'redefine';
-  local *{DBIx::Class::Storage::DBI::txn_rollback} = sub { die 'die die my darling' };
-  Class::C3->reinitialize() if DBIx::Class::_ENV_::OLD_MRO;
-
-#The warn from within a DESTROY callback freaks out Test::Warn, do it old-school
-=begin
-  warnings_exist (
-    sub {
-      my $guard = $schema->txn_scope_guard;
-      $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
-
-      # this should freak out the guard rollback
-      # but it won't work because DBD::SQLite is buggy
-      # instead just install a toxic rollback above
-      #$schema->storage->_dbh( $schema->storage->_dbh->clone );
-    },
-    [
-      qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./,
-      qr/\*+ ROLLBACK FAILED\!\!\! \*+/,
-    ],
-    'proper warnings generated on out-of-scope+rollback failure'
-  );
-=cut
-
-# delete this once the above works properly (same test)
+require DBICTest::AntiPattern::TrueZeroLen;
+require DBICTest::AntiPattern::NullObject;
+{
   my @want = (
     qr/A DBIx::Class::Storage::TxnScopeGuard went out of scope without explicit commit or error. Rolling back./,
     qr/\*+ ROLLBACK FAILED\!\!\! \*+/,
@@ -163,30 +137,70 @@ for my $post_poison (0,1) {
     }
   };
 
-  {
-    eval { die 'pre-GIFT!' if $pre_poison };
-    my $guard = $schema->txn_scope_guard;
-    eval { die 'post-GIFT!' if $post_poison };
-    $schema->resultset ('Artist')->create ({ name => 'bohhoo'});
-  }
+  no warnings 'redefine';
+  local *DBIx::Class::Storage::DBI::txn_rollback = sub { die 'die die my darling' };
+  Class::C3->reinitialize() if DBIx::Class::_ENV_::OLD_MRO;
 
-  local $TODO = 'Do not know how to deal with trapped exceptions occuring after guard instantiation...'
-    if ( $post_poison and (
-      # take no chances on installation
-      ( DBICTest::RunMode->is_plain and ($ENV{TRAVIS}||'') ne 'true' )
-        or
-      # this always fails
-      ! $pre_poison
-        or
-      # I do not understand why but on <= 5.8.8 and on 5.10.0 "$pre_poison && $post_poison" passes...
-      ($] > 5.008008 and $] < 5.010000 ) or $] > 5.010000
-    ));
+  my @poisons = shuffle (
+    undef,
+    DBICTest::AntiPattern::TrueZeroLen->new,
+    DBICTest::AntiPattern::NullObject->new,
+    'GIFT!',
+  );
 
-  is (@w, 2, "Both expected warnings found - \$\@ pre-poison: $pre_poison, post-poison: $post_poison" );
+  for my $pre_poison (@poisons) {
+    for my $post_poison (@poisons) {
 
-  # just to mask off warning since we could not disconnect above
-  $schema->storage->_dbh->disconnect;
-}}
+      @w = ();
+
+      my $schema = DBICTest->init_schema(no_populate => 1);
+
+      # the actual scope where the guard is created/freed
+      {
+        # in this particular case these are not the warnings we are looking for
+        local $SIG{__WARN__} = sigwarn_silencer qr/implementing the so called null-object-pattern/;
+
+        # if is inside the eval, to clear $@ in the undef case
+        eval { die $pre_poison if defined $pre_poison };
+
+        my $guard = $schema->txn_scope_guard;
+
+        eval { die $post_poison if defined $post_poison };
+
+        $schema->resultset ('Artist')->create ({ name => "bohhoo, too bad we'll roll you back"});
+      }
+
+      local $TODO = 'Do not know how to deal with trapped exceptions occuring after guard instantiation...'
+        if ( defined $post_poison and (
+          # take no chances on installation
+          DBICTest::RunMode->is_plain
+            or
+          # I do not understand why but on <= 5.8.8 and on 5.10.0
+          # "$pre_poison == $post_poison == string" passes...
+          # so todoify 5.8.9 and 5.10.1+, and deal with the rest below
+          ( ( "$]" > 5.008008 and "$]" < 5.010000 ) or "$]" > 5.010000 )
+            or
+          ! defined $pre_poison
+            or
+          length ref $pre_poison
+            or
+          length ref $post_poison
+        ));
+
+      is (@w, 2, sprintf 'Both expected warnings found - $@ poisonstate:   pre-poison:%s   post-poison:%s',
+        map {
+          ! defined $_      ? 'UNDEF'
+        : ! length ref $_   ? $_
+                            : ref $_
+
+        } ($pre_poison, $post_poison)
+      );
+
+      # just to mask off warning since we could not disconnect above
+      $schema->storage->_dbh->disconnect;
+    }
+  }
+}
 
 # add a TODO to catch when Text::Balanced is finally fixed
 # https://rt.cpan.org/Public/Bug/Display.html?id=74994
@@ -199,7 +213,7 @@ for my $post_poison (0,1) {
 
   my @w;
   local $SIG{__WARN__} = sub {
-    $_[0] =~ /External exception object .+? \Qimplements partial (broken) overloading/
+    $_[0] =~ /External exception class .+? \Qimplements partial (broken) overloading/
       ? push @w, @_
       : warn @_
   };
@@ -207,6 +221,7 @@ for my $post_poison (0,1) {
   lives_ok {
     # this is what poisons $@
     Text::Balanced::extract_bracketed( '(foo', '()' );
+    DBIx::Class::_Util::is_exception($@);
 
     my $s = DBICTest::Schema->connect('dbi:SQLite::memory:');
     my $g = $s->txn_scope_guard;
@@ -217,4 +232,29 @@ for my $post_poison (0,1) {
   is(scalar @w, 0, 'no warnings \o/');
 }
 
+# ensure Devel::StackTrace-refcapture-like effects are countered
+{
+  my $s = DBICTest::Schema->connect('dbi:SQLite::memory:');
+  my $g = $s->txn_scope_guard;
+
+  my @arg_capture;
+  {
+    local $SIG{__WARN__} = sub {
+      package DB;
+      my $frnum;
+      while (my @f = CORE::caller(++$frnum) ) {
+        push @arg_capture, @DB::args;
+      }
+    };
+
+    undef $g;
+    1;
+  }
+
+  warnings_exist
+    { @arg_capture = () }
+    qr/\QPreventing *MULTIPLE* DESTROY() invocations on DBIx::Class::Storage::TxnScopeGuard/
+  ;
+}
+
 done_testing;
diff --git a/t/zzzzzzz_authors.t b/t/zzzzzzz_authors.t
new file mode 100644 (file)
index 0000000..a46a247
--- /dev/null
@@ -0,0 +1,28 @@
+use warnings;
+use strict;
+
+use Test::More 'no_plan';
+use lib 't/lib';
+use DBICTest::RunMode;
+
+my $authorcount = scalar do {
+  open (my $fh, '<', 'AUTHORS') or die "Unable to open AUTHORS - can't happen: $!\n";
+  map { chomp; ( ( ! $_ or $_ =~ /^\s*\#/ ) ? () : $_ ) } <$fh>;
+} or die "Known AUTHORS file seems empty... can't happen...";
+
+# do not announce anything under ci - we are watching for STDERR silence
+diag <<EOD unless DBICTest::RunMode->is_ci;
+
+
+
+$authorcount contributors made this library what it is today
+
+
+Distinguished patrons:
+  * ( 2014 ~ 2015 ) Henry Van Styn, creator of http://p3rl.org/RapidApp
+
+
+EOD
+
+# looks funny if we do it before stuff
+ok 1;
index c1e0ab8..4434e1c 100644 (file)
@@ -99,7 +99,7 @@ SKIP: {
     }
 
     sub _possibly_has_bad_overload_performance {
-        return $] < 5.008009 && !_has_bug_34925();
+        return( "$]" < 5.008009 and !_has_bug_34925() );
     }
 
     # If this next one fails then you almost certainly have a RH derived
diff --git a/xt/dist/authors.t b/xt/dist/authors.t
new file mode 100644 (file)
index 0000000..8ee1bf3
--- /dev/null
@@ -0,0 +1,87 @@
+use warnings;
+use strict;
+
+use Test::More;
+use Config;
+use File::Spec;
+
+my @known_authors = do {
+  # according to #p5p this is how one safely reads random unicode
+  # this set of boilerplate is insane... wasn't perl unicode-king...?
+  no warnings 'once';
+  require Encode;
+  require PerlIO::encoding;
+  local $PerlIO::encoding::fallback = Encode::FB_CROAK();
+
+  open (my $fh, '<:encoding(UTF-8)', 'AUTHORS') or die "Unable to open AUTHORS - can't happen: $!\n";
+  map { chomp; ( ( ! $_ or $_ =~ /^\s*\#/ ) ? () : $_ ) } <$fh>;
+
+} or die "Known AUTHORS file seems empty... can't happen...";
+
+is_deeply (
+  [ grep { /^\s/ or /\s\s/ } @known_authors ],
+  [],
+  "No entries with leading or doubled space",
+);
+
+is_deeply (
+  [ grep { / \:[^\s\/] /x or /^ [^:]*? \s+ \: /x } @known_authors ],
+  [],
+  "No entries with malformed nicks",
+);
+
+is_deeply (
+  \@known_authors,
+  [ sort { lc $a cmp lc $b } @known_authors ],
+  'Author list is case-insensitively sorted'
+);
+
+my $email_re = qr/( \< [^\<\>]+ \> ) $/x;
+
+my %known_authors;
+for (@known_authors) {
+  my ($name_email) = m/ ^ (?: [^\:]+ \: \s )? (.+) /x;
+  my ($email) = $name_email =~ $email_re;
+
+  fail "Duplicate found: $name_email" if (
+    $known_authors{$name_email}++
+      or
+    ( $email and $known_authors{$email}++ )
+  );
+}
+
+# augh taint mode
+if (length $ENV{PATH}) {
+  ( $ENV{PATH} ) = join ( $Config{path_sep},
+    map { length($_) ? File::Spec->rel2abs($_) : () }
+      split /\Q$Config{path_sep}/, $ENV{PATH}
+  ) =~ /\A(.+)\z/;
+}
+
+# no git-check when smoking a PR
+if (
+  (
+    ! $ENV{TRAVIS_PULL_REQUEST}
+      or
+    $ENV{TRAVIS_PULL_REQUEST} eq "false"
+  )
+    and
+  -d '.git'
+) {
+
+  binmode (Test::More->builder->$_, ':utf8') for qw/output failure_output todo_output/;
+
+  # this may fail - not every system has git
+  for (
+    map
+      { my ($gitname) = m/^ \s* \d+ \s* (.+?) \s* $/mx; utf8::decode($gitname); $gitname }
+      qx( git shortlog -e -s )
+  ) {
+    my ($eml) = $_ =~ $email_re;
+
+    ok $known_authors{$eml},
+      "Commit author '$_' (from .mailmap-aware `git shortlog -e -s`) reflected in ./AUTHORS";
+  }
+}
+
+done_testing;
@@ -1,6 +1,8 @@
 use warnings;
 use strict;
 
+BEGIN { delete $ENV{DBICTEST_VERSION_WARNS_INDISCRIMINATELY} }
+
 use DBIx::Class::_Util 'sigwarn_silencer';
 use if DBIx::Class::_ENV_::BROKEN_FORK, 'threads';
 
similarity index 94%
rename from xt/podcoverage.t
rename to xt/dist/pod_coverage.t
index da48580..1f3195a 100644 (file)
@@ -1,20 +1,15 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_podcoverage';
+
 use warnings;
 use strict;
 
 use Test::More;
 use List::Util 'first';
+use Module::Runtime 'require_module';
 use lib qw(t/lib maint/.Generated_Pod/lib);
 use DBICTest;
 use namespace::clean;
 
-require DBIx::Class;
-unless ( DBIx::Class::Optional::Dependencies->req_ok_for ('test_podcoverage') ) {
-  my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('test_podcoverage');
-  $ENV{RELEASE_TESTING}
-    ? die ("Failed to load release-testing module requirements: $missing")
-    : plan skip_all => "Test needs: $missing"
-}
-
 # this has already been required but leave it here for CPANTS static analysis
 require Test::Pod::Coverage;
 
@@ -40,6 +35,11 @@ my $exceptions = {
             mk_classaccessor
         /]
     },
+    'DBIx::Class::Optional::Dependencies' => {
+        ignore => [qw/
+            croak
+        /]
+    },
     'DBIx::Class::Carp' => {
         ignore => [qw/
             unimport
@@ -57,6 +57,7 @@ my $exceptions = {
             store_column
             get_column
             get_columns
+            has_column_loaded
         /],
     },
     'DBIx::Class::ResultSource' => {
@@ -172,6 +173,8 @@ foreach my $module (@modules) {
 
     skip ("$module exempt", 1) if ($ex->{skip});
 
+    skip ("$module not loadable", 1) unless eval { require_module($module) };
+
     # build parms up from ignore list
     my $parms = {};
     $parms->{trustme} =
diff --git a/xt/dist/postdistdir/pod_footers.t b/xt/dist/postdistdir/pod_footers.t
new file mode 100644 (file)
index 0000000..9882b52
--- /dev/null
@@ -0,0 +1,51 @@
+use warnings;
+use strict;
+
+use Test::More;
+use File::Find;
+
+my $boilerplate_headings = q{
+=head1 FURTHER QUESTIONS?
+
+Check the list of L<additional DBIC resources|DBIx::Class/GETTING HELP/SUPPORT>.
+
+=head1 COPYRIGHT AND LICENSE
+
+This module is free software L<copyright|DBIx::Class/COPYRIGHT AND LICENSE>
+by the L<DBIx::Class (DBIC) authors|DBIx::Class/AUTHORS>. You can
+redistribute it and/or modify it under the same terms as the
+L<DBIx::Class library|DBIx::Class/COPYRIGHT AND LICENSE>.
+};
+
+find({
+  wanted => sub {
+    my $fn = $_;
+
+    return unless -f $fn;
+    return unless $fn =~ / \. (?: pm | pod ) $ /ix;
+
+    my $data = do { local (@ARGV, $/) = $fn; <> };
+
+    if ($data !~ /^=head1 NAME/m) {
+
+      # the generator is full of false positives, .pod is where it's at
+      return if $fn =~ qr{\Qlib/DBIx/Class/Optional/Dependencies.pm};
+
+      ok ( $data !~ /\bcopyright\b/i, "No copyright notices in $fn without apparent POD" );
+    }
+    elsif ($fn =~ qr{\Qlib/DBIx/Class.}) {
+      # nothing to check there - a static set of words
+    }
+    else {
+      ok ( $data !~ / ^ =head1 \s $_ /xmi, "No standalone $_ headings in $fn" )
+        for qw(AUTHOR CONTRIBUTOR LICENSE LICENCE);
+
+      ok ( $data !~ / ^ =head1 \s COPYRIGHT \s (?! AND \s LICENSE )/xmi, "No standalone COPYRIGHT headings in $fn" );
+
+      ok ($data =~ / \Q$boilerplate_headings\E (?! .*? ^ =head )/xms, "Expected headings found at the end of $fn");
+    }
+  },
+  no_chdir => 1,
+}, (qw(lib examples)) );
+
+done_testing;
diff --git a/xt/dist/postdistdir/pod_validity.t b/xt/dist/postdistdir/pod_validity.t
new file mode 100644 (file)
index 0000000..773e5ac
--- /dev/null
@@ -0,0 +1,14 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_pod';
+
+use warnings;
+use strict;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest;
+
+# this has already been required but leave it here for CPANTS static analysis
+require Test::Pod;
+
+my $generated_pod_dir = 'maint/.Generated_Pod';
+Test::Pod::all_pod_files_ok( 'lib', -d $generated_pod_dir ? $generated_pod_dir : () );
similarity index 80%
rename from xt/whitespace.t
rename to xt/dist/postdistdir/whitespace.t
index 62405bb..3576da6 100644 (file)
@@ -1,3 +1,5 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_whitespace';
+
 use warnings;
 use strict;
 
@@ -6,14 +8,6 @@ use File::Glob 'bsd_glob';
 use lib 't/lib';
 use DBICTest ':GlobalLock';
 
-require DBIx::Class;
-unless ( DBIx::Class::Optional::Dependencies->req_ok_for ('test_whitespace') ) {
-  my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('test_whitespace');
-  $ENV{RELEASE_TESTING}
-    ? die ("Failed to load release-testing module requirements: $missing")
-    : plan skip_all => "Test needs: $missing"
-}
-
 # FIXME - temporary workaround for RT#82032, RT#82033
 # also add all scripts (no extension) and some extra extensions
 # we want to check
diff --git a/xt/dist/strictures.t b/xt/dist/strictures.t
new file mode 100644 (file)
index 0000000..70efc7c
--- /dev/null
@@ -0,0 +1,64 @@
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_strictures';
+
+use warnings;
+use strict;
+
+use Test::More;
+use File::Find;
+use File::Spec;
+use Config;
+use lib 't/lib';
+use DBICTest;
+
+# The rationale is - if we can load all our optdeps
+# that are related to lib/ - then we should be able to run
+# perl -c checks (via syntax_ok), and all should just work
+my $missing_groupdeps_present = grep
+  { ! DBIx::Class::Optional::Dependencies->req_ok_for($_) }
+  grep
+    { $_ !~ /^ (?: test | rdbms | dist ) _ /x }
+    keys %{DBIx::Class::Optional::Dependencies->req_group_list}
+;
+
+# don't test syntax when RT#106935 is triggered (mainly CI)
+# FIXME - remove when RT is resolved
+my $tainted_relpath = (
+  length $ENV{PATH}
+    and
+  ${^TAINT}
+    and
+  grep
+    { ! File::Spec->file_name_is_absolute($_) }
+    split /\Q$Config{path_sep}/, $ENV{PATH}
+) ? 1 : 0;
+
+find({
+  wanted => sub {
+    -f $_ or return;
+    m/\.(?: pm | pl | t )$ /ix or return;
+
+    return if m{^(?:
+      maint/Makefile.PL.inc/.+                        # all the maint inc snippets are auto-strictured
+        |
+      t/lib/DBICTest/Util/OverrideRequire.pm          # no stictures by design (load order sensitive)
+        |
+      lib/DBIx/Class/Optional/Dependencies.pm         # no stictures by design (load spee sensitive)
+    )$}x;
+
+    my $f = $_;
+
+    Test::Strict::strict_ok($f);
+    Test::Strict::warnings_ok($f);
+
+    Test::Strict::syntax_ok($f) if (
+      ! $tainted_relpath
+        and
+      ! $missing_groupdeps_present
+        and
+      $f =~ /^ (?: lib  )/x
+    );
+  },
+  no_chdir => 1,
+}, (qw(lib t examples maint)) );
+
+done_testing;
similarity index 98%
rename from t/04_c3_mro.t
rename to xt/extra/c3_mro.t
index 0b7314c..55effb5 100644 (file)
@@ -67,7 +67,7 @@ is (
   'Correct method picked'
 );
 
-if ($] >= 5.010) {
+if ( "$]" >= 5.010 ) {
   ok (! $INC{'Class/C3.pm'}, 'No Class::C3 loaded on perl 5.10+');
 
   # Class::C3::Componentised loads MRO::Compat unconditionally to satisfy
similarity index 92%
rename from t/admin/10script.t
rename to xt/extra/dbicadmin.t
index 9414b84..cc79190 100644 (file)
@@ -1,27 +1,23 @@
-# vim: filetype=perl
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_admin_script';
+
 use strict;
 use warnings;
 
+BEGIN {
+  # just in case the user env has stuff in it
+  delete $ENV{JSON_ANY_ORDER};
+  delete $ENV{DBICTEST_VERSION_WARNS_INDISCRIMINATELY};
+}
+
 use Test::More;
 use Config;
 use File::Spec;
 use lib qw(t/lib);
 use DBICTest;
 
-BEGIN {
-  require DBIx::Class;
-  plan skip_all => 'Test needs ' .
-    DBIx::Class::Optional::Dependencies->req_missing_for('test_admin_script')
-      unless DBIx::Class::Optional::Dependencies->req_ok_for('test_admin_script');
-
-  # just in case the user env has stuff in it
-  delete $ENV{JSON_ANY_ORDER};
-}
-
 $ENV{PATH} = '';
 $ENV{PERL5LIB} = join ($Config{path_sep}, @INC);
 
-require JSON::Any;
 my @json_backends = qw(DWIW PP JSON CPANEL XS);
 
 # test the script is setting @INC properly
diff --git a/xt/extra/diagnostics/malformed_rel_declaration.t b/xt/extra/diagnostics/malformed_rel_declaration.t
new file mode 100644 (file)
index 0000000..a1abdb7
--- /dev/null
@@ -0,0 +1,28 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBICTest::Schema::Artist;
+
+my $pkg = 'DBICTest::Schema::Artist';
+
+for my $call (qw(has_many might_have has_one belongs_to)) {
+  {
+    local $TODO = 'stupid stupid heuristic - needs to die'
+      if $call eq 'belongs_to';
+
+    throws_ok {
+      $pkg->$call( foos => 'nonexistent bars', { foo => 'self.artistid' } );
+    } qr/Malformed relationship condition key 'foo': must be prefixed with 'foreign.'/,
+    "Correct exception on $call with malformed foreign.";
+  }
+
+  throws_ok {
+    $pkg->has_many( foos => 'nonexistent bars', { 'foreign.foo' => 'name' } );
+  } qr/\QMalformed relationship condition value 'name': must be prefixed with 'self.'/,
+  "Correct exception on $call with malformed self.";
+}
+
+done_testing;
similarity index 85%
rename from t/91merge_joinpref_attr.t
rename to xt/extra/internals/merge_joinpref_attr.t
index 0e9f601..bb77358 100644 (file)
@@ -6,8 +6,6 @@ use lib qw(t/lib);
 use DBICTest;
 use Test::More;
 
-plan tests => 15;
-
 my $schema = DBICTest->init_schema();
 my $rs = $schema->resultset( 'CD' );
 
@@ -131,5 +129,20 @@ my $rs = $schema->resultset( 'CD' );
   is_deeply( $result, $expected );
 }
 
+{
+  my $a = [ { 'artist' => { 'manager' => {} } }, 'cd' ];
+  my $b = [ 'artist', { 'artist' => { 'manager' => {} } } ];
+  my $expected = [ { 'artist' => { 'manager' => {} } }, 'cd', { 'artist' => { 'manager' => {} } } ];
+  my $result = $rs->_merge_joinpref_attr($a, $b);
+  is_deeply( $result, $expected );
+}
+
+{
+  my $a = [ { 'artist' => { 'manager' => undef } }, 'cd' ];
+  my $b = [ 'artist', { 'artist' => { 'manager' => undef } } ];
+  my $expected = [ { 'artist' => { 'manager' => undef } }, 'cd', { 'artist' => { 'manager' => undef } } ];
+  my $result = $rs->_merge_joinpref_attr($a, $b);
+  is_deeply( $result, $expected );
+}
 
-1;
+done_testing;
similarity index 89%
rename from t/55namespaces_cleaned.t
rename to xt/extra/internals/namespaces_cleaned.t
index 176de5e..552a81e 100644 (file)
@@ -1,5 +1,5 @@
 BEGIN {
-  if ($] < 5.010) {
+  if ( "$]" < 5.010) {
 
     # Pre-5.10 perls pollute %INC on unsuccesfull module
     # require, making it appear as if the module is already
@@ -36,8 +36,8 @@ use warnings;
 use Test::More;
 
 use lib 't/lib';
-use DBICTest;
 
+use DBICTest;
 use File::Find;
 use File::Spec;
 use B qw/svref_2object/;
@@ -81,12 +81,15 @@ my $skip_idx = { map { $_ => 1 } (
   'DBIx::Class::ResultSet::Pager',
 
   # utility classes, not part of the inheritance chain
+  'DBIx::Class::Optional::Dependencies',
   'DBIx::Class::ResultSource::RowParser::Util',
   'DBIx::Class::_Util',
 ) };
 
 my $has_moose = eval { require Moose::Util };
 
+Sub::Defer::undefer_all();
+
 # can't use Class::Inspector for the mundane parts as it does not
 # distinguish imports from anything else, what a crock of...
 # Moose is not always available either - hence just do it ourselves
@@ -143,9 +146,18 @@ for my $mod (@modules) {
             last;
           }
         }
-        fail ("${mod}::${name} appears to have entered inheritance chain by import into "
-            . ($via || 'UNKNOWN')
-        );
+
+        # exception time
+        if (
+          ( $name eq 'import' and $via = 'Exporter' )
+        ) {
+          pass("${mod}::${name} is a valid uncleaned import from ${name}");
+        }
+        else {
+          fail ("${mod}::${name} appears to have entered inheritance chain by import into "
+              . ($via || 'UNKNOWN')
+          );
+        }
       }
     }
 
@@ -173,7 +185,7 @@ for my $mod (@modules) {
 sub find_modules {
   my @modules;
 
-  find({
+  find( {
     wanted => sub {
       -f $_ or return;
       s/\.pm$// or return;
@@ -181,7 +193,12 @@ sub find_modules {
       push @modules, join ('::', File::Spec->splitdir($_));
     },
     no_chdir => 1,
-  }, (-e 'blib' ? 'blib' : 'lib') );
+  }, (
+    # find them in both lib and blib, duplicates are fine, since
+    # @INC is preadjusted for us by the harness
+    'lib',
+    ( -e 'blib' ? 'blib' : () ),
+  ));
 
   return sort @modules;
 }
diff --git a/xt/extra/internals/optional_deps.t b/xt/extra/internals/optional_deps.t
new file mode 100644 (file)
index 0000000..7da1cc4
--- /dev/null
@@ -0,0 +1,279 @@
+my ($inc_before, $inc_after);
+BEGIN {
+  $inc_before = [ keys %INC ];
+  require DBIx::Class::Optional::Dependencies;
+  $inc_after = [ keys %INC ];
+}
+
+use strict;
+use warnings;
+no warnings qw/once/;
+
+use Test::More;
+use Test::Exception;
+
+# load before we break require()
+use Scalar::Util();
+use MRO::Compat();
+use Carp 'confess';
+use List::Util 'shuffle';
+
+SKIP: {
+  skip 'Lean load pattern testing unsafe with $ENV{PERL5OPT}', 1 if $ENV{PERL5OPT};
+  skip 'Lean load pattern testing useless with $ENV{RELEASE_TESTING}', 1 if $ENV{RELEASE_TESTING};
+  is_deeply
+    $inc_before,
+    [],
+    'Nothing was loaded before inc-test'
+  ;
+  is_deeply
+    $inc_after,
+    [ 'DBIx/Class/Optional/Dependencies.pm' ],
+    'Nothing was loaded other than DBIx::Class::OptDeps'
+  ;
+}
+
+# check the project-local groups for sanity
+lives_ok {
+  DBIx::Class::Optional::Dependencies->req_group_list
+} "The entire optdep list is well formed";
+
+is_deeply (
+  [ keys %{ DBIx::Class::Optional::Dependencies->req_list_for ('deploy') } ],
+  [ 'SQL::Translator' ],
+  'Correct deploy() dependency list',
+);
+
+# scope to break require()
+{
+
+# make module loading impossible, regardless of actual libpath contents
+  local @INC;
+
+# basic test using the deploy target
+  for ('deploy', ['deploy']) {
+
+    # explicitly blow up cache
+    %DBIx::Class::Optional::Dependencies::req_unavailability_cache = ();
+
+    ok (
+      ! DBIx::Class::Optional::Dependencies->req_ok_for ($_),
+      'deploy() deps missing',
+    );
+
+    like (
+      DBIx::Class::Optional::Dependencies->modreq_missing_for ($_),
+      qr/
+        \A
+        SQL::Translator \~ [\d\.]+
+        \z
+      /x,
+      'expected modreq missing string contents',
+    );
+
+    like (
+      DBIx::Class::Optional::Dependencies->req_missing_for ($_),
+      qr/
+        \A
+        SQL::Translator \~ [\d\.]+
+        \Q (see DBIx::Class::Optional::Dependencies documentation for details)\E
+        \z
+      /x,
+      'expected missing string contents',
+    );
+
+    like (
+      DBIx::Class::Optional::Dependencies->modreq_errorlist_for ($_)->{'SQL::Translator'},
+      qr|\QCan't locate SQL/Translator.pm|,
+      'correct "unable to locate"  exception found in errorlist',
+    );
+
+    #make it so module appears loaded
+    local $INC{'SQL/Translator.pm'} = 1;
+    local $SQL::Translator::VERSION = 999;
+
+    ok (
+      ! DBIx::Class::Optional::Dependencies->req_ok_for ($_),
+      'deploy() deps missing cached properly from previous run',
+    );
+
+    # blow cache again
+    %DBIx::Class::Optional::Dependencies::req_unavailability_cache = ();
+
+    ok (
+      DBIx::Class::Optional::Dependencies->req_ok_for ($_),
+      'deploy() deps present',
+    );
+
+    is (
+      DBIx::Class::Optional::Dependencies->req_missing_for ($_),
+      '',
+      'expected null missing string',
+    );
+
+    is_deeply (
+      # use the deprecated method name
+      DBIx::Class::Optional::Dependencies->req_errorlist_for ($_),
+      undef,
+      'expected empty errorlist',
+    );
+  }
+
+# test single-db text
+  local $ENV{DBICTEST_MYSQL_DSN};
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->req_list_for('test_rdbms_mysql'),
+    undef,
+    'unknown optional dependencies list for testing MySQL without ENV var',
+  );
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->modreq_list_for('test_rdbms_mysql'),
+    { 'DBD::mysql' => 0 },
+    'correct optional module dependencies list for testing MySQL without ENV var',
+  );
+
+  local $ENV{DBICTEST_MYSQL_DSN};
+  local $ENV{DBICTEST_PG_DSN};
+
+# regular
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->modreq_list_for([shuffle qw( test_rdbms_pg binary_data )]),
+    { 'DBD::Pg' => '2.009002' },
+    'optional dependencies list for testing Postgres without envvar',
+  );
+
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->req_list_for([shuffle qw( test_rdbms_pg binary_data )]),
+    undef,
+    'optional dependencies list for testing Postgres without envvar',
+  );
+
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->req_list_for('rdbms_pg'),
+    { 'DBD::Pg' => '0', },
+    'optional dependencies list for using Postgres matches',
+  );
+
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->req_missing_for('rdbms_pg'),
+    'DBD::Pg (see DBIx::Class::Optional::Dependencies documentation for details)',
+    'optional dependencies missing list for using Postgres matches',
+  );
+
+# test combination of different requirements on same module (pg's are relatively stable)
+  is_deeply (
+    DBIx::Class::Optional::Dependencies->req_list_for([shuffle qw( rdbms_pg test_rdbms_pg )]),
+    { 'DBD::Pg' => '0' },
+    'optional module dependencies list for testing Postgres matches without envvar',
+  );
+
+  is(
+    DBIx::Class::Optional::Dependencies->req_missing_for([shuffle qw( rdbms_pg test_rdbms_pg binary_data )]),
+    'DBD::Pg~2.009002 as well as the following group(s) of environment variables: DBICTEST_PG_DSN/..._USER/..._PASS',
+    'optional dependencies for testing Postgres without envvar'
+  );
+
+  is(
+    DBIx::Class::Optional::Dependencies->req_missing_for([shuffle qw( test_rdbms_mysql test_rdbms_pg binary_data)]),
+    'DBD::mysql DBD::Pg~2.009002 as well as the following group(s) of environment variables: DBICTEST_MYSQL_DSN/..._USER/..._PASS and DBICTEST_PG_DSN/..._USER/..._PASS',
+    'optional dependencies for testing Postgres+MySQL without envvars'
+  );
+
+  $ENV{DBICTEST_PG_DSN} = 'boo';
+  is_deeply (
+    DBIx::Class::Optional::Dependencies->modreq_list_for([shuffle qw( rdbms_pg test_rdbms_pg binary_data)]),
+    { 'DBD::Pg' => '2.009002' },
+    'optional module dependencies list for testing Postgres matches with envvar',
+  );
+
+  is(
+    DBIx::Class::Optional::Dependencies->req_missing_for([shuffle qw( rdbms_pg test_rdbms_pg binary_data )]),
+    'DBD::Pg~2.009002',
+    'optional dependencies error text for testing Postgres matches with evvar',
+  );
+
+# ICDT augmentation
+  my %expected_icdt_base = ( DateTime => '0.55', 'DateTime::TimeZone::OlsonDB' => 0 );
+
+  my $mysql_icdt = [shuffle qw( test_rdbms_mysql ic_dt )];
+
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->modreq_list_for($mysql_icdt),
+    {
+      %expected_icdt_base,
+      'DBD::mysql' => 0,
+      'DateTime::Format::MySQL' => 0,
+    },
+    'optional module dependencies list for testing ICDT MySQL without envvar',
+  );
+
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->req_list_for($mysql_icdt),
+    \%expected_icdt_base,
+    'optional dependencies list for testing ICDT MySQL without envvar',
+  );
+
+  is(
+    DBIx::Class::Optional::Dependencies->req_missing_for($mysql_icdt),
+    "DateTime~0.55 DateTime::Format::MySQL DateTime::TimeZone::OlsonDB DBD::mysql as well as the following group(s) of environment variables: DBICTEST_MYSQL_DSN/..._USER/..._PASS",
+    'missing optional dependencies for testing ICDT MySQL without envvars'
+  );
+
+# test multi-level include with a variable and mandatory part converging on same included dep
+  local $ENV{DBICTEST_MSACCESS_ODBC_DSN};
+  local $ENV{DBICTEST_MSSQL_ODBC_DSN} = 'foo';
+  my $msaccess_mssql_icdt = [ shuffle qw( test_rdbms_msaccess_odbc test_rdbms_mssql_odbc ic_dt ) ];
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->req_missing_for($msaccess_mssql_icdt),
+    'Data::GUID DateTime~0.55 DateTime::Format::Strptime~1.2 DateTime::TimeZone::OlsonDB DBD::ODBC as well as the following group(s) of environment variables: DBICTEST_MSACCESS_ODBC_DSN/..._USER/..._PASS',
+    'Correct req_missing_for on multi-level converging include',
+  );
+
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->modreq_missing_for($msaccess_mssql_icdt),
+    'Data::GUID DateTime~0.55 DateTime::Format::Strptime~1.2 DateTime::TimeZone::OlsonDB DBD::ODBC',
+    'Correct modreq_missing_for on multi-level converging include',
+  );
+
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->req_list_for($msaccess_mssql_icdt),
+    {
+      'DBD::ODBC' => 0,
+      'DateTime::Format::Strptime' => '1.2',
+      %expected_icdt_base,
+    },
+    'Correct req_list_for on multi-level converging include',
+  );
+
+  is_deeply(
+    DBIx::Class::Optional::Dependencies->modreq_list_for($msaccess_mssql_icdt),
+    {
+      'DBD::ODBC' => 0,
+      'Data::GUID' => 0,
+      'DateTime::Format::Strptime' => '1.2',
+      %expected_icdt_base,
+    },
+    'Correct modreq_list_for on multi-level converging include',
+  );
+
+}
+
+# test multiple times to find autovivification bugs
+for my $meth (qw(req_list_for modreq_list_for)) {
+  throws_ok {
+    DBIx::Class::Optional::Dependencies->$meth();
+  } qr/\Qreq_list_for() expects a requirement group name/,
+  "$meth without groupname throws exception";
+
+  throws_ok {
+    DBIx::Class::Optional::Dependencies->$meth('');
+  } qr/\Q$meth() expects a requirement group name/,
+  "$meth with empty groupname throws exception";
+
+  throws_ok {
+    DBIx::Class::Optional::Dependencies->$meth('invalid_groupname');
+  } qr/Requirement group 'invalid_groupname' is not defined/,
+  "$meth with invalid groupname throws exception";
+}
+
+done_testing;
diff --git a/xt/extra/internals/quote_sub.t b/xt/extra/internals/quote_sub.t
new file mode 100644 (file)
index 0000000..77b4905
--- /dev/null
@@ -0,0 +1,48 @@
+use warnings;
+use strict;
+
+use Test::More;
+use Test::Warn;
+
+use DBIx::Class::_Util 'quote_sub';
+
+my $q = do {
+  no strict 'vars';
+  quote_sub '$x = $x . "buh"; $x += 42';
+};
+
+warnings_exist {
+  is $q->(), 42, 'Expected result after uninit and string/num conversion'
+} [
+  qr/Use of uninitialized value/i,
+  qr/isn't numeric in addition/,
+], 'Expected warnings, strict did not leak inside the qsub'
+  or do {
+    require B::Deparse;
+    diag( B::Deparse->new->coderef2text( Sub::Quote::unquote_sub($q) ) )
+  }
+;
+
+my $no_nothing_q = do {
+  no strict;
+  no warnings;
+  quote_sub <<'EOC';
+    BEGIN { warn "-->${^WARNING_BITS}<--\n" };
+    my $n = "Test::Warn::warnings_exist";
+    warn "-->@{[ *{$n}{CODE} ]}<--\n";
+EOC
+};
+
+my $we_cref = Test::Warn->can('warnings_exist');
+
+warnings_exist { $no_nothing_q->() } [
+  qr/^\-\-\>\0+\<\-\-$/m,
+  qr/^\Q-->$we_cref<--\E$/m,
+], 'Expected warnings, strict did not leak inside the qsub'
+  or do {
+    require B::Deparse;
+    diag( B::Deparse->new->coderef2text( Sub::Quote::unquote_sub($no_nothing_q) ) )
+  }
+;
+
+done_testing;
similarity index 88%
rename from t/53lean_startup.t
rename to xt/extra/lean_startup.t
index 311fa24..8c220dd 100644 (file)
@@ -6,6 +6,9 @@ BEGIN {
   # these envvars *will* bring in more stuff than the baseline
   delete @ENV{qw(DBICTEST_SQLT_DEPLOY DBIC_TRACE)};
 
+  # make sure extras do not load even when this is set
+  $ENV{PERL_STRICTURES_EXTRA} = 1;
+
   unshift @INC, 't/lib';
   require DBICTest::Util::OverrideRequire;
 
@@ -50,10 +53,8 @@ BEGIN {
       CORE::require('Test/More.pm');
       Test::More::fail ("Unexpected require of '$req' by $caller[0] ($caller[1] line $caller[2])");
 
-      if ($ENV{TEST_VERBOSE}) {
-        CORE::require('DBICTest/Util.pm');
-        Test::More::diag( 'Require invoked' .  DBICTest::Util::stacktrace() );
-      }
+      CORE::require('DBICTest/Util.pm');
+      Test::More::diag( 'Require invoked' .  DBICTest::Util::stacktrace() );
     }
 
     return $res;
@@ -69,7 +70,7 @@ BEGIN {
     if $ENV{PERL5OPT};
 
   plan skip_all => 'Dependency load patterns are radically different before perl 5.10'
-    if $] < 5.010;
+    if "$]" < 5.010;
 
   # add what we loaded so far
   for (keys %INC) {
@@ -80,6 +81,13 @@ BEGIN {
   }
 }
 
+BEGIN {
+  delete $ENV{$_} for qw(
+    DBICTEST_VIA_REPLICATED
+    DBICTEST_DEBUG_CONCURRENCY_LOCKS
+  );
+}
+
 #######
 ### This is where the test starts
 #######
@@ -99,13 +107,16 @@ BEGIN {
     namespace::clean
     Try::Tiny
     Sub::Name
+    Sub::Defer
+    Sub::Quote
 
     Scalar::Util
     List::Util
-    Data::Compare
+    Storable
 
     Class::Accessor::Grouped
     Class::C3::Componentised
+    SQL::Abstract
   ));
 
   require DBICTest::Schema;
@@ -116,7 +127,9 @@ BEGIN {
 {
   register_lazy_loadable_requires(qw(
     Moo
-    Sub::Quote
+    Moo::Object
+    Method::Generate::Accessor
+    Method::Generate::Constructor
     Context::Preserve
   ));
 
@@ -129,7 +142,6 @@ BEGIN {
 {
   register_lazy_loadable_requires(qw(
     DBI
-    SQL::Abstract
     Hash::Merge
   ));
 
@@ -185,16 +197,11 @@ sub assert_no_missing_expected_requires {
   my $nl;
   for my $mod (keys %$expected_dbic_deps) {
     (my $modfn = "$mod.pm") =~ s/::/\//g;
-    unless ($INC{$modfn}) {
-      my $err = sprintf "Expected DBIC core dependency '%s' never loaded - %s needs adjustment", $mod, __FILE__;
-      if (DBICTest::RunMode->is_smoker or DBICTest::RunMode->is_author) {
-        fail ($err)
-      }
-      else {
-        diag "\n" unless $nl->{$mod}++;
-        diag $err;
-      }
-    }
+    fail sprintf (
+      "Expected DBIC core dependency '%s' never loaded - %s needs adjustment",
+      $mod,
+      __FILE__
+    ) unless $INC{$modfn};
   }
   pass(sprintf 'All modules expected at %s line %s loaded by DBIC: %s',
     __FILE__,
similarity index 88%
rename from t/zzzzzzz_sqlite_deadlock.t
rename to xt/extra/sqlite_deadlock.t
index 6a38d2c..a9fdca9 100644 (file)
@@ -2,17 +2,11 @@ use strict;
 use warnings;
 
 use Test::More;
+use Test::Exception;
+use File::Temp ();
 
 use lib 't/lib';
-use DBICTest::RunMode;
-
-if ( DBICTest::RunMode->is_plain ) {
-  plan( skip_all => "Skipping test on plain module install" );
-}
-
-use Test::Exception;
 use DBICTest;
-use File::Temp ();
 
 plan tests => 2;
 my $wait_for = 120;  # how many seconds to wait
similarity index 81%
rename from t/105view_deps.t
rename to xt/extra/sqlite_view_deps.t
index 21aa92b..39bb632 100644 (file)
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'deploy';
 
 use strict;
 use warnings;
@@ -11,15 +11,6 @@ use DBICTest;
 use ViewDeps;
 use ViewDepsBad;
 
-BEGIN {
-    require DBIx::Class;
-    plan skip_all => 'Test needs ' .
-        DBIx::Class::Optional::Dependencies->req_missing_for('deploy')
-      unless DBIx::Class::Optional::Dependencies->req_ok_for('deploy');
-}
-
-use_ok('DBIx::Class::ResultSource::View');
-
 #################### SANITY
 
 my $view = DBIx::Class::ResultSource::View->new;
@@ -73,10 +64,16 @@ can_ok( $view, $_ ) for qw/new from deploy_depends_on/;
     = ViewDepsBad->connect( DBICTest->_database ( quote_char => '"') );
   ok( $schema2, 'Connected to ViewDepsBad schema OK' );
 
+  my $lazy_view_validity = !(
+    $schema2->storage->_server_info->{normalized_dbms_version}
+      <
+    3.009
+  );
+
 #################### DEPLOY2
 
   warnings_exist { $schema2->deploy }
-    [qr/no such table: main.aba_name_artists/],
+    [ $lazy_view_validity ? () : qr/no such table: main.aba_name_artists/ ],
     "Deploying the bad schema produces a warning: aba_name_artists was not created.";
 
 #################### DOES ORDERING WORK 2?
@@ -106,9 +103,15 @@ can_ok( $view, $_ ) for qw/new from deploy_depends_on/;
     } grep { !/AbaNameArtistsAnd2010CDsWithManyTracks/ }
     @{ [ $schema2->sources ] };
 
+  $schema2->storage->dbh->do(q( DROP VIEW "aba_name_artists" ))
+    if $lazy_view_validity;
+
   throws_ok { $schema2->resultset('AbaNameArtistsAnd2010CDsWithManyTracks')->next }
-    qr/no such table: aba_name_artists_and_2010_cds_with_many_tracks/,
-    "Query on AbaNameArtistsAnd2010CDsWithManyTracks throws, because the table does not exist"
+    qr/no such table: (?:main\.)?aba_name_artists/,
+    sprintf(
+      "Query on AbaNameArtistsAnd2010CDsWithManyTracks throws, because the%s view does not exist",
+      $lazy_view_validity ? ' underlying' : ''
+    )
   ;
 }
 
diff --git a/xt/optional_deps.t b/xt/optional_deps.t
deleted file mode 100644 (file)
index 0ae8023..0000000
+++ /dev/null
@@ -1,130 +0,0 @@
-use strict;
-use warnings;
-no warnings qw/once/;
-
-use Test::More;
-use Test::Exception;
-use lib qw(t/lib);
-use Scalar::Util; # load before we break require()
-use Carp ();   # Carp is not used in the test, but we want to have it loaded for proper %INC comparison
-
-# a dummy test which lazy-loads more modules (so we can compare INC below)
-ok (1);
-
-# record contents of %INC - makes sure there are no extra deps slipping into
-# Opt::Dep.
-my $inc_before = [ keys %INC ];
-ok ( (! grep { $_ =~ m|DBIx/Class| } @$inc_before ), 'Nothing DBIC related is yet loaded');
-
-# DBIx::Class::Optional::Dependencies queries $ENV at compile time
-# to build the optional requirements
-BEGIN {
-  $ENV{DBICTEST_PG_DSN} = '1';
-  delete $ENV{DBICTEST_ORA_DSN};
-}
-
-use_ok 'DBIx::Class::Optional::Dependencies';
-
-my $inc_after = [ keys %INC ];
-
-is_deeply (
-  [ sort @$inc_after],
-  [ sort (@$inc_before, 'DBIx/Class/Optional/Dependencies.pm') ],
-  'Nothing loaded other than DBIx::Class::OptDeps',
-);
-
-my $sqlt_dep = DBIx::Class::Optional::Dependencies->req_list_for ('deploy');
-is_deeply (
-  [ keys %$sqlt_dep ],
-  [ 'SQL::Translator' ],
-  'Correct deploy() dependency list',
-);
-
-# make module loading impossible, regardless of actual libpath contents
-{
-  local @INC = (sub { die('Optional Dep Test') } );
-
-  ok (
-    ! DBIx::Class::Optional::Dependencies->req_ok_for ('deploy'),
-    'deploy() deps missing',
-  );
-
-  like (
-    DBIx::Class::Optional::Dependencies->req_missing_for ('deploy'),
-    qr/^SQL::Translator \>\= \d/,
-    'expected missing string contents',
-  );
-
-  like (
-    DBIx::Class::Optional::Dependencies->req_errorlist_for ('deploy')->{'SQL::Translator'},
-    qr/Optional Dep Test/,
-    'custom exception found in errorlist',
-  );
-}
-
-#make it so module appears loaded
-$INC{'SQL/Translator.pm'} = 1;
-$SQL::Translator::VERSION = 999;
-
-ok (
-  ! DBIx::Class::Optional::Dependencies->req_ok_for ('deploy'),
-  'deploy() deps missing cached properly',
-);
-
-#reset cache
-%DBIx::Class::Optional::Dependencies::req_availability_cache = ();
-
-
-ok (
-  DBIx::Class::Optional::Dependencies->req_ok_for ('deploy'),
-  'deploy() deps present',
-);
-
-is (
-  DBIx::Class::Optional::Dependencies->req_missing_for ('deploy'),
-  '',
-  'expected null missing string',
-);
-
-is_deeply (
-  DBIx::Class::Optional::Dependencies->req_errorlist_for ('deploy'),
-  {},
-  'expected empty errorlist',
-);
-
-# test multiple times to find autovivification bugs
-for (1..2) {
-  throws_ok {
-    DBIx::Class::Optional::Dependencies->req_list_for();
-  } qr/\Qreq_list_for() expects a requirement group name/,
-  "req_list_for without groupname throws exception on run $_";
-
-  throws_ok {
-    DBIx::Class::Optional::Dependencies->req_list_for('');
-  } qr/\Qreq_list_for() expects a requirement group name/,
-  "req_list_for with empty groupname throws exception on run $_";
-
-  throws_ok {
-    DBIx::Class::Optional::Dependencies->req_list_for('invalid_groupname');
-  } qr/Requirement group 'invalid_groupname' does not exist/,
-  "req_list_for with invalid groupname throws exception on run $_";
-}
-
-is_deeply(
-  DBIx::Class::Optional::Dependencies->req_list_for('rdbms_pg'),
-  {
-    'DBD::Pg' => '0',
-  }, 'optional dependencies for deploying to Postgres ok');
-
-is_deeply(
-  DBIx::Class::Optional::Dependencies->req_list_for('test_rdbms_pg'),
-  {
-    $^O ne 'MSWin32' ? ('Sys::SigAction' => '0') : (),
-    'DBD::Pg'        => '2.009002',
-  }, 'optional dependencies for testing Postgres with ENV var ok');
-
-is_deeply(
-  DBIx::Class::Optional::Dependencies->req_list_for('test_rdbms_oracle'),
-  {}, 'optional dependencies for testing Oracle without ENV var ok');
-
-done_testing;
diff --git a/xt/pod.t b/xt/pod.t
deleted file mode 100644 (file)
index 0ed796b..0000000
--- a/xt/pod.t
+++ /dev/null
@@ -1,20 +0,0 @@
-use warnings;
-use strict;
-
-use Test::More;
-use lib qw(t/lib);
-use DBICTest;
-
-require DBIx::Class;
-unless ( DBIx::Class::Optional::Dependencies->req_ok_for ('test_pod') ) {
-  my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('test_pod');
-  $ENV{RELEASE_TESTING}
-    ? die ("Failed to load release-testing module requirements: $missing")
-    : plan skip_all => "Test needs: $missing"
-}
-
-# this has already been required but leave it here for CPANTS static analysis
-require Test::Pod;
-
-my $generated_pod_dir = 'maint/.Generated_Pod';
-Test::Pod::all_pod_files_ok( 'lib', -d $generated_pod_dir ? $generated_pod_dir : () );
diff --git a/xt/strictures.t b/xt/strictures.t
deleted file mode 100644 (file)
index 3996621..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-use warnings;
-use strict;
-
-use Test::More;
-use lib 't/lib';
-use DBICTest;
-
-unless ( DBIx::Class::Optional::Dependencies->req_ok_for ('test_strictures') ) {
-  my $missing = DBIx::Class::Optional::Dependencies->req_missing_for ('test_strictures');
-  $ENV{RELEASE_TESTING}
-    ? die ("Failed to load release-testing module requirements: $missing")
-    : plan skip_all => "Test needs: $missing"
-}
-
-
-use File::Find;
-
-find({
-  wanted => sub {
-    -f $_ or return;
-    m/\.(?: pm | pl | t )$ /ix or return;
-
-    return if m{^(?:
-      maint/Makefile.PL.inc/.+                        # all the maint inc snippets are auto-strictured
-        |
-      t/lib/DBICTest/Util/OverrideRequire.pm          # no stictures by design (load order sensitive)
-    )$}x;
-
-    my $f = $_;
-
-    Test::Strict::strict_ok($f);
-    Test::Strict::warnings_ok($f);
-
-    #Test::Strict::syntax_ok($f) if $f =~ /^ (?: lib  )/x;
-  },
-  no_chdir => 1,
-}, (qw(lib t examples maint)) );
-
-done_testing;