Merge 'trunk' into 'multicreate_fixes'
Peter Rabbitson [Tue, 4 Aug 2009 12:29:01 +0000 (12:29 +0000)]
r6636@Thesaurus (orig r6635):  ribasushi | 2009-06-11 21:29:10 +0200
Release 0.08105
r6637@Thesaurus (orig r6636):  frew | 2009-06-11 22:02:11 +0200
Escape filename so that t\9... won't look like octal
r6638@Thesaurus (orig r6637):  ribasushi | 2009-06-11 22:09:55 +0200
Adjust tests for the DT::F::MySQL -> DT::F::SQLite change
r6639@Thesaurus (orig r6638):  arcanez | 2009-06-11 22:29:53 +0200
cookbook tweak for count distinct
r6640@Thesaurus (orig r6639):  ribasushi | 2009-06-11 23:39:12 +0200
Factor out as_query properly
r6641@Thesaurus (orig r6640):  ribasushi | 2009-06-11 23:50:41 +0200
Release 0.08106
r6643@Thesaurus (orig r6642):  caelum | 2009-06-12 01:48:28 +0200
add support for DBICTEST_MSSQL_PERL5LIB to 74mssql.t
r6648@Thesaurus (orig r6647):  timbunce | 2009-06-12 16:27:38 +0200
Added reference to get_inflated_columns in get_columns docs.

r6662@Thesaurus (orig r6661):  ribasushi | 2009-06-13 17:56:54 +0200
Test resultset serialization as well
r6663@Thesaurus (orig r6662):  ribasushi | 2009-06-13 18:08:14 +0200
local()ize sqla for in the right place
r6664@Thesaurus (orig r6663):  ribasushi | 2009-06-13 18:10:22 +0200
Do not use raw sources in {from} - proxy via source handles
r6666@Thesaurus (orig r6665):  ribasushi | 2009-06-13 19:31:55 +0200
really local()ize for as the author intended
r6667@Thesaurus (orig r6666):  michaelr | 2009-06-14 00:23:15 +0200
Added documentation for from => $rs->as_query

r6668@Thesaurus (orig r6667):  caelum | 2009-06-14 01:20:02 +0200
fix master debug output for ::Replicated
r6669@Thesaurus (orig r6668):  ribasushi | 2009-06-14 10:22:28 +0200
Release 0.08107
r6671@Thesaurus (orig r6670):  ribasushi | 2009-06-14 11:00:35 +0200
Lapse in copy() docs
r6673@Thesaurus (orig r6672):  ribasushi | 2009-06-14 11:33:22 +0200
Forgotten piece of as_query refactor
r6683@Thesaurus (orig r6682):  timbunce | 2009-06-15 16:30:21 +0200
Added note that column inflation is not performed

r6690@Thesaurus (orig r6689):  timbunce | 2009-06-16 15:06:07 +0200
Removed wording from txn_do that implies the coderef could be executed more than once.

r6691@Thesaurus (orig r6690):  timbunce | 2009-06-16 15:14:23 +0200
Added doc note that txn_commit does not perform an actual storage commit unless
there's a DBIx::Class transaction currently in effect

r6692@Thesaurus (orig r6691):  timbunce | 2009-06-16 15:40:11 +0200
Reverted doc patch r6689 for now, sadly. I'll open a ticket to explain.

r6694@Thesaurus (orig r6693):  ribasushi | 2009-06-16 17:22:59 +0200
Fix possible regression with prefetch select resolution
r6699@Thesaurus (orig r6698):  wintrmute | 2009-06-17 10:32:30 +0200
Replace vague language around whether load_classes/namespaces is preferred,
with an explanation that load_namespaces() is generally preferred, and explain
when load_classes is appropriate.
r6702@Thesaurus (orig r6701):  caelum | 2009-06-17 19:50:47 +0200
fix page with offset bug
r6704@Thesaurus (orig r6703):  ribasushi | 2009-06-18 08:40:18 +0200
Cleanup attribute handling - I deal with resolved attributes throughout, no point in complicating things further
r6705@Thesaurus (orig r6704):  abraxxa | 2009-06-18 12:30:01 +0200
added test for nested has_many prefetch without entries

r6707@Thesaurus (orig r6706):  ribasushi | 2009-06-18 12:43:36 +0200
HRI fix
r6708@Thesaurus (orig r6707):  ribasushi | 2009-06-18 14:05:42 +0200
wtf
r6714@Thesaurus (orig r6713):  caelum | 2009-06-19 01:03:01 +0200
fix broken link in manual
r6726@Thesaurus (orig r6725):  ribasushi | 2009-06-19 17:25:19 +0200
 r6706@Thesaurus (orig r6705):  ribasushi | 2009-06-18 12:30:08 +0200
 Branch to attempt prefetch with limit fix
 r6709@Thesaurus (orig r6708):  ribasushi | 2009-06-18 15:54:38 +0200
 This seems to be the prefetch+limit fix - ugly as hell but appears to work
 r6710@Thesaurus (orig r6709):  ribasushi | 2009-06-18 16:13:31 +0200
 More comments
 r6717@Thesaurus (orig r6716):  ribasushi | 2009-06-19 15:39:43 +0200
 single() throws with has_many prefetch
 r6718@Thesaurus (orig r6717):  ribasushi | 2009-06-19 15:40:38 +0200
 Rename test
 r6719@Thesaurus (orig r6718):  ribasushi | 2009-06-19 15:44:26 +0200
 cleanup svn attrs
 r6720@Thesaurus (orig r6719):  ash | 2009-06-19 16:31:11 +0200
 Add extra test for prefetch+has_many

 r6721@Thesaurus (orig r6720):  ribasushi | 2009-06-19 16:33:49 +0200
 no need to slice as use_prefetch already has a limit
 r6722@Thesaurus (orig r6721):  ribasushi | 2009-06-19 16:36:08 +0200
 throw in an extra limit, sophisticate test a bit
 r6723@Thesaurus (orig r6722):  ribasushi | 2009-06-19 16:36:54 +0200
 Fix the fix
 r6725@Thesaurus (orig r6724):  ribasushi | 2009-06-19 17:24:23 +0200
 Fix dubious optimization

r6734@Thesaurus (orig r6733):  ribasushi | 2009-06-20 10:16:02 +0200
todoify skip
r6736@Thesaurus (orig r6735):  ribasushi | 2009-06-20 12:37:52 +0200
Clarify test
r6740@Thesaurus (orig r6739):  ribasushi | 2009-06-20 15:22:06 +0200
Disambiguate populate() return
r6743@Thesaurus (orig r6742):  ribasushi | 2009-06-20 23:30:23 +0200
 r6737@Thesaurus (orig r6736):  ribasushi | 2009-06-20 12:39:34 +0200
 new branch to streamline count() and introduce count_rs()
 r6738@Thesaurus (orig r6737):  ribasushi | 2009-06-20 12:44:09 +0200
 Add count_rs, move the code back from DBI - leave only sql specific hooks
 r6739@Thesaurus (orig r6738):  ribasushi | 2009-06-20 12:54:11 +0200
 Test count_rs
 r6742@Thesaurus (orig r6741):  ribasushi | 2009-06-20 23:30:10 +0200
 More tests and a really working count_rs

r6753@Thesaurus (orig r6752):  ribasushi | 2009-06-21 09:00:21 +0200
Clenaup text
r6754@Thesaurus (orig r6753):  ribasushi | 2009-06-21 14:37:56 +0200
make_column_dirty fix
r6756@Thesaurus (orig r6755):  ribasushi | 2009-06-21 23:12:40 +0200
Fix borked test
r6764@Thesaurus (orig r6763):  ribasushi | 2009-06-23 10:33:59 +0200
Real inheritance ordering for load_namespaces
r6772@Thesaurus (orig r6771):  ribasushi | 2009-06-23 16:46:18 +0200
Move tests around, add extra has_one relationship
r6773@Thesaurus (orig r6772):  caelum | 2009-06-23 18:36:22 +0200
add missing ' to doc
r6781@Thesaurus (orig r6780):  ribasushi | 2009-06-24 11:08:02 +0200
Properly name the relinfo variable
r6782@Thesaurus (orig r6781):  ribasushi | 2009-06-24 12:12:49 +0200
find_related fix for single-type relationships
r6783@Thesaurus (orig r6782):  nigel | 2009-06-24 17:28:33 +0200
 r11786@hex:  nigel | 2009-06-24 16:27:58 +0100
 Fixed set_$rel with where restriction deleting rows outside the restriction

r6784@Thesaurus (orig r6783):  nigel | 2009-06-24 17:47:31 +0200
 r11788@hex:  nigel | 2009-06-24 16:47:04 +0100
 Rework of set_$rel patch with less obfuscation

r6789@Thesaurus (orig r6788):  ribasushi | 2009-06-25 09:19:10 +0200
Commit test inspired by joel - it seemingly fails on Mac?
r6790@Thesaurus (orig r6789):  ribasushi | 2009-06-25 11:04:26 +0200
Minor cleanups
r6793@Thesaurus (orig r6792):  teejay | 2009-06-26 14:43:05 +0200
normalised artist_id, and plural relationships to plural names making use of alias/relname less ambiguous than relname/tablename being the same, also added a little more info on joining/relationships
r6794@Thesaurus (orig r6793):  tomboh | 2009-06-26 15:25:19 +0200
Documentation fix:
- timezone is no longer an extra setting
- fix a typo of 'subsequently'

r6795@Thesaurus (orig r6794):  gphat | 2009-06-26 16:33:35 +0200
Fix typo in ResultSet docs

r6803@Thesaurus (orig r6802):  ribasushi | 2009-06-27 12:39:03 +0200
Todoified (unsolvable) test from RT#42466
r6804@Thesaurus (orig r6803):  ribasushi | 2009-06-27 12:52:26 +0200
POD patch from RT#46808
r6805@Thesaurus (orig r6804):  ribasushi | 2009-06-27 13:59:03 +0200
Adjust sqlt schema parser to add tables in FK dependency order
r6806@Thesaurus (orig r6805):  ribasushi | 2009-06-27 14:08:35 +0200
Bump author SQLT dependency for early developer testing
Regenerate SQLite schema with new parser/sqlt
Use throw_exception in lieu of plain die when possible
r6813@Thesaurus (orig r6812):  castaway | 2009-06-28 06:11:08 +0200
Tests for grouping with prefetch

r6820@Thesaurus (orig r6819):  ribasushi | 2009-06-28 13:00:03 +0200
The prefetch+group_by is a complex problem - branch
r6844@Thesaurus (orig r6843):  abraxxa | 2009-06-29 11:02:17 +0200
fixed typo in test

r6848@Thesaurus (orig r6847):  ribasushi | 2009-06-29 19:09:00 +0200
Minor Ordered optimization (don't use count)
r6856@Thesaurus (orig r6855):  caelum | 2009-06-29 23:42:11 +0200
 r5451@hlagh (orig r6605):  caelum | 2009-06-10 09:23:44 -0700
 new branch to implement on_connect_call
 r5484@hlagh (orig r6633):  caelum | 2009-06-11 11:03:10 -0700
 on_connect_call implementation and set_datetime_format support for Oracle
 r5492@hlagh (orig r6641):  caelum | 2009-06-11 16:39:28 -0700
 connect_call_set_datetime_format for Oracle, I have no idea why this didn't get committed before...
 r5504@hlagh (orig r6655):  caelum | 2009-06-12 17:28:06 -0700
 finished up on_connect_call stuff
 r5507@hlagh (orig r6658):  caelum | 2009-06-13 04:03:36 -0700
 fixup _setup_connect_do, other minor cleanups
 r5508@hlagh (orig r6659):  caelum | 2009-06-13 04:35:33 -0700
 make the on_(dis)?connect_do accessors returnn the original structure
 r5509@hlagh (orig r6660):  caelum | 2009-06-13 08:31:52 -0700
 allow undef for _setup_connect_do
 r5522@hlagh (orig r6679):  caelum | 2009-06-14 09:56:40 -0700
 rename connect_do store
 r5621@hlagh (orig r6769):  caelum | 2009-06-23 07:38:33 -0700
 minor doc update
 r5628@hlagh (orig r6777):  caelum | 2009-06-23 16:36:12 -0700
 properly test nanosecond precision with oracle and datetime_setup
 r5669@hlagh (orig r6784):  caelum | 2009-06-24 10:49:25 -0700
 IC::DT does support timestamp with timezone
 r5768@hlagh (orig r6846):  caelum | 2009-06-29 08:20:32 -0700
 remove DateTime from 73oracle.t
 r5781@hlagh (orig r6849):  caelum | 2009-06-29 13:07:43 -0700
 remove the _store stuff for on_connect_do
 r5785@hlagh (orig r6853):  ribasushi | 2009-06-29 14:38:30 -0700
 Some beautification

r6871@Thesaurus (orig r6870):  ribasushi | 2009-06-30 10:09:03 +0200
Cleanup dependency handling a bit
r6875@Thesaurus (orig r6874):  ribasushi | 2009-06-30 12:39:06 +0200
Allow broken resultsource-class-derived objects to still work
r6876@Thesaurus (orig r6875):  ribasushi | 2009-06-30 12:40:46 +0200
clarify
r6878@Thesaurus (orig r6877):  ash | 2009-06-30 13:48:13 +0200
Update POD on Dynamic sub-classing

r6883@Thesaurus (orig r6882):  ribasushi | 2009-06-30 17:36:38 +0200
 r6815@Thesaurus (orig r6814):  ribasushi | 2009-06-28 10:32:42 +0200
 Branch to explore double joins on search_related
 r6816@Thesaurus (orig r6815):  ribasushi | 2009-06-28 10:34:16 +0200
 Thetest case that started it all
 r6817@Thesaurus (orig r6816):  ribasushi | 2009-06-28 10:35:11 +0200
 The proposed fix (do not add an extra join if it is already present in the topmost join)
 r6818@Thesaurus (orig r6817):  ribasushi | 2009-06-28 11:04:26 +0200
 Minor omission
 r6819@Thesaurus (orig r6818):  ribasushi | 2009-06-28 11:07:33 +0200
 Adjust a couple of tests for new behavior (thus all of this might be backwards incompatible to the point of being useless):
 The counts in t/90join_torture.t are now 5*3, not 5*3*3, as a second join is not induced by search_related
 The raw sql scan in t/prefetch/standard.t is just silly, won't even try to understand it
 Just to maintain the TreeLike folding, I add a 3rd children join which was inserted by search_related before the code changes

r6889@Thesaurus (orig r6888):  ribasushi | 2009-06-30 19:36:11 +0200
Todoify test for now
r6890@Thesaurus (orig r6889):  ribasushi | 2009-06-30 19:37:05 +0200
Todoify test for now (2)
r6892@Thesaurus (orig r6891):  ribasushi | 2009-06-30 19:52:31 +0200
Todoify test for now (3)
r6903@Thesaurus (orig r6902):  ribasushi | 2009-07-01 08:46:12 +0200
Fixed deadlock test
r6904@Thesaurus (orig r6903):  ribasushi | 2009-07-01 12:22:00 +0200
Clarify exception text
r6907@Thesaurus (orig r6906):  ribasushi | 2009-07-01 13:23:46 +0200
 r6821@Thesaurus (orig r6820):  ribasushi | 2009-06-28 13:09:11 +0200
 Branch for prefetch+group play
 r6823@Thesaurus (orig r6822):  ribasushi | 2009-06-28 14:38:36 +0200
 Normalize group_by
 r6824@Thesaurus (orig r6823):  ribasushi | 2009-06-28 14:39:54 +0200
 Proper prefetch+group test
 r6826@Thesaurus (orig r6825):  ribasushi | 2009-06-28 14:42:48 +0200
 Whoops
 r6828@Thesaurus (orig r6827):  ribasushi | 2009-06-28 15:06:57 +0200
 Lose the literal sql bits - castaway is right it's silly to support those
 r6833@Thesaurus (orig r6832):  ribasushi | 2009-06-28 22:38:43 +0200
 Rogue comments
 r6837@Thesaurus (orig r6836):  ribasushi | 2009-06-29 09:44:25 +0200
 A couple of test fixes
 r6838@Thesaurus (orig r6837):  ribasushi | 2009-06-29 09:46:13 +0200
 Support for -select/-as in SQLAHacks field selection
 r6839@Thesaurus (orig r6838):  ribasushi | 2009-06-29 09:49:53 +0200
 This is tested elsewhere
 r6840@Thesaurus (orig r6839):  ribasushi | 2009-06-29 09:50:43 +0200
 This is tested elsewhere (2)
 r6841@Thesaurus (orig r6840):  ribasushi | 2009-06-29 10:07:09 +0200
 Test cleanups
 r6842@Thesaurus (orig r6841):  ribasushi | 2009-06-29 10:11:13 +0200
 Most of the grouped prefetch solution
 r6843@Thesaurus (orig r6842):  ribasushi | 2009-06-29 10:14:45 +0200
 clearer
 r6845@Thesaurus (orig r6844):  ribasushi | 2009-06-29 12:05:37 +0200
 And score! (all works)
 r6882@Thesaurus (orig r6881):  ribasushi | 2009-06-30 16:23:06 +0200
 rs->get_column now properly recognizes prefetch and collapses if at all possible
 r6886@Thesaurus (orig r6885):  ribasushi | 2009-06-30 17:39:58 +0200
 Whoops

r6910@Thesaurus (orig r6909):  ribasushi | 2009-07-01 13:27:15 +0200
Optimize set_column on uninserted objects
r6921@Thesaurus (orig r6920):  caelum | 2009-07-01 17:40:32 +0200
 r5859@hlagh (orig r6912):  caelum | 2009-07-01 06:21:30 -0700
 new connected() for dbd::sybase users
 r5860@hlagh (orig r6913):  caelum | 2009-07-01 06:25:46 -0700
 add a couple of dbd::sybase reconnection tests
 r5861@hlagh (orig r6914):  caelum | 2009-07-01 06:35:07 -0700
 better connection test
 r5862@hlagh (orig r6915):  caelum | 2009-07-01 06:45:05 -0700
 use dbh->do for connected instead of prepare_cached
 r5863@hlagh (orig r6916):  ribasushi | 2009-07-01 06:55:21 -0700
 Segfault
 r5864@hlagh (orig r6917):  caelum | 2009-07-01 07:03:22 -0700
 use ->do instead of ->prepare_cached in oracle's connected() too
 r5865@hlagh (orig r6918):  caelum | 2009-07-01 08:20:52 -0700
 fix segfault with old DBD::Sybase
 r5866@hlagh (orig r6919):  caelum | 2009-07-01 08:39:18 -0700
 move connection tests into _ping()

r6924@Thesaurus (orig r6923):  ijw | 2009-07-01 19:34:32 +0200
Added a test for a resultset to related-resultset join for 0 related records
r6928@Thesaurus (orig r6927):  ijw | 2009-07-01 20:04:16 +0200
Additional tests on prefetch - illustrates the bug with left-join has_many (NULL row returned) and the one that results from the trivial fix (prefetch gives no artist)
r6932@Thesaurus (orig r6931):  ribasushi | 2009-07-02 08:08:33 +0200
Another candidate for somethingawful.com (fix left join-ed count)
r6934@Thesaurus (orig r6933):  ribasushi | 2009-07-02 09:04:13 +0200
Changelog
r6935@Thesaurus (orig r6934):  ribasushi | 2009-07-02 11:23:48 +0200
cleanup
r6936@Thesaurus (orig r6935):  ijw | 2009-07-02 12:41:01 +0200
Check fetched rows == count for related resultsets
r6937@Thesaurus (orig r6936):  ijw | 2009-07-02 12:43:47 +0200
Confirm prefetch doesn't affect main row fetch, and main row fetch works with and without counting
r6938@Thesaurus (orig r6937):  ribasushi | 2009-07-02 12:52:51 +0200
More fail (fix is known but needs work)
r6939@Thesaurus (orig r6938):  ribasushi | 2009-07-02 13:07:22 +0200
And more fail
r6940@Thesaurus (orig r6939):  ribasushi | 2009-07-02 13:16:46 +0200
These tests are in prefetch/count.t
r6941@Thesaurus (orig r6940):  ribasushi | 2009-07-02 13:38:31 +0200
cleanup
r6942@Thesaurus (orig r6941):  ribasushi | 2009-07-02 13:38:49 +0200
Solve more prefetch inflation crap
r6943@Thesaurus (orig r6942):  ribasushi | 2009-07-02 13:47:41 +0200
Make the code readable
r6944@Thesaurus (orig r6943):  ribasushi | 2009-07-02 15:52:35 +0200
Everything works, just need to fix join-path chaining over search_related (to guard against obscure db quirks)
r6946@Thesaurus (orig r6945):  caelum | 2009-07-02 21:06:32 +0200
add sybase reconnect test
r6948@Thesaurus (orig r6947):  ribasushi | 2009-07-02 22:20:21 +0200
Last part of the join handling puzzle
r6951@Thesaurus (orig r6950):  ribasushi | 2009-07-03 00:14:50 +0200
 r6360@Thesaurus (orig r6359):  arcanez | 2009-05-21 20:18:52 +0200
 branch to work on prefetch/select
 r6361@Thesaurus (orig r6360):  arcanez | 2009-05-21 20:32:46 +0200
 failing test
 r6373@Thesaurus (orig r6372):  ribasushi | 2009-05-22 11:07:26 +0200
 Simplify unresolvable test by arcanez
 r6905@Thesaurus (orig r6904):  ribasushi | 2009-07-01 12:54:03 +0200
 Extend test
 r6950@Thesaurus (orig r6949):  ribasushi | 2009-07-03 00:14:09 +0200
 Apparent fix - simply delay the in_storage flagging of the main object until all prefetched objects are inflated. The rest of the changes are just cosmetics, preparing for the collapse_result rewrite

r6953@Thesaurus (orig r6952):  ribasushi | 2009-07-03 00:17:22 +0200
Changes
r6965@Thesaurus (orig r6964):  ribasushi | 2009-07-03 13:19:27 +0200
Add set_ansi_mode on_connect_call for mysql
Also switch to _do_query instead of plain dbh->do (shows up in the trace)
r6966@Thesaurus (orig r6965):  ribasushi | 2009-07-03 13:37:06 +0200
Capitalize mysql commands
r6967@Thesaurus (orig r6966):  ribasushi | 2009-07-03 15:07:49 +0200
Double an existing might_have test as has_one
r6968@Thesaurus (orig r6967):  ribasushi | 2009-07-03 16:36:32 +0200
Extra test to demonstrate has_one working, and a POD clarification of multicreate
r6973@Thesaurus (orig r6972):  ribasushi | 2009-07-03 20:20:42 +0200
 r6554@Thesaurus (orig r6553):  frew | 2009-06-09 00:06:42 +0200
 branch for mssql top issues
 r6572@Thesaurus (orig r6571):  frew | 2009-06-09 23:18:46 +0200
 more tests for SQL Server!
 r6573@Thesaurus (orig r6572):  frew | 2009-06-09 23:49:10 +0200
 Added AmbiguousGlob.pm for silly servers like mssql and mysql.  See docs for more info
 r6574@Thesaurus (orig r6573):  frew | 2009-06-09 23:55:22 +0200
 fix plan
 r6602@Thesaurus (orig r6601):  frew | 2009-06-10 17:03:30 +0200
 more failing tests
 r6608@Thesaurus (orig r6607):  frew | 2009-06-10 20:05:53 +0200
 don't use eval!
 r6610@Thesaurus (orig r6609):  frew | 2009-06-10 20:07:49 +0200
 beginning of DWIM for IDENTITY_INSERT
 r6628@Thesaurus (orig r6627):  frew | 2009-06-11 18:13:02 +0200
 still busted :-(
 r6631@Thesaurus (orig r6630):  frew | 2009-06-11 19:39:00 +0200
 general function to go from column names and ident to result source
 r6632@Thesaurus (orig r6631):  frew | 2009-06-11 19:40:11 +0200
 Use new _resolve_column_sources method and begin insert_bulk method
 r6635@Thesaurus (orig r6634):  frew | 2009-06-11 20:12:38 +0200
 updated _resolve_column_source to _resolve_column_info as per ribasushi's suggestion
 r6650@Thesaurus (orig r6649):  frew | 2009-06-12 17:13:32 +0200
 Now I just need to check if the actual values are set...
 r6651@Thesaurus (orig r6650):  frew | 2009-06-12 17:26:53 +0200
 Insert Identity works!
 r6652@Thesaurus (orig r6651):  frew | 2009-06-12 17:34:13 +0200
 silly warns.
 r6684@Thesaurus (orig r6683):  frew | 2009-06-15 16:49:00 +0200
 failing test
 r6686@Thesaurus (orig r6685):  ribasushi | 2009-06-15 18:10:26 +0200
 make all resolved attrs visible to sqla
 r6698@Thesaurus (orig r6697):  ribasushi | 2009-06-17 02:31:37 +0200
 Half way working stuff, needs a LOT of tweaking still
 r6729@Thesaurus (orig r6728):  ribasushi | 2009-06-19 19:49:27 +0200
 Merge badness
 r6730@Thesaurus (orig r6729):  ribasushi | 2009-06-19 19:49:40 +0200
 fix eol
 r6731@Thesaurus (orig r6730):  ribasushi | 2009-06-19 19:55:47 +0200
 augment inheritance
 r6735@Thesaurus (orig r6734):  ribasushi | 2009-06-20 10:34:42 +0200
 Maybe I've nailed it
 r6746@Thesaurus (orig r6745):  ribasushi | 2009-06-20 23:53:55 +0200
 Test and merge fixes
 r6747@Thesaurus (orig r6746):  ribasushi | 2009-06-21 00:01:09 +0200
 Really fix tests
 r6748@Thesaurus (orig r6747):  ribasushi | 2009-06-21 00:01:54 +0200
 Really fix tests
 r6749@Thesaurus (orig r6748):  ribasushi | 2009-06-21 00:18:33 +0200
 Now really final
 r6750@Thesaurus (orig r6749):  ribasushi | 2009-06-21 00:22:23 +0200
 whoops
 r6751@Thesaurus (orig r6750):  ribasushi | 2009-06-21 00:42:18 +0200
 That should be all
 r6752@Thesaurus (orig r6751):  ribasushi | 2009-06-21 08:54:00 +0200
 Make sure quoting works
 r6755@Thesaurus (orig r6754):  ribasushi | 2009-06-21 15:21:23 +0200
 Groundwork for sanification of the toplimit test
 r6863@Thesaurus (orig r6862):  ribasushi | 2009-06-30 01:13:49 +0200
 Make sure storage classes use c3, just like the rest of dbic (tested on 5.8 as well)
 r6869@Thesaurus (orig r6868):  ribasushi | 2009-06-30 09:53:27 +0200
 Some fixes after review
 r6874@Thesaurus (orig r6873):  ribasushi | 2009-06-30 11:54:34 +0200
 Fix borked next invocation
 r6896@Thesaurus (orig r6895):  frew | 2009-06-30 21:38:26 +0200
 silly misspells and trailing whitespace
 r6955@Thesaurus (orig r6954):  ribasushi | 2009-07-03 01:21:28 +0200
 Some hack consolidation
 r6962@Thesaurus (orig r6961):  ribasushi | 2009-07-03 12:06:57 +0200
 Fix some mssql shortcommings when confronted with the new subequeried prefetch sql
 r6963@Thesaurus (orig r6962):  ribasushi | 2009-07-03 12:47:57 +0200
 Ask for newer DBD::Pg in author mode, suggest the newer version otherwise (proper array support). Make test more resilient as well
 r6964@Thesaurus (orig r6963):  ribasushi | 2009-07-03 12:49:16 +0200
 Switch to C3 mro throughout the ::Storage hierarchy (DBIx::Class brings in MRO::Compat, and all ::Storage's are based on it, tested on 5.8
 r6969@Thesaurus (orig r6968):  ribasushi | 2009-07-03 19:54:04 +0200
 Duh
 r6970@Thesaurus (orig r6969):  frew | 2009-07-03 19:59:48 +0200
 fix tests for new codez
 r6971@Thesaurus (orig r6970):  ribasushi | 2009-07-03 20:18:53 +0200
 detabify
 r6972@Thesaurus (orig r6971):  ribasushi | 2009-07-03 20:20:07 +0200
 changes

r6980@Thesaurus (orig r6979):  ribasushi | 2009-07-04 11:34:08 +0200
Hide devel documentation from the indexer
r6981@Thesaurus (orig r6980):  ribasushi | 2009-07-04 11:37:25 +0200
Add set_ansi_mode POD
r6982@Thesaurus (orig r6981):  ribasushi | 2009-07-04 11:45:24 +0200
Backout mysql changes for further polishing
r6985@Thesaurus (orig r6984):  ribasushi | 2009-07-04 12:08:16 +0200
Missing newline
r6986@Thesaurus (orig r6985):  ribasushi | 2009-07-04 12:11:18 +0200
typo
r6987@Thesaurus (orig r6986):  ribasushi | 2009-07-04 12:40:47 +0200
Fix POD
r6988@Thesaurus (orig r6987):  ribasushi | 2009-07-04 13:09:39 +0200
todos are shorter now
r6990@Thesaurus (orig r6989):  castaway | 2009-07-05 22:00:55 +0200
Added Pod::Inherit use to Makefile.PL at author-time, comments/suggestions as to whether its too "noisy" welcome.

r6991@Thesaurus (orig r6990):  ribasushi | 2009-07-06 00:06:52 +0200
Couple of makefile fixes:
use is compile time, use require
recommends is for distro maintainers only, push the dependency into the authors hash (it is not to be executed by mere mortals)

r6992@Thesaurus (orig r6991):  ribasushi | 2009-07-06 00:55:36 +0200
Forgotten pod exclusions
r6993@Thesaurus (orig r6992):  ribasushi | 2009-07-06 01:07:05 +0200
Temporarily backout Pod::Inherit changes
r6994@Thesaurus (orig r6993):  ribasushi | 2009-07-06 01:10:22 +0200
Put Pod::Inherit stuff back after proper copy
r7010@Thesaurus (orig r7009):  ribasushi | 2009-07-09 12:45:02 +0200
 r6995@Thesaurus (orig r6994):  ribasushi | 2009-07-06 01:12:57 +0200
 Where 08108 will come from

r7028@Thesaurus (orig r7027):  caelum | 2009-07-10 23:56:57 +0200
fix PodInherit call in Makefile.PL
r7030@Thesaurus (orig r7029):  robkinyon | 2009-07-11 00:03:07 +0200
Applied patch from kados regarding use of a DateTime::Format class to validate
r7031@Thesaurus (orig r7030):  caelum | 2009-07-11 11:26:40 +0200
reword IC::DT doc patch
r7038@Thesaurus (orig r7037):  dandv | 2009-07-13 14:06:08 +0200
PK::Auto has moved into Core since 2007
r7039@Thesaurus (orig r7038):  dandv | 2009-07-13 14:15:13 +0200
Fixed has_many example in Intro.pod
r7040@Thesaurus (orig r7039):  dandv | 2009-07-13 22:58:45 +0200
Fixed run-on sentences in FAQ
r7041@Thesaurus (orig r7040):  dandv | 2009-07-13 23:18:11 +0200
Minor POD fixes in Example.pod
r7042@Thesaurus (orig r7041):  dandv | 2009-07-13 23:48:18 +0200
Favored using ->single to get the topmost result over less readable ->slice(0)
r7043@Thesaurus (orig r7042):  dandv | 2009-07-14 00:56:31 +0200
Minor POD fixes in Cookbook
r7046@Thesaurus (orig r7045):  ribasushi | 2009-07-14 13:30:55 +0200
Minor logic cleanup
r7047@Thesaurus (orig r7046):  ribasushi | 2009-07-14 14:07:11 +0200
grouped prefetch fix
r7054@Thesaurus (orig r7053):  ijw | 2009-07-15 18:55:35 +0200
Added SQLA link for more comprehensive documentation of order_by options available
r7057@Thesaurus (orig r7056):  caelum | 2009-07-16 00:54:22 +0200
add "smalldatetime" support to IC::DT
r7060@Thesaurus (orig r7059):  ribasushi | 2009-07-16 06:29:41 +0200
 r7013@Thesaurus (orig r7012):  jnapiorkowski | 2009-07-09 17:00:22 +0200
 new branch
 r7014@Thesaurus (orig r7013):  jnapiorkowski | 2009-07-09 20:06:44 +0200
 changed the way transactions are detected for replication to work with the standard way to do this, minor doc updates, fix to the force pool so you can force a particular slave, changes to the way the debugging is created
 r7015@Thesaurus (orig r7014):  jnapiorkowski | 2009-07-09 20:17:03 +0200
 more changes to the way debug output works
 r7016@Thesaurus (orig r7015):  jnapiorkowski | 2009-07-09 22:26:47 +0200
 big update to the test suite so that we now check to make sure the storage that was expected was actually used
 r7017@Thesaurus (orig r7016):  jnapiorkowski | 2009-07-09 23:23:37 +0200
 set correct number of tests, changed the debuggin output to not warn on DDL, minor change to a test resultclass so we can deploy to mysql properly
 r7018@Thesaurus (orig r7017):  jnapiorkowski | 2009-07-09 23:26:59 +0200
 corrected the number of skipped tests
 r7019@Thesaurus (orig r7018):  jnapiorkowski | 2009-07-09 23:52:22 +0200
 fixed test resultclass formatting, added a few more DBIC::Storage::DBI methods that I might need to delegate.
 r7020@Thesaurus (orig r7019):  jnapiorkowski | 2009-07-10 01:23:07 +0200
 some documention updates and changed the way we find paths for the sqlite dbfiles to use File::Spec, which I hope will solve some of the Win32 error messages
 r7023@Thesaurus (orig r7022):  jnapiorkowski | 2009-07-10 18:00:38 +0200
 pod cleanup, fixed broken pod links, and new Introduction pod
 r7024@Thesaurus (orig r7023):  jnapiorkowski | 2009-07-10 19:10:57 +0200
 updated Changes file to reflect work completed
 r7025@Thesaurus (orig r7024):  jnapiorkowski | 2009-07-10 19:37:53 +0200
 a few more Moose Type related fixes and added diag to the replication test to report the moose and types version used, to help us debug some of the moose related errors being reported
 r7058@Thesaurus (orig r7057):  ribasushi | 2009-07-16 06:28:44 +0200
 A couple of typos, and general whitespace cleanup (ick)

r7063@Thesaurus (orig r7062):  jnapiorkowski | 2009-07-16 17:03:32 +0200
increased Moose version requirements due to changes in the way type constraints get validated, which is not backwardly compatible
r7064@Thesaurus (orig r7063):  dandv | 2009-07-17 03:37:28 +0200
Minor POD grammar: it's -> its where appropriate
r7075@Thesaurus (orig r7074):  tomboh | 2009-07-20 18:20:37 +0200
Fix POD changes from r7040.
r7078@Thesaurus (orig r7077):  norbi | 2009-07-21 00:59:30 +0200

r7079@Thesaurus (orig r7078):  norbi | 2009-07-21 00:59:58 +0200
 r7232@vger:  mendel | 2009-07-21 00:58:12 +0200
 Fixed documentation and added test for the "Arbitrary SQL through a custom ResultSource" Cookbook alternate (subclassing) recipe.

r7080@Thesaurus (orig r7079):  norbi | 2009-07-21 01:05:32 +0200
 r7235@vger:  mendel | 2009-07-21 01:05:18 +0200
 Fixed 'typo' (removed a word that I left there by accident).

r7081@Thesaurus (orig r7080):  norbi | 2009-07-21 10:06:21 +0200
 r7237@vger:  mendel | 2009-07-21 10:06:05 +0200
 Fixing what my svk client screwed up.

r7082@Thesaurus (orig r7081):  caelum | 2009-07-21 16:51:55 +0200
update Storage::Replicated prereqs
r7083@Thesaurus (orig r7082):  caelum | 2009-07-21 18:16:34 +0200
show Oracle datetime_setup alter session statements in debug output
r7086@Thesaurus (orig r7085):  ribasushi | 2009-07-22 03:50:57 +0200
Lazy folks do not run the whole test suite before merging >:(
r7100@Thesaurus (orig r7097):  caelum | 2009-07-23 20:14:11 +0200
 r6092@hlagh (orig r7090):  caelum | 2009-07-23 08:24:39 -0400
 new branch for fixing the MONEY type in MSSQL
 r6093@hlagh (orig r7091):  caelum | 2009-07-23 08:34:01 -0400
 add test
 r6283@hlagh (orig r7093):  caelum | 2009-07-23 10:31:08 -0400
 fix money columns
 r6284@hlagh (orig r7094):  caelum | 2009-07-23 10:34:06 -0400
 minor change
 r6285@hlagh (orig r7095):  caelum | 2009-07-23 11:01:37 -0400
 add test for updating money value to NULL
 r6286@hlagh (orig r7096):  caelum | 2009-07-23 14:09:26 -0400
 add money type tests to dbd::sybase+mssql tests

r7129@Thesaurus (orig r7126):  caelum | 2009-07-28 02:03:47 +0200
add postgres "timestamp without time zone" support
r7143@Thesaurus (orig r7140):  caelum | 2009-07-30 14:46:04 +0200
update sqlite test schema
r7148@Thesaurus (orig r7145):  robkinyon | 2009-07-30 16:13:21 +0200
Added prefetch caveats
r7149@Thesaurus (orig r7146):  robkinyon | 2009-07-30 16:20:02 +0200
Fixed caveats
r7152@Thesaurus (orig r7149):  caelum | 2009-07-30 17:56:01 +0200
make ::Oracle::Generic load without DBD::Oracle
r7153@Thesaurus (orig r7150):  caelum | 2009-07-30 18:04:47 +0200
make sure DBD::Oracle is loaded when using constants from it
r7157@Thesaurus (orig r7154):  castaway | 2009-07-30 22:17:33 +0200
Mangled Rob's example somewhat, still needs explaining whch circs exactly cause the borken results

r7161@Thesaurus (orig r7158):  mo | 2009-07-31 12:51:20 +0200
POD fix
r7162@Thesaurus (orig r7159):  mo | 2009-07-31 12:52:42 +0200
undo that attributes merge stuff
r7169@Thesaurus (orig r7166):  castaway | 2009-08-02 12:41:25 +0200
Mention ResultSet, ResultSource and Row in synopsis

r7170@Thesaurus (orig r7167):  castaway | 2009-08-02 14:10:53 +0200
Docs: Explainations of result sources and how to find them

r7175@Thesaurus (orig r7172):  ribasushi | 2009-08-03 11:01:44 +0200
Disable Pod::Inherit makefile calls, until we get to version 0.02
r7179@Thesaurus (orig r7176):  ribasushi | 2009-08-03 11:51:42 +0200
 r6983@Thesaurus (orig r6982):  ribasushi | 2009-07-04 11:46:57 +0200
 New branch to experiment with a sanifying mysql on_connect_call
 r6984@Thesaurus (orig r6983):  ribasushi | 2009-07-04 11:49:44 +0200
 Initial set_ansi_mode code - make sure to utilize _do_query instead of dbh->do, so the result is visible in the trace
 r6987@Thesaurus (orig r6986):  ribasushi | 2009-07-04 12:40:47 +0200
 Fix POD
 r7178@Thesaurus (orig r7175):  ribasushi | 2009-08-03 11:51:15 +0200
 Wrap up set_strict_mode for mysql

r7181@Thesaurus (orig r7178):  ribasushi | 2009-08-03 12:41:32 +0200
Sanify unqualified column bindtype handling
Silence a warning when using a custom {from}

153 files changed:
Changes
Features_09
Makefile.PL
TODO
lib/DBIx/Class.pm
lib/DBIx/Class/CDBICompat.pm
lib/DBIx/Class/CDBICompat/ColumnCase.pm
lib/DBIx/Class/CDBICompat/ColumnGroups.pm
lib/DBIx/Class/CDBICompat/ColumnsAsHash.pm
lib/DBIx/Class/CDBICompat/Copy.pm
lib/DBIx/Class/CDBICompat/ImaDBI.pm
lib/DBIx/Class/CDBICompat/Iterator.pm
lib/DBIx/Class/CDBICompat/LazyLoading.pm
lib/DBIx/Class/CDBICompat/LiveObjectIndex.pm
lib/DBIx/Class/CDBICompat/Relationship.pm
lib/DBIx/Class/CDBICompat/Relationships.pm
lib/DBIx/Class/CDBICompat/Retrieve.pm
lib/DBIx/Class/CDBICompat/TempColumns.pm
lib/DBIx/Class/DB.pm
lib/DBIx/Class/Exception.pm
lib/DBIx/Class/InflateColumn/DateTime.pm
lib/DBIx/Class/InflateColumn/File.pm
lib/DBIx/Class/Manual/Cookbook.pod
lib/DBIx/Class/Manual/Example.pod
lib/DBIx/Class/Manual/FAQ.pod
lib/DBIx/Class/Manual/Intro.pod
lib/DBIx/Class/Manual/Joining.pod
lib/DBIx/Class/Ordered.pm
lib/DBIx/Class/PK.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/ResultSetColumn.pm
lib/DBIx/Class/ResultSource.pm
lib/DBIx/Class/ResultSource/View.pm
lib/DBIx/Class/ResultSourceHandle.pm
lib/DBIx/Class/ResultSourceProxy/Table.pm
lib/DBIx/Class/Row.pm
lib/DBIx/Class/SQLAHacks.pm
lib/DBIx/Class/SQLAHacks/MSSQL.pm [new file with mode: 0644]
lib/DBIx/Class/Schema.pm
lib/DBIx/Class/Schema/Versioned.pm
lib/DBIx/Class/StartupCheck.pm
lib/DBIx/Class/Storage.pm
lib/DBIx/Class/Storage/DBI.pm
lib/DBIx/Class/Storage/DBI/AmbiguousGlob.pm [new file with mode: 0644]
lib/DBIx/Class/Storage/DBI/Cursor.pm
lib/DBIx/Class/Storage/DBI/DB2.pm
lib/DBIx/Class/Storage/DBI/MSSQL.pm
lib/DBIx/Class/Storage/DBI/MultiColumnIn.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/Microsoft_SQL_Server.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 [new file with mode: 0644]
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/SQLite.pm
lib/DBIx/Class/Storage/DBI/Sybase.pm
lib/DBIx/Class/Storage/DBI/Sybase/Base.pm [new file with mode: 0644]
lib/DBIx/Class/Storage/DBI/Sybase/MSSQL.pm
lib/DBIx/Class/Storage/DBI/Sybase/Microsoft_SQL_Server.pm
lib/DBIx/Class/Storage/DBI/mysql.pm
lib/DBIx/Class/Storage/Statistics.pm
lib/DBIx/Class/UTF8Columns.pm
lib/SQL/Translator/Parser/DBIx/Class.pm
lib/SQL/Translator/Producer/DBIx/Class/File.pm
t/03podcoverage.t
t/31stats.t
t/42toplimit.t
t/46where_attribute.t
t/60core.t
t/67pager.t
t/71mysql.t
t/72pg.t
t/73oracle.t
t/746mssql.t
t/746sybase.t
t/74mssql.t
t/83cache.t
t/84serialize.t
t/85utf8.t
t/86might_have.t
t/86sqlt.t
t/88result_set_column.t
t/90join_torture.t
t/92storage_on_connect_call.t [new file with mode: 0644]
t/93storage_replication.t
t/95sql_maker.t
t/95sql_maker_quote.t
t/99dbic_sqlt_parser.t
t/bind/attribute.t
t/cdbi/02-Film.t
t/cdbi/DeepAbstractSearch/01_search.t [changed mode: 0755->0644]
t/count/count_rs.t [new file with mode: 0644]
t/count/distinct.t
t/count/joined.t
t/count/prefetch.t
t/from_subquery.t
t/inflate/datetime_oracle.t [moved from t/73oracle_inflate.t with 75% similarity]
t/inflate/datetime_pg.t
t/inflate/hri.t
t/inflate/serialize.t
t/lib/DBICTest/Schema.pm
t/lib/DBICTest/Schema/ArtistUndirectedMap.pm
t/lib/DBICTest/Schema/Artwork.pm
t/lib/DBICTest/Schema/Bookmark.pm
t/lib/DBICTest/Schema/CD.pm
t/lib/DBICTest/Schema/CustomSql.pm [new file with mode: 0644]
t/lib/DBICTest/Schema/Event.pm
t/lib/DBICTest/Schema/EventTZPg.pm
t/lib/DBICTest/Schema/Genre.pm
t/lib/DBICTest/Schema/Money.pm [new file with mode: 0644]
t/lib/DBICTest/Schema/TwoKeys.pm [changed mode: 0755->0644]
t/lib/sqlite.sql
t/multi_create/in_memory.t [moved from t/96multi_create_new.t with 100% similarity]
t/multi_create/m2m.t
t/multi_create/multilev_single_PKeqFK.t [new file with mode: 0644]
t/multi_create/standard.t [moved from t/96multi_create.t with 98% similarity]
t/multi_create/torture.t [moved from t/96multi_create_torture.t with 98% similarity]
t/prefetch/attrs_untouched.t
t/prefetch/count.t [new file with mode: 0644]
t/prefetch/double_prefetch.t [new file with mode: 0644]
t/prefetch/grouped.t [new file with mode: 0644]
t/prefetch/incomplete.t [new file with mode: 0644]
t/prefetch/multiple_hasmany.t
t/prefetch/rows_bug.t [deleted file]
t/prefetch/standard.t
t/prefetch/with_limit.t [new file with mode: 0644]
t/relationship/core.t
t/relationship/update_or_create_multi.t [new file with mode: 0644]
t/relationship/update_or_create_single.t [new file with mode: 0644]
t/zzzzzzz_perl_perf_bug.t
t/zzzzzzz_sqlite_deadlock.t [new file with mode: 0644]

diff --git a/Changes b/Changes
index 65fef40..3a07811 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,10 +1,73 @@
 Revision history for DBIx::Class
 
+        - Replication updates: Improved the replication tests so that they are
+          more reliable and accurate, and hopefully solve some cross platform
+          issues.  Bugfixes related to naming particular replicants in a
+          'force_pool' attribute.  Lots of documentation updates, including a
+          new Introduction.pod file. Fixed the way we detect transaction to 
+          make this more reliable and forward looking. Fixed some trouble with
+          the way Moose Types are used.
+        - Added new MySQL specific on_connect_call macro 'set_strict_mode'
+          (also known as make_mysql_not_suck_as_much)
+        - Added call to Pod::Inherit in Makefile.PL -
+          currently at author-time only, so we need to add the produced
+          .pod files to the MANIFEST
+
+
+0.08108 2009-07-05 23:15:00 (UTC)
+        - Fixed the has_many prefetch with limit/group deficiency -
+          it is now possible to select "top 5 commenters" while
+          prefetching all their comments
+        - New resultsed method count_rs, returns a ::ResultSetColumn
+          which in turn returns a single count value
+        - Even better support of count with limit
+        - New on_connect_call/on_disconnect_call functionality (check
+          POD of Storage::DBI)
+        - Automatic datetime handling environment/session setup for
+          Oracle via connect_call_datetime_setup()
+        - count/all on related left-joined empty resultsets now correctly
+          returns 0/()
+        - Fixed regression when both page and offset are specified on
+          a resultset
+        - Fixed HRI returning too many empty results on multilevel
+          nonexisting prefetch
+        - make_column_dirty() now overwrites the deflated value with an
+          inflated one if such exists
+        - Fixed set_$rel with where restriction deleting rows outside 
+          the restriction
+        - populate() returns the created objects or an arrayref of the
+          created objects depending on scalar vs. list context
+        - Fixed find_related on 'single' relationships - the former
+          implementation would overspecify the WHERE condition, reporting
+          no related objects when there in fact is one
+        - SQL::Translator::Parser::DBIx::Class now attaches tables to the
+          central schema object in relationship dependency order
+        - Fixed regression in set_column() preventing sourceless object
+          manipulations
+        - Fixed a bug in search_related doubling a join if the original
+          $rs already joins/prefetches the same relation
+        - Storage::DBI::connected() improvements for Oracle and Sybase
+        - Fixed prefetch+incomplete select regression introduced in
+          0.08100
+        - MSSQL limit (TOP emulation) fixes and improvements
+
+0.08107 2009-06-14 08:21:00 (UTC)
+        - Fix serialization regression introduced in 0.08103 (affects
+          Cursor::Cached)
+        - POD fixes
+        - Fixed incomplete ::Replicated debug output
+
+0.08106 2009-06-11 21:42:00 (UTC)
+        - Switched SQLite storage driver to DateTime::Format::SQLite
+          (proper timezone handling)
+        - Fix more test problems
+
+0.08105 2009-06-11 19:04:00 (UTC)
         - Update of numeric columns now properly uses != to determine
           dirtyness instead of the usual eq
         - Fixes to IC::DT tests
-        - Fixed exception when undef_if_invalid and timezone are both set on 
-          an invalid datetime column
+        - Fixed exception when undef_if_invalid and timezone are both set
+          on an invalid datetime column
 
 0.08104 2009-06-10 13:38:00 (UTC)
         - order_by now can take \[$sql, @bind] as in
index 24a6aa2..d9c17b2 100644 (file)
@@ -14,12 +14,8 @@ Syntax improvements?
  - "belongs_to" to "contains/refers/something"
 
 Using inflated objects/references as values in searches
- - Goes together with subselects above
  - should deflate then run search
 
-FilterColumn - like Inflate, only for changing scalar values
- - This seems to be vaporware atm..
-
 SQL/API feature complete?
  - UNION
  - proper join conditions!
@@ -27,17 +23,16 @@ SQL/API feature complete?
 
 Moosification - ouch
 
+Metamodel stuff - introspection
+
 Prefetch improvements
  - slow on mysql, speedup?
  - multi has_many prefetch
- - paging working with prefetch
 
 Magically "discover" needed joins/prefetches and add them
  - eg $books->search({ 'author.name' => 'Fred'}), autoadds: join => 'author'
  - also guess aliases when supplying column names that are on joined/related tables
 
-Metamodel stuff - introspection
-
 Storage API/restructure
  - call update/insert etc on the ResultSource, which then calls to storage
  - handle different storages/db-specific code better
@@ -52,4 +47,3 @@ Joining/searching weird attribute tables?
 Documentation - improvements
  - better indexing for finding of stuff in general
  - more cross-referencing of docs
-
index 3713df1..d21951b 100644 (file)
@@ -9,21 +9,12 @@ name     'DBIx-Class';
 perl_version '5.006001';
 all_from 'lib/DBIx/Class.pm';
 
-requires 'DBD::SQLite'              => 1.25;
-requires 'Data::Page'               => 2.00;
-requires 'SQL::Abstract'            => 1.56;
-requires 'SQL::Abstract::Limit'     => 0.13;
-requires 'Class::C3::Componentised' => 1.0005;
-requires 'Carp::Clan'               => 6.0;
-requires 'DBI'                      => 1.605;
-requires 'Module::Find'             => 0.06;
-requires 'Class::Inspector'         => 1.24;
-requires 'Class::Accessor::Grouped' => 0.08003;
-requires 'JSON::Any'                => 1.18;
-requires 'Scope::Guard'             => 0.03;
-requires 'Path::Class'              => 0.16;
-requires 'Sub::Name'                => 0.04;
-requires 'MRO::Compat'              => 0.09;
+
+test_requires 'Test::Builder'       => 0.33;
+test_requires 'Test::Deep'          => 0;
+test_requires 'Test::Exception'     => 0;
+test_requires 'Test::More'          => 0.82;
+test_requires 'Test::Warn'          => 0.11;
 
 # Core
 requires 'List::Util'               => 0;
@@ -33,11 +24,22 @@ requires 'Storable'                 => 0;
 # Perl 5.8.0 doesn't have utf8::is_utf8()
 requires 'Encode'                   => 0 if ($] <= 5.008000);
 
-test_requires 'Test::More'          => 0.82;
-test_requires 'Test::Builder'       => 0.33;
-test_requires 'Test::Warn'          => 0.11;
-test_requires 'Test::Exception'     => 0;
-test_requires 'Test::Deep'          => 0;
+# Dependencies (keep in alphabetical order)
+requires 'Carp::Clan'               => 6.0;
+requires 'Class::Accessor::Grouped' => 0.08003;
+requires 'Class::C3::Componentised' => 1.0005;
+requires 'Class::Inspector'         => 1.24;
+requires 'Data::Page'               => 2.00;
+requires 'DBD::SQLite'              => 1.25;
+requires 'DBI'                      => 1.605;
+requires 'JSON::Any'                => 1.18;
+requires 'MRO::Compat'              => 0.09;
+requires 'Module::Find'             => 0.06;
+requires 'Path::Class'              => 0.16;
+requires 'Scope::Guard'             => 0.03;
+requires 'SQL::Abstract'            => 1.56;
+requires 'SQL::Abstract::Limit'     => 0.13;
+requires 'Sub::Name'                => 0.04;
 
 recommends 'SQL::Translator'        => 0.09004;
 
@@ -58,8 +60,9 @@ resources 'MailingList' => 'http://lists.scsys.co.uk/cgi-bin/mailman/listinfo/db
 # re-build README and require extra modules for testing if we're in a checkout
 
 my %force_requires_if_author = (
+#  'Module::Install::Pod::Inherit' => 0.01,
   'Test::Pod::Coverage'       => 1.04,
-  'SQL::Translator'           => 0.09004,
+  'SQL::Translator'           => 0.09007,
 
   # CDBI-compat related
   'DBIx::ContextualFetch'     => 0,
@@ -73,30 +76,41 @@ my %force_requires_if_author = (
   'Test::Memory::Cycle'       => 0,
   'Devel::Cycle'              => 1.10,
 
-  # t/inflate/datetime*.t
-  # t/72.pg
   # t/36datetime.t
   # t/60core.t
   'DateTime::Format::SQLite'  => 0,
-  'DateTime::Format::MySQL'   => 0,
-  'DateTime::Format::Pg'      => 0,
 
   # t/96_is_deteministic_value.t
-  'DateTime::Format::Strptime' => 0,
+  'DateTime::Format::Strptime'=> 0,
 
-  # t/72pg.t
+  # t/93storage_replication.t
+  'Moose',                    => 0.87,
+  'MooseX::AttributeHelpers'  => 0.21,
+  'MooseX::Types',            => 0.16,
+  'namespace::clean'          => 0.11,
+  'Hash::Merge',              => 0.11,
+
+  # database-dependent reqs
+  #
   $ENV{DBICTEST_PG_DSN}
-    ? ('Sys::SigAction'=> 0)
-    : ()
+    ? (
+      'Sys::SigAction' => 0,
+      'DBD::Pg' => 2.009002,
+      'DateTime::Format::Pg' => 0,
+    ) : ()
   ,
 
-  # t/93storage_replication.t
-  'Moose',                        => 0.77,
-  'MooseX::AttributeHelpers'      => 0.12,
-  'MooseX::Types',                => 0.10,
-  'namespace::clean'              => 0.11,
-  'Hash::Merge',                  => 0.11,
+  $ENV{DBICTEST_MYSQL_DSN}
+    ? (
+      'DateTime::Format::MySQL' => 0,
+    ) : ()
+  ,
 
+  $ENV{DBICTEST_ORACLE_DSN}
+    ? (
+      'DateTime::Format::Oracle' => 0,
+    ) : ()
+  ,
 );
 
 if ($Module::Install::AUTHOR) {
@@ -111,7 +125,7 @@ if ($Module::Install::AUTHOR) {
 
 EOW
 
-  foreach my $module (keys %force_requires_if_author) {
+  foreach my $module (sort keys %force_requires_if_author) {
     build_requires ($module => $force_requires_if_author{$module});
   }
 
@@ -122,6 +136,9 @@ EOW
     print "Removing MANIFEST\n";
     unlink 'MANIFEST';
   }
+
+#  eval { require Module::Install::Pod::Inherit };
+#  PodInherit() if !$@;
 }
 
 auto_install();
diff --git a/TODO b/TODO
index 531eb73..9712a22 100644 (file)
--- a/TODO
+++ b/TODO
@@ -3,12 +3,8 @@
   - ResultSource objects caching ->resultset causes interesting problems
   - find why XSUB dumper kills schema in Catalyst (may be Pg only?)
 
-2006-04-11 by castaway
- - docs of copy() should say that is_auto_increment is essential for auto_incrementing keys
-
 2006-03-25 by mst
   - find a way to un-wantarray search without breaking compat
-  - audit logging component
   - delay relationship setup if done via ->load_classes
   - double-sided relationships
   - make short form of class specifier in relationships work
@@ -21,9 +17,6 @@
    We should still support the old inflate/deflate syntax, but this new 
    way should be recommended. 
 
-2006-02-07 by castaway
- - Extract DBIC::SQL::Abstract into a separate module for CPAN
-
 2006-03-18 by bluefeet
  - Support table locking.
 
        if you haven't specified one of the others
 
 2008-10-30 by ribasushi
- Leftovers for next dev-release
   - Rewrite the test suite to rely on $schema->deploy, allowing for seamless
     testing of various RDBMS using the same tests
-  - Proper support of default create (i.e. create({}) ), with proper workarounds
-    for different Storage's
   - Automatically infer quote_char/name_sep from $schema->storage
-  - Finally incorporate View support (needs real tests)
   - Fix and properly test chained search attribute merging
-
-2008-11-07 by ribasushi
-  - Be loud when a relationship resolution fails because we did not select/as
-    a neccessary pk
   - Recursive update() (all code seems to be already available)
-  - $rs->populate changes its syntax depending on wantarray context (BAD)
-    Also the interface differs from $schema->populate (not so good)
index 99aa8f3..34ab70d 100644 (file)
@@ -3,11 +3,12 @@ package DBIx::Class;
 use strict;
 use warnings;
 
+use MRO::Compat;
+
 use vars qw($VERSION);
 use base qw/DBIx::Class::Componentised Class::Accessor::Grouped/;
 use DBIx::Class::StartupCheck;
 
-
 sub mk_classdata {
   shift->mk_classaccessor(@_);
 }
@@ -24,7 +25,7 @@ sub component_base_class { 'DBIx::Class' }
 # i.e. first release of 0.XX *must* be 0.XX000. This avoids fBSD ports
 # brain damage and presumably various other packaging systems too
 
-$VERSION = '0.08104';
+$VERSION = '0.08108';
 
 $VERSION = eval $VERSION; # numify for warning-free dev releases
 
@@ -72,9 +73,11 @@ Create a schema class called MyDB/Schema.pm:
 
   1;
 
-Create a table class to represent artists, who have many CDs, in
+Create a result class to represent artists, who have many CDs, in
 MyDB/Schema/Result/Artist.pm:
 
+See L<DBIx::Class::ResultSource> for docs on defining result classes.
+
   package MyDB::Schema::Result::Artist;
   use base qw/DBIx::Class/;
 
@@ -86,7 +89,7 @@ MyDB/Schema/Result/Artist.pm:
 
   1;
 
-A table class to represent a CD, which belongs to an artist, in
+A result class to represent a CD, which belongs to an artist, in
 MyDB/Schema/Result/CD.pm:
 
   package MyDB::Schema::Result::CD;
@@ -108,9 +111,17 @@ Then you can use these classes in your application's code:
 
   # Query for all artists and put them in an array,
   # or retrieve them as a result set object.
+  # $schema->resultset returns a DBIx::Class::ResultSet
   my @all_artists = $schema->resultset('Artist')->all;
   my $all_artists_rs = $schema->resultset('Artist');
 
+  # Output all artists names
+  # $artist here is a DBIx::Class::Row, which has accessors 
+  # for all its columns. Rows are also subclasses of your Result class.
+  foreach $artist (@artists) {
+    print $artist->name, "\n";
+  }
+
   # Create a result set to search for artists.
   # This does not query the DB.
   my $johns_rs = $schema->resultset('Artist')->search(
index ec063a9..835adfe 100644 (file)
@@ -11,7 +11,7 @@ my @Extra_Modules = qw(
     DBIx::ContextualFetch
     Clone
 );
-                
+
 my @didnt_load;
 for my $module (@Extra_Modules) {
     push @didnt_load, $module unless eval qq{require $module};
@@ -149,13 +149,13 @@ Relationships between tables (has_a, has_many...) must be delcared after all tab
 
     package Foo;
     use base qw(Class::DBI);
-    
+
     Foo->table("foo");
     Foo->columns( All => qw(this that bar) );
 
     package Bar;
     use base qw(Class::DBI);
-    
+
     Bar->table("bar");
     Bar->columns( All => qw(up down) );
 
index d6db87b..eb4b0c0 100644 (file)
@@ -16,10 +16,10 @@ sub add_columns {
 
 sub has_a {
     my($self, $col, @rest) = @_;
-    
+
     $self->_declare_has_a(lc $col, @rest);
     $self->_mk_inflated_column_accessor($col);
-    
+
     return 1;
 }
 
index cbc1124..3a026b2 100644 (file)
@@ -73,7 +73,7 @@ sub _register_column_group {
 
   sub _has_custom_accessor {
     my($class, $name) = @_;
-    
+
     no strict 'refs';
     my $existing_accessor = *{$class .'::'. $name}{CODE};
     return $existing_accessor && !$our_accessors{$existing_accessor};
@@ -90,7 +90,7 @@ sub _register_column_group {
       my $fullname = join '::', $class, $name;
       *$fullname = Sub::Name::subname $fullname, $accessor;
     }
-    
+
     $our_accessors{$accessor}++;
 
     return 1;
@@ -120,7 +120,7 @@ sub _mk_group_accessors {
     # warn "  $field $alias\n";
     {
       no strict 'refs';
-      
+
       $class->_deploy_accessor($name,  $accessor);
       $class->_deploy_accessor($alias, $accessor);
     }
index 7b81f09..291db64 100644 (file)
@@ -39,16 +39,16 @@ sub inflate_result {
     my $class = shift;
 
     my $new = $class->next::method(@_);
-    
+
     $new->_make_columns_as_hash;
-    
+
     return $new;
 }
 
 
 sub _make_columns_as_hash {
     my $self = shift;
-    
+
     for my $col ($self->columns) {
         if( exists $self->{$col} ) {
             warn "Skipping mapping $col to a hash key because it exists";
index ed42d95..ffd5381 100644 (file)
@@ -25,7 +25,7 @@ Emulates C<<Class::DBI->copy($new_id)>>.
 sub copy {
     my($self, $arg) = @_;
     return $self->next::method($arg) if ref $arg;
-    
+
     my @primary_columns = $self->primary_columns;
     croak("Need hash-ref to edit copied column values")
         if @primary_columns > 1;
index db844e8..49fc1e0 100644 (file)
@@ -59,7 +59,7 @@ __PACKAGE__->mk_classdata('_transform_sql_handlers' =>
             $rel_obj->{cond}, $to, $from) );
         return $join;
       }
-        
+
   } );
 
 sub db_Main {
@@ -115,7 +115,7 @@ sub sth_to_objects {
 
 sub transform_sql {
   my ($class, $sql, @args) = @_;
-  
+
   my $tclass = $class->sql_transformer_class;
   $class->ensure_class_loaded($tclass);
   my $t = $tclass->new($class, $sql, @args);
index 80e788c..3e93154 100644 (file)
@@ -25,7 +25,7 @@ The CDBI iterator returns true if there were any results, false otherwise.  The
 
 sub _init_result_source_instance {
   my $class = shift;
-  
+
   my $table = $class->next::method(@_);
   $table->resultset_class("DBIx::Class::CDBICompat::Iterator::ResultSet");
 
index e07579a..0817ef2 100644 (file)
@@ -16,12 +16,12 @@ sub resultset_instance {
 # request in case the database modifies the new value (say, via a trigger)
 sub update {
     my $self = shift;
-    
+
     my @dirty_columns = keys %{$self->{_dirty_columns}};
-    
+
     my $ret = $self->next::method(@_);
     $self->_clear_column_data(@dirty_columns);
-    
+
     return $ret;
 }
 
@@ -30,12 +30,12 @@ sub update {
 sub create {
     my $class = shift;
     my($data) = @_;
-    
+
     my @columns = keys %$data;
-    
+
     my $obj = $class->next::method(@_);
     return $obj unless defined $obj;
-    
+
     my %primary_cols = map { $_ => 1 } $class->primary_columns;
     my @data_cols = grep !$primary_cols{$_}, @columns;
     $obj->_clear_column_data(@data_cols);
@@ -46,7 +46,7 @@ sub create {
 
 sub _clear_column_data {
     my $self = shift;
-    
+
     delete $self->{_column_data}{$_}     for @_;
     delete $self->{_inflated_column}{$_} for @_;
 }
@@ -71,7 +71,7 @@ sub copy {
   for my $col ($self->primary_columns) {
     $changes->{$col} = undef unless exists $changes->{$col};
   }
-  
+
   return $self->next::method($changes);
 }
 
index a461a13..f05eff7 100644 (file)
@@ -20,9 +20,9 @@ __PACKAGE__->mk_classdata('__nocache' => 0);
 
 sub nocache {
     my $class = shift;
-    
+
     return $class->__nocache(@_) if @_;
-    
+
     return 1 if $Class::DBI::Weaken_Is_Available == 0;
     return $class->__nocache;
 }
@@ -74,9 +74,9 @@ sub insert {
 sub inflate_result {
   my ($class, @rest) = @_;
   my $new = $class->next::method(@rest);
-  
+
   return $new if $new->nocache;
-  
+
   if (my $key = $new->ID) {
     #warn "Key $key";
     my $live = $class->live_object_index;
index 5d71924..b0c10fa 100644 (file)
@@ -25,7 +25,7 @@ my %method2key = (
 
 sub new {
     my($class, $args) = @_;
-    
+
     return bless $args, $class;
 }
 
@@ -34,7 +34,7 @@ for my $method (keys %method2key) {
     my $code = sub {
         $_[0]->{$key};
     };
-    
+
     no strict 'refs';
     *{$method} = Sub::Name::subname $method, $code;
 }
index 7572870..58b29e0 100644 (file)
@@ -24,10 +24,10 @@ Emulate C<has_a>, C<has_many>, C<might_have> and C<meta_info>.
 
 sub has_a {
     my($self, $col, @rest) = @_;
-    
+
     $self->_declare_has_a($col, @rest);
     $self->_mk_inflated_column_accessor($col);
-    
+
     return 1;
 }
 
@@ -37,7 +37,7 @@ sub _declare_has_a {
   $self->throw_exception( "No such column ${col}" )
    unless $self->has_column($col);
   $self->ensure_class_loaded($f_class);
-  
+
   my $rel_info;
 
   if ($args{'inflate'} || $args{'deflate'}) { # Non-database has_a
@@ -50,7 +50,7 @@ sub _declare_has_a {
       $args{'deflate'} = sub { shift->$meth; };
     }
     $self->inflate_column($col, \%args);
-    
+
     $rel_info = {
         class => $f_class
     };
@@ -59,9 +59,9 @@ sub _declare_has_a {
     $self->belongs_to($col, $f_class);
     $rel_info = $self->result_source_instance->relationship_info($col);
   }
-  
+
   $rel_info->{args} = \%args;
-  
+
   $self->_extend_meta(
     has_a => $col,
     $rel_info
@@ -72,7 +72,7 @@ sub _declare_has_a {
 
 sub _mk_inflated_column_accessor {
     my($class, $col) = @_;
-    
+
     return $class->mk_group_accessors('inflated_column' => $col);
 }
 
@@ -137,7 +137,7 @@ sub has_many {
 
 sub might_have {
   my ($class, $rel, $f_class, @columns) = @_;
-  
+
   my $ret;
   if (ref $columns[0] || !defined $columns[0]) {
     $ret = $class->next::method($rel, $f_class, @columns);
@@ -153,7 +153,7 @@ sub might_have {
     might_have => $rel,
     $rel_info
   );
-  
+
   return $ret;
 }
 
index e701cfc..34be5f3 100644 (file)
@@ -74,7 +74,7 @@ sub construct {
     my $class = shift;
     my $obj = $class->resultset_instance->new_result(@_);
     $obj->in_storage(1);
-    
+
     return $obj;
 }
 
index f5b60d4..428719e 100644 (file)
@@ -11,7 +11,7 @@ __PACKAGE__->mk_classdata('_temp_columns' => { });
 
 sub _add_column_group {
   my ($class, $group, @cols) = @_;
-  
+
   return $class->next::method($group, @cols) unless $group eq 'TEMP';
 
   my %new_cols = map { $_ => 1 } @cols;
@@ -61,11 +61,11 @@ sub find_column {
 
 sub set {
   my($self, %data) = @_;
-  
+
   my $temp_data = $self->_extract_temp_data(\%data);
-  
+
   $self->set_temp($_, $temp_data->{$_}) for keys %$temp_data;
-  
+
   return $self->next::method(%data);
 }
 
index 57381d0..3b2f7cc 100644 (file)
@@ -174,7 +174,7 @@ sub _maybe_attach_source_to_schema {
 sub result_source_instance {
   my $class = shift;
   $class = ref $class || $class;
-  
+
   if (@_) {
     my $source = $_[0];
     $class->_result_source_instance([$source, $class]);
@@ -186,7 +186,7 @@ sub result_source_instance {
   return unless Scalar::Util::blessed($source);
 
   if ($result_class ne $class) {  # new class
-    # Give this new class it's own source and register it.
+    # Give this new class its own source and register it.
     $source = $source->new({ 
         %$source, 
         source_name  => $class,
index 85c08b2..e8e9ff7 100644 (file)
@@ -61,7 +61,7 @@ sub throw {
     else {
         $msg = Carp::longmess($msg);
     }
-    
+
     my $self = { msg => $msg };
     bless $self => $class;
 
index 5646768..7e50807 100644 (file)
@@ -40,17 +40,26 @@ use inflate_datetime or inflate_date:
   __PACKAGE__->add_columns(
     starts_when => { data_type => 'varchar', inflate_datetime => 1 }
   );
-  
+
   __PACKAGE__->add_columns(
     starts_when => { data_type => 'varchar', inflate_date => 1 }
   );
 
 It's also possible to explicitly skip inflation:
-  
+
   __PACKAGE__->add_columns(
     starts_when => { data_type => 'datetime', inflate_datetime => 0 }
   );
 
+NOTE: Don't rely on C<InflateColumn::DateTime> to parse date strings for you.
+The column is set directly for any non-references and C<InflateColumn::DateTime>
+is completely bypassed.  Instead, use an input parser to create a DateTime
+object. For instance, if your user input comes as a 'YYYY-MM-DD' string, you can
+use C<DateTime::Format::ISO8601> thusly:
+
+  use DateTime::Format::ISO8601;
+  my $dt = DateTime::Format::ISO8601->parse_datetime('YYYY-MM-DD');
+
 =head1 DESCRIPTION
 
 This module figures out the type of DateTime::Format::* class to 
@@ -77,7 +86,7 @@ directly called by end users.
 In the case of an invalid date, L<DateTime> will throw an exception.  To
 bypass these exceptions and just have the inflation return undef, use
 the C<datetime_undef_if_invalid> option in the column info:
-  
+
     "broken_date",
     {
         data_type => "datetime",
@@ -110,6 +119,12 @@ sub register_column {
     if ($type eq "timestamp with time zone" || $type eq "timestamptz") {
       $type = "timestamp";
       $info->{_ic_dt_method} ||= "timestamp_with_timezone";
+    } elsif ($type eq "timestamp without time zone") {
+      $type = "timestamp";
+      $info->{_ic_dt_method} ||= "timestamp_without_timezone";
+    } elsif ($type eq "smalldatetime") {
+      $type = "datetime";
+      $info->{_ic_dt_method} ||= "datetime";
     }
   }
 
@@ -126,7 +141,7 @@ sub register_column {
          "please put it directly into the '$column' column definition.";
     $locale = $info->{extra}{locale};
   }
-  
+
   $locale   = $info->{locale}   if defined $info->{locale};
   $timezone = $info->{timezone} if defined $info->{timezone};
 
@@ -211,7 +226,7 @@ __END__
 
 =head1 USAGE NOTES
 
-If you have a datetime column with the C<timezone> extra setting, and subsenquently 
+If you have a datetime column with an associated C<timezone>, and subsequently
 create/update this column with a DateTime object in the L<DateTime::TimeZone::Floating>
 timezone, you will get a warning (as there is a very good chance this will not have the
 result you expect). For example:
index 78e316b..1901187 100644 (file)
@@ -58,7 +58,7 @@ sub delete {
 
 sub insert {
     my $self = shift;
+
     # cache our file columns so we can write them to the fs
     # -after- we have a PK
     my %file_column;
@@ -114,7 +114,7 @@ DBIx::Class::InflateColumn::File -  map files from the Database to the filesyste
 In your L<DBIx::Class> table class:
 
     __PACKAGE__->load_components( "PK::Auto", "InflateColumn::File", "Core" );
-    
+
     # define your columns
     __PACKAGE__->add_columns(
         "id",
@@ -136,7 +136,7 @@ In your L<DBIx::Class> table class:
             size                => 255,
         },
     );
-    
+
 
 In your L<Catalyst::Controller> class:
 
@@ -152,15 +152,15 @@ name as name.
         body => '....'
     });
     $c->stash->{entry}=$entry;
-    
+
 
 And Place the following in your TT template
-    
+
     Article Subject: [% entry.subject %]
     Uploaded File: 
     <a href="/static/files/[% entry.id %]/[% entry.filename.filename %]">File</a>
     Body: [% entry.body %]
-    
+
 The file will be stored on the filesystem for later retrieval.  Calling delete
 on your resultset will delete the file from the filesystem.  Retrevial of the
 record automatically inflates the column back to the set hash with the
index 574a8e1..050a433 100644 (file)
@@ -1,4 +1,4 @@
-=head1 NAME 
+=head1 NAME
 
 DBIx::Class::Manual::Cookbook - Miscellaneous recipes
 
@@ -62,12 +62,12 @@ L<SQL::Abstract/WHERE CLAUSES>.
 Sometimes you need only the first "top" row of a resultset. While this can be
 easily done with L<< $rs->first|DBIx::Class::ResultSet/first >>, it is suboptimal,
 as a full blown cursor for the resultset will be created and then immediately
-destroyed after fetching the first row object. 
+destroyed after fetching the first row object.
 L<< $rs->single|DBIx::Class::ResultSet/single >> is
 designed specifically for this case - it will grab the first returned result
-without even instantiating a cursor. 
+without even instantiating a cursor.
 
-Before replacing all your calls to C<first()> with C<single()> please observe the 
+Before replacing all your calls to C<first()> with C<single()> please observe the
 following CAVEATS:
 
 =over
@@ -96,62 +96,61 @@ you can silence the warning by explicitly limiting the resultset size:
 
 Sometimes you have to run arbitrary SQL because your query is too complex
 (e.g. it contains Unions, Sub-Selects, Stored Procedures, etc.) or has to
-be optimized for your database in a special way, but you still want to 
-get the results as a L<DBIx::Class::ResultSet>. 
-The recommended way to accomplish this is by defining a separate ResultSource 
-for your query. You can then inject complete SQL statements using a scalar 
+be optimized for your database in a special way, but you still want to
+get the results as a L<DBIx::Class::ResultSet>.
+The recommended way to accomplish this is by defining a separate ResultSource
+for your query. You can then inject complete SQL statements using a scalar
 reference (this is a feature of L<SQL::Abstract>).
 
 Say you want to run a complex custom query on your user data, here's what
 you have to add to your User class:
 
   package My::Schema::Result::User;
-  
+
   use base qw/DBIx::Class/;
-  
+
   # ->load_components, ->table, ->add_columns, etc.
 
   # Make a new ResultSource based on the User class
   my $source = __PACKAGE__->result_source_instance();
   my $new_source = $source->new( $source );
   $new_source->source_name( 'UserFriendsComplex' );
-  
+
   # Hand in your query as a scalar reference
   # It will be added as a sub-select after FROM,
   # so pay attention to the surrounding brackets!
   $new_source->name( \<<SQL );
-  ( SELECT u.* FROM user u 
-  INNER JOIN user_friends f ON u.id = f.user_id 
+  ( SELECT u.* FROM user u
+  INNER JOIN user_friends f ON u.id = f.user_id
   WHERE f.friend_user_id = ?
-  UNION 
-  SELECT u.* FROM user u 
-  INNER JOIN user_friends f ON u.id = f.friend_user_id 
+  UNION
+  SELECT u.* FROM user u
+  INNER JOIN user_friends f ON u.id = f.friend_user_id
   WHERE f.user_id = ? )
-  SQL 
+  SQL
 
   # Finally, register your new ResultSource with your Schema
   My::Schema->register_extra_source( 'UserFriendsComplex' => $new_source );
 
 Next, you can execute your complex query using bind parameters like this:
 
-  my $friends = [ $schema->resultset( 'UserFriendsComplex' )->search( {}, 
+  my $friends = [ $schema->resultset( 'UserFriendsComplex' )->search( {},
     {
       bind  => [ 12345, 12345 ]
     }
   ) ];
-  
+
 ... and you'll get back a perfect L<DBIx::Class::ResultSet> (except, of course,
 that you cannot modify the rows it contains, ie. cannot call L</update>,
 L</delete>, ...  on it).
 
 If you prefer to have the definitions of these custom ResultSources in separate
-files (instead of stuffing all of them into the same resultset class), you can
-achieve the same with subclassing the resultset class and defining the
-ResultSource there:
+files (instead of stuffing all of them into the same ResultSource class), you
+can achieve the same with subclassing the ResultSource class and defining the
+new ResultSource there:
 
   package My::Schema::Result::UserFriendsComplex;
 
-  use My::Schema::Result::User;
   use base qw/My::Schema::Result::User/;
 
   __PACKAGE__->table('dummy');  # currently must be called before anything else
@@ -159,7 +158,7 @@ ResultSource there:
   # Hand in your query as a scalar reference
   # It will be added as a sub-select after FROM,
   # so pay attention to the surrounding brackets!
-  __PACKAGE__->name( \<<SQL );
+  __PACKAGE__->result_source_instance->name( \<<SQL );
   ( SELECT u.* FROM user u
   INNER JOIN user_friends f ON u.id = f.user_id
   WHERE f.friend_user_id = ?
@@ -169,6 +168,8 @@ ResultSource there:
   WHERE f.user_id = ? )
   SQL
 
+  1;
+
 TIMTOWDI.
 
 =head2 Using specific columns
@@ -231,7 +232,7 @@ any of your aliases using either of these:
 
   # Define accessor manually:
   sub name_length { shift->get_column('name_length'); }
-    
+
   # Or use DBIx::Class::AccessorGroup:
   __PACKAGE__->mk_group_accessors('column' => 'name_length');
 
@@ -240,23 +241,23 @@ any of your aliases using either of these:
   my $rs = $schema->resultset('Artist')->search(
     {},
     {
-      columns => [ qw/artistid name rank/ ],
+      columns => [ qw/artist_id name rank/ ],
       distinct => 1
-    } 
+    }
   );
 
   my $rs = $schema->resultset('Artist')->search(
     {},
     {
-      columns => [ qw/artistid name rank/ ],
-      group_by => [ qw/artistid name rank/ ],
+      columns => [ qw/artist_id name rank/ ],
+      group_by => [ qw/artist_id name rank/ ],
     }
   );
 
   # Equivalent SQL:
-  # SELECT me.artistid, me.name, me.rank
+  # SELECT me.artist_id, me.name, me.rank
   # FROM artist me
-  # GROUP BY artistid, name, rank
+  # GROUP BY artist_id, name, rank
 
 =head2 SELECT COUNT(DISTINCT colname)
 
@@ -279,7 +280,7 @@ any of your aliases using either of these:
   my $count = $rs->count;
 
   # Equivalent SQL:
-  # SELECT COUNT( DISTINCT( me.name ) ) FROM artist me 
+  # SELECT COUNT( * ) FROM (SELECT me.name FROM artist me GROUP BY me.name) count_subq:
 
 =head2 Grouping results
 
@@ -336,7 +337,7 @@ from, select, and +select attributes.
   my $rs = $cdrs->search({
     year => {
       '=' => $cdrs->search(
-        { artistid => { '=' => \'me.artistid' } },
+        { artist_id => { '=' => \'me.artist_id' } },
         { alias => 'inner' }
       )->get_column('year')->max_rs->as_query,
     },
@@ -349,7 +350,7 @@ That creates the following SQL:
    WHERE year = (
       SELECT MAX(inner.year)
         FROM cd inner
-       WHERE artistid = me.artistid
+       WHERE artist_id = me.artist_id
       )
 
 =head3 EXPERIMENTAL
@@ -359,7 +360,7 @@ Please note that subqueries are considered an experimental feature.
 =head2 Predefined searches
 
 You can write your own L<DBIx::Class::ResultSet> class by inheriting from it
-and define often used searches as methods:
+and defining often used searches as methods:
 
   package My::DBIC::ResultSet::CD;
   use strict;
@@ -429,15 +430,20 @@ C<bind> attributes:
 =head2 Using joins and prefetch
 
 You can use the C<join> attribute to allow searching on, or sorting your
-results by, one or more columns in a related table. To return all CDs matching
-a particular artist name:
+results by, one or more columns in a related table.
+
+This requires that you have defined the L<DBIx::Class::Relationship>. For example :
+
+  My::Schema::CD->has_many( artists => 'My::Schema::Artist', 'artist_id');
+
+To return all CDs matching a particular artist name, you specify the name of the relationship ('artists'):
 
   my $rs = $schema->resultset('CD')->search(
     {
-      'artist.name' => 'Bob Marley'    
+      'artists.name' => 'Bob Marley'
     },
     {
-      join => 'artist', # join the artist table
+      join => 'artists', # join the artist table
     }
   );
 
@@ -446,16 +452,19 @@ a particular artist name:
   # JOIN artist ON cd.artist = artist.id
   # WHERE artist.name = 'Bob Marley'
 
+In that example both the join, and the condition use the relationship name rather than the table name
+(see L<DBIx::Class::Manual::Joining> for more details on aliasing ).
+
 If required, you can now sort on any column in the related tables by including
-it in your C<order_by> attribute:
+it in your C<order_by> attribute, (again using the aliased relation name rather than table name) :
 
   my $rs = $schema->resultset('CD')->search(
     {
-      'artist.name' => 'Bob Marley'
+      'artists.name' => 'Bob Marley'
     },
     {
-      join     => 'artist',
-      order_by => [qw/ artist.name /]
+      join     => 'artists',
+      order_by => [qw/ artists.name /]
     }
   );
 
@@ -492,12 +501,12 @@ This allows you to fetch results from related tables in advance:
 
   my $rs = $schema->resultset('CD')->search(
     {
-      'artist.name' => 'Bob Marley'
+      'artists.name' => 'Bob Marley'
     },
     {
-      join     => 'artist',
-      order_by => [qw/ artist.name /],
-      prefetch => 'artist' # return artist data too!
+      join     => 'artists',
+      order_by => [qw/ artists.name /],
+      prefetch => 'artists' # return artist data too!
     }
   );
 
@@ -665,7 +674,7 @@ It is possible to get a Schema object from a row object like so:
 
   my $schema = $cd->result_source->schema;
   # use the schema as normal:
-  my $artist_rs = $schema->resultset('Artist'); 
+  my $artist_rs = $schema->resultset('Artist');
 
 This can be useful when you don't want to pass around a Schema object to every
 method.
@@ -685,7 +694,7 @@ not work, but then you already know the value of the last primary key anyway.
 
 =head2 Stringification
 
-Employ the standard stringification technique by using the C<overload>
+Employ the standard stringification technique by using the L<overload>
 module.
 
 To make an object stringify itself as a single column, use something
@@ -733,16 +742,16 @@ Just use C<find_or_new> instead, then check C<in_storage>:
     # do whatever else you wanted if it was a new row
   }
 
-=head2 Static sub-classing DBIx::Class result classes 
+=head2 Static sub-classing DBIx::Class result classes
 
 AKA adding additional relationships/methods/etc. to a model for a
 specific usage of the (shared) model.
 
-B<Schema definition> 
-    package My::App::Schema; 
-     
-    use base DBIx::Class::Schema; 
+B<Schema definition>
+
+    package My::App::Schema;
+
+    use base DBIx::Class::Schema;
 
     # load subclassed classes from My::App::Schema::Result/ResultSet
     __PACKAGE__->load_namespaces;
@@ -755,35 +764,35 @@ B<Schema definition>
         /]});
 
     1;
-B<Result-Subclass definition> 
+
+B<Result-Subclass definition>
+
     package My::App::Schema::Result::Baz;
-     
-    use strict; 
-    use warnings; 
-    use base My::Shared::Model::Result::Baz; 
-    
+
+    use strict;
+    use warnings;
+    use base My::Shared::Model::Result::Baz;
+
     # WARNING: Make sure you call table() again in your subclass,
     # otherwise DBIx::Class::ResultSourceProxy::Table will not be called
     # and the class name is not correctly registered as a source
-    __PACKAGE__->table('baz'); 
-     
-    sub additional_method { 
-        return "I'm an additional method only needed by this app"; 
+    __PACKAGE__->table('baz');
+
+    sub additional_method {
+        return "I'm an additional method only needed by this app";
     }
 
     1;
-     
-=head2 Dynamic Sub-classing DBIx::Class proxy classes 
+
+=head2 Dynamic Sub-classing DBIx::Class proxy classes
 
 AKA multi-class object inflation from one table
+
 L<DBIx::Class> classes are proxy classes, therefore some different
 techniques need to be employed for more than basic subclassing.  In
 this example we have a single user table that carries a boolean bit
 for admin.  We would like like to give the admin users
-objects(L<DBIx::Class::Row>) the same methods as a regular user but
+objects (L<DBIx::Class::Row>) the same methods as a regular user but
 also special admin only methods.  It doesn't make sense to create two
 seperate proxy-class files for this.  We would be copying all the user
 methods into the Admin class.  There is a cleaner way to accomplish
@@ -795,125 +804,128 @@ L<DBIx::Class::ResultSet> when inflating a result from storage.  So we
 grab the object being returned, inspect the values we are looking for,
 bless it if it's an admin object, and then return it.  See the example
 below:
-B<Schema Definition> 
-    package My::Schema; 
-     
-    use base qw/DBIx::Class::Schema/; 
+
+B<Schema Definition>
+
+    package My::Schema;
+
+    use base qw/DBIx::Class::Schema/;
+
     __PACKAGE__->load_namespaces;
 
     1;
-B<Proxy-Class definitions> 
-    package My::Schema::Result::User; 
-     
-    use strict; 
-    use warnings; 
-    use base qw/DBIx::Class/; 
-     
-    ### Defined what our admin class is for ensure_class_loaded 
-    my $admin_class = __PACKAGE__ . '::Admin'; 
-     
-    __PACKAGE__->load_components(qw/Core/); 
-     
-    __PACKAGE__->table('users'); 
-     
-    __PACKAGE__->add_columns(qw/user_id   email    password  
-                                firstname lastname active 
-                                admin/); 
-     
-    __PACKAGE__->set_primary_key('user_id'); 
-     
-    sub inflate_result { 
-        my $self = shift;  
-        my $ret = $self->next::method(@_); 
-        if( $ret->admin ) {### If this is an admin rebless for extra functions  
-            $self->ensure_class_loaded( $admin_class ); 
-            bless $ret, $admin_class; 
-        } 
-        return $ret; 
-    } 
-     
-    sub hello { 
-        print "I am a regular user.\n"; 
-        return ; 
-    } 
-    
+
+
+B<Proxy-Class definitions>
+
+    package My::Schema::Result::User;
+
+    use strict;
+    use warnings;
+    use base qw/DBIx::Class/;
+
+    ### Define what our admin class is, for ensure_class_loaded()
+    my $admin_class = __PACKAGE__ . '::Admin';
+
+    __PACKAGE__->load_components(qw/Core/);
+
+    __PACKAGE__->table('users');
+
+    __PACKAGE__->add_columns(qw/user_id   email    password
+                                firstname lastname active
+                                admin/);
+
+    __PACKAGE__->set_primary_key('user_id');
+
+    sub inflate_result {
+        my $self = shift;
+        my $ret = $self->next::method(@_);
+        if( $ret->admin ) {### If this is an admin, rebless for extra functions
+            $self->ensure_class_loaded( $admin_class );
+            bless $ret, $admin_class;
+        }
+        return $ret;
+    }
+
+    sub hello {
+        print "I am a regular user.\n";
+        return ;
+    }
+
     1;
 
-     
-    package My::Schema::Result::User::Admin; 
-     
-    use strict; 
-    use warnings; 
-    use base qw/My::Schema::Result::User/; 
-     
-    sub hello 
-    { 
-        print "I am an admin.\n"; 
-        return; 
-    } 
-     
-    sub do_admin_stuff 
-    { 
-        print "I am doing admin stuff\n"; 
-        return ; 
+
+    package My::Schema::Result::User::Admin;
+
+    use strict;
+    use warnings;
+    use base qw/My::Schema::Result::User/;
+
+    # This line is important
+    __PACKAGE__->table('users');
+
+    sub hello
+    {
+        print "I am an admin.\n";
+        return;
+    }
+
+    sub do_admin_stuff
+    {
+        print "I am doing admin stuff\n";
+        return ;
     }
 
     1;
-B<Test File> test.pl 
-    use warnings; 
-    use strict; 
-    use My::Schema; 
-     
-    my $user_data = { email    => 'someguy@place.com',  
-                      password => 'pass1',  
-                      admin    => 0 }; 
-                           
-    my $admin_data = { email    => 'someadmin@adminplace.com',  
-                       password => 'pass2',  
-                       admin    => 1 }; 
-                           
-    my $schema = My::Schema->connection('dbi:Pg:dbname=test'); 
-     
-    $schema->resultset('User')->create( $user_data ); 
-    $schema->resultset('User')->create( $admin_data ); 
-     
-    ### Now we search for them 
-    my $user = $schema->resultset('User')->single( $user_data ); 
-    my $admin = $schema->resultset('User')->single( $admin_data ); 
-     
-    print ref $user, "\n"; 
-    print ref $admin, "\n"; 
-     
-    print $user->password , "\n"; # pass1 
-    print $admin->password , "\n";# pass2; inherited from User 
-    print $user->hello , "\n";# I am a regular user. 
-    print $admin->hello, "\n";# I am an admin. 
-    ### The statement below will NOT print 
-    print "I can do admin stuff\n" if $user->can('do_admin_stuff'); 
-    ### The statement below will print 
-    print "I can do admin stuff\n" if $admin->can('do_admin_stuff'); 
+
+B<Test File> test.pl
+
+    use warnings;
+    use strict;
+    use My::Schema;
+
+    my $user_data = { email    => 'someguy@place.com',
+                      password => 'pass1',
+                      admin    => 0 };
+
+    my $admin_data = { email    => 'someadmin@adminplace.com',
+                       password => 'pass2',
+                       admin    => 1 };
+
+    my $schema = My::Schema->connection('dbi:Pg:dbname=test');
+
+    $schema->resultset('User')->create( $user_data );
+    $schema->resultset('User')->create( $admin_data );
+
+    ### Now we search for them
+    my $user = $schema->resultset('User')->single( $user_data );
+    my $admin = $schema->resultset('User')->single( $admin_data );
+
+    print ref $user, "\n";
+    print ref $admin, "\n";
+
+    print $user->password , "\n"; # pass1
+    print $admin->password , "\n";# pass2; inherited from User
+    print $user->hello , "\n";# I am a regular user.
+    print $admin->hello, "\n";# I am an admin.
+
+    ### The statement below will NOT print
+    print "I can do admin stuff\n" if $user->can('do_admin_stuff');
+    ### The statement below will print
+    print "I can do admin stuff\n" if $admin->can('do_admin_stuff');
 
 =head2 Skip row object creation for faster results
 
 DBIx::Class is not built for speed, it's built for convenience and
 ease of use, but sometimes you just need to get the data, and skip the
 fancy objects.
-  
+
 To do this simply use L<DBIx::Class::ResultClass::HashRefInflator>.
-  
+
  my $rs = $schema->resultset('CD');
+
  $rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
+
  my $hash_ref = $rs->find(1);
 
 Wasn't that easy?
@@ -957,7 +969,7 @@ ways, the obvious one is to use search:
 
   my $rs = $schema->resultset('Items')->search(
     {},
-    { 
+    {
        select => [ { sum => 'Cost' } ],
        as     => [ 'total_cost' ], # remember this 'as' is for DBIx::Class::ResultSet not SQL
     }
@@ -986,7 +998,7 @@ Or just iterate through the values of this column only:
     print $c;
   }
 
-C<ResultSetColumn> only has a limited number of built-in functions, if
+C<ResultSetColumn> only has a limited number of built-in functions. If
 you need one that it doesn't have, then you can use the C<func> method
 instead:
 
@@ -1001,7 +1013,7 @@ See L<DBIx::Class::ResultSetColumn> for more documentation.
 
 =head2 Creating a result set from a set of rows
 
-Sometimes you have a (set of) row objects that you want to put into a 
+Sometimes you have a (set of) row 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:
 
@@ -1036,7 +1048,7 @@ Deletes only the book named Titanic by the author in $author.
 
 =head2 Ordering a relationship result set
 
-If you always want a relation to be ordered, you can specify this when you 
+If you always want a relation to be ordered, you can specify this when you
 create the relationship.
 
 To order C<< $book->pages >> by descending page_number, create the relation
@@ -1097,11 +1109,11 @@ declaration, like so...
   package MyDatabase::Main::Artist;
   use base qw/DBIx::Class/;
   __PACKAGE__->load_components(qw/PK::Auto Core/);
-  
+
   __PACKAGE__->table('database1.artist'); # will use "database1.artist" in FROM clause
-  
-  __PACKAGE__->add_columns(qw/ artistid name /);
-  __PACKAGE__->set_primary_key('artistid');
+
+  __PACKAGE__->add_columns(qw/ artist_id name /);
+  __PACKAGE__->set_primary_key('artist_id');
   __PACKAGE__->has_many('cds' => 'MyDatabase::Main::Cd');
 
   1;
@@ -1120,16 +1132,16 @@ building a renaming facility, like so:
 
   package MyDatabase::Schema;
   use Moose;
-  
+
   extends 'DBIx::Class::Schema';
-  
+
   around connection => sub {
     my ( $inner, $self, $dsn, $username, $pass, $attr ) = ( shift, @_ );
-   
+
     my $postfix = delete $attr->{schema_name_postfix};
-    
+
     $inner->(@_);
-    
+
     if ( $postfix ) {
         $self->append_db_name($postfix);
     }
@@ -1137,18 +1149,18 @@ building a renaming facility, like so:
 
   sub append_db_name {
     my ( $self, $postfix ) = @_;
-    
-    my @sources_with_db 
-        = grep 
-            { $_->name =~ /^\w+\./mx } 
-            map 
-                { $self->source($_) } 
+
+    my @sources_with_db
+        = grep
+            { $_->name =~ /^\w+\./mx }
+            map
+                { $self->source($_) }
                 $self->sources;
-    
+
     foreach my $source (@sources_with_db) {
         my $name = $source->name;
         $name =~ s{^(\w+)\.}{${1}${postfix}\.}mx;
-        
+
         $source->name($name);
     }
   }
@@ -1160,17 +1172,17 @@ method and extracting a custom option from the provided \%attr hashref one can
 then simply iterate over all the Schema's ResultSources, renaming them as
 needed.
 
-To use this facility, simply add or modify the \%attr hashref that is passed to 
+To use this facility, simply add or modify the \%attr hashref that is passed to
 L<connection|DBIx::Class::Schama/connect>, as follows:
 
-  my $schema 
+  my $schema
     = MyDatabase::Schema->connect(
-      $dsn, 
-      $user, 
+      $dsn,
+      $user,
       $pass,
       {
         schema_name_postfix => '_dev'
-        # ... Other options as desired ... 
+        # ... Other options as desired ...
       })
 
 Obviously, one could accomplish even more advanced mapping via a hash map or a
@@ -1216,14 +1228,14 @@ transaction to fail. Support for savepoints and for true nested
 transactions (for databases that support them) will hopefully be added
 in the future.
 
-=head1 SQL 
+=head1 SQL
 
 =head2 Creating Schemas From An Existing Database
 
-L<DBIx::Class::Schema::Loader> will connect to a database and create a 
+L<DBIx::Class::Schema::Loader> will connect to a database and create a
 L<DBIx::Class::Schema> and associated sources by examining the database.
 
-The recommend way of achieving this is to use the 
+The recommend way of achieving this is to use the
 L<make_schema_at|DBIx::Class::Schema::Loader/make_schema_at> method:
 
   perl -MDBIx::Class::Schema::Loader=make_schema_at,dump_to_dir:./lib \
@@ -1285,7 +1297,7 @@ other than a select, if you CRUD on your dual table you *will* break
 your database.
 
 Make a table class as you would for any other table
-                                                                               
+
   package MyAppDB::Dual;
   use strict;
   use warnings;
@@ -1296,34 +1308,34 @@ Make a table class as you would for any other table
     "dummy",
     { data_type => "VARCHAR2", is_nullable => 0, size => 1 },
   );
+
 Once you've loaded your table class select from it using C<select>
 and C<as> instead of C<columns>
+
   my $rs = $schema->resultset('Dual')->search(undef,
     { select => [ 'sydate' ],
       as     => [ 'now' ]
     },
   );
+
 All you have to do now is be careful how you access your resultset, the below
 will not work because there is no column called 'now' in the Dual table class
+
   while (my $dual = $rs->next) {
     print $dual->now."\n";
   }
   # Can't locate object method "now" via package "MyAppDB::Dual" at headshot.pl line 23.
+
 You could of course use 'dummy' in C<as> instead of 'now', or C<add_columns> to
 your Dual class for whatever you wanted to select from dual, but that's just
 silly, instead use C<get_column>
+
   while (my $dual = $rs->next) {
     print $dual->get_column('now')."\n";
   }
+
 Or use C<cursor>
+
   my $cursor = $rs->cursor;
   while (my @vals = $cursor->next) {
     print $vals[0]."\n";
@@ -1340,48 +1352,48 @@ L<SQL::Translator> to not create table dual:
         parser_args    => { sources => [ grep $_ ne 'Dual', schema->sources ] },
     };
     $schema->create_ddl_dir( [qw/Oracle/], undef, './sql', undef, $sqlt_args );
+
 Or use L<DBIx::Class::ResultClass::HashRefInflator>
+
   $rs->result_class('DBIx::Class::ResultClass::HashRefInflator');
   while ( my $dual = $rs->next ) {
     print $dual->{now}."\n";
   }
+
 Here are some example C<select> conditions to illustrate the different syntax
-you could use for doing stuff like 
+you could use for doing stuff like
 C<oracles.heavily(nested(functions_can('take', 'lots'), OF), 'args')>
+
   # get a sequence value
   select => [ 'A_SEQ.nextval' ],
+
   # get create table sql
   select => [ { 'dbms_metadata.get_ddl' => [ "'TABLE'", "'ARTIST'" ]} ],
+
   # get a random num between 0 and 100
   select => [ { "trunc" => [ { "dbms_random.value" => [0,100] } ]} ],
+
   # what year is it?
   select => [ { 'extract' => [ \'year from sysdate' ] } ],
+
   # do some math
   select => [ {'round' => [{'cos' => [ \'180 * 3.14159265359/180' ]}]}],
+
   # which day of the week were you born on?
   select => [{'to_char' => [{'to_date' => [ "'25-DEC-1980'", "'dd-mon-yyyy'" ]}, "'day'"]}],
+
   # select 16 rows from dual
   select   => [ "'hello'" ],
   as       => [ 'world' ],
   group_by => [ 'cube( 1, 2, 3, 4 )' ],
+
+
 
 =head2 Adding Indexes And Functions To Your SQL
 
 Often you will want indexes on columns on your table to speed up searching. To
-do this, create a method called C<sqlt_deploy_hook> in the relevant source 
-class (refer to the advanced 
+do this, create a method called C<sqlt_deploy_hook> in the relevant source
+class (refer to the advanced
 L<callback system|DBIx::Class::ResultSource/sqlt_deploy_callback> if you wish
 to share a hook between multiple sources):
 
@@ -1398,13 +1410,13 @@ to share a hook between multiple sources):
 
  1;
 
-Sometimes you might want to change the index depending on the type of the 
+Sometimes you might want to change the index depending on the type of the
 database for which SQL is being generated:
 
   my ($db_type = $sqlt_table->schema->translator->producer_type)
     =~ s/^SQL::Translator::Producer:://;
 
-You can also add hooks to the schema level to stop certain tables being 
+You can also add hooks to the schema level to stop certain tables being
 created:
 
  package My::Schema;
@@ -1497,7 +1509,7 @@ database thinks it has.
 Alternatively, you can send the conversion sql scripts to your
 customers as above.
 
-=head2 Setting quoting for the generated SQL. 
+=head2 Setting quoting for the generated SQL.
 
 If the database contains column names with spaces and/or reserved words, they
 need to be quoted in the SQL queries. This is done using:
@@ -1507,14 +1519,14 @@ need to be quoted in the SQL queries. This is done using:
 
 The first sets the quote characters. Either a pair of matching
 brackets, or a C<"> or C<'>:
-  
+
  __PACKAGE__->storage->sql_maker->quote_char('"');
 
 Check the documentation of your database for the correct quote
 characters to use. C<name_sep> needs to be set to allow the SQL
 generator to put the quotes the correct place.
 
-In most cases you should set these as part of the arguments passed to 
+In most cases you should set these as part of the arguments passed to
 L<DBIx::Class::Schema/connect>:
 
  my $schema = My::Schema->connect(
@@ -1542,7 +1554,7 @@ to Microsoft SQL-server (See more names in SQL::Abstract::Limit
 The JDBC bridge is one way of getting access to a MSSQL server from a platform
 that Microsoft doesn't deliver native client libraries for. (e.g. Linux)
 
-The limit dialect can also be set at connect time by specifying a 
+The limit dialect can also be set at connect time by specifying a
 C<limit_dialect> key in the final hash as shown above.
 
 =head2 Working with PostgreSQL array types
@@ -1583,7 +1595,7 @@ the bind values (the C<[1, 2, 3]> arrayref in the above example) wrapped in
 arrayrefs together with the column name, like this: C<< [column_name => value]
 >>.
 
-=head1 BOOTSTRAPPING/MIGRATING 
+=head1 BOOTSTRAPPING/MIGRATING
 
 =head2 Easy migration from class-based to schema-based setup
 
@@ -1594,10 +1606,10 @@ instead:
 
   use MyDB;
   use SQL::Translator;
-  
+
   my $schema = MyDB->schema_instance;
-  
-  my $translator           =  SQL::Translator->new( 
+
+  my $translator           =  SQL::Translator->new(
       debug                => $debug          ||  0,
       trace                => $trace          ||  0,
       no_comments          => $no_comments    ||  0,
@@ -1611,13 +1623,13 @@ instead:
           'prefix'         => 'My::Schema',
                          },
   );
-  
+
   $translator->parser('SQL::Translator::Parser::DBIx::Class');
   $translator->producer('SQL::Translator::Producer::DBIx::Class::File');
-  
+
   my $output = $translator->translate(@args) or die
           "Error: " . $translator->error;
-  
+
   print $output;
 
 You could use L<Module::Find> to search for all subclasses in the MyDB::*
@@ -1646,16 +1658,16 @@ C<next::method>.
     return $new;
   }
 
-For more information about C<next::method>, look in the L<Class::C3> 
+For more information about C<next::method>, look in the L<Class::C3>
 documentation. See also L<DBIx::Class::Manual::Component> for more
 ways to write your own base classes to do this.
 
 People looking for ways to do "triggers" with DBIx::Class are probably
-just looking for this. 
+just looking for this.
 
 =head2 Changing one field whenever another changes
 
-For example, say that you have three columns, C<id>, C<number>, and 
+For example, say that you have three columns, C<id>, C<number>, and
 C<squared>.  You would like to make changes to C<number> and have
 C<squared> be automagically set to the value of C<number> squared.
 You can accomplish this by overriding C<store_column>:
@@ -1673,7 +1685,7 @@ redispatches your call to store_column in the superclass(es).
 
 =head2 Automatically creating related objects
 
-You might have a class C<Artist> which has many C<CD>s.  Further, if you
+You might have a class C<Artist> which has many C<CD>s.  Further, you
 want to create a C<CD> object every time you insert an C<Artist> object.
 You can accomplish this by overriding C<insert> on your objects:
 
@@ -1870,7 +1882,7 @@ Typically L<DBIx::Class> result classes start off with
 If this preamble is moved into a common base class:-
 
     package MyDBICbase;
-    
+
     use base qw/DBIx::Class/;
     __PACKAGE__->load_components(qw/InflateColumn::DateTime Core/);
     1;
@@ -1891,7 +1903,7 @@ The schema class will normally contain
 to load the result classes. This will use L<Module::Find|Module::Find>
 to find and load the appropriate modules. Explicitly defining the
 classes you wish to load will remove the overhead of
-L<Module::Find|Module::Find> and the related directory operations:-
+L<Module::Find|Module::Find> and the related directory operations:
 
     __PACKAGE__->load_classes(qw/ CD Artist Track /);
 
index ff64f5e..1f332fc 100644 (file)
@@ -43,7 +43,7 @@ Save the following into a example.sql in the directory db
 
   CREATE TABLE artist (
     artistid INTEGER PRIMARY KEY,
-    name TEXT NOT NULL 
+    name TEXT NOT NULL
   );
 
   CREATE TABLE cd (
@@ -60,7 +60,7 @@ Save the following into a example.sql in the directory db
 
 and create the sqlite database file:
 
-sqlite3 example.db < example.sql
+  sqlite3 example.db < example.sql
 
 =head3 Set up DBIx::Class::Schema
 
@@ -78,7 +78,7 @@ Now create some more directories:
 Then, create the following DBIx::Class::Schema classes:
 
 MyDatabase/Main.pm:
-    
+
   package MyDatabase::Main;
   use base qw/DBIx::Class::Schema/;
   __PACKAGE__->load_namespaces;
@@ -90,7 +90,7 @@ MyDatabase/Main/Result/Artist.pm:
 
   package MyDatabase::Main::Result::Artist;
   use base qw/DBIx::Class/;
-  __PACKAGE__->load_components(qw/PK::Auto Core/);
+  __PACKAGE__->load_components(qw/Core/);
   __PACKAGE__->table('artist');
   __PACKAGE__->add_columns(qw/ artistid name /);
   __PACKAGE__->set_primary_key('artistid');
@@ -103,7 +103,7 @@ MyDatabase/Main/Result/Cd.pm:
 
   package MyDatabase::Main::Result::Cd;
   use base qw/DBIx::Class/;
-  __PACKAGE__->load_components(qw/PK::Auto Core/);
+  __PACKAGE__->load_components(qw/Core/);
   __PACKAGE__->table('cd');
   __PACKAGE__->add_columns(qw/ cdid artist title/);
   __PACKAGE__->set_primary_key('cdid');
@@ -117,7 +117,7 @@ MyDatabase/Main/Result/Track.pm:
 
   package MyDatabase::Main::Result::Track;
   use base qw/DBIx::Class/;
-  __PACKAGE__->load_components(qw/PK::Auto Core/);
+  __PACKAGE__->load_components(qw/Core/);
   __PACKAGE__->table('track');
   __PACKAGE__->add_columns(qw/ trackid cd title/);
   __PACKAGE__->set_primary_key('trackid');
@@ -137,7 +137,7 @@ insertdb.pl
 
   my $schema = MyDatabase::Main->connect('dbi:SQLite:db/example.db');
 
-  #  here's some of the sql that is going to be generated by the schema
+  #  here's some of the SQL that is going to be generated by the schema
   #  INSERT INTO artist VALUES (NULL,'Michael Jackson');
   #  INSERT INTO artist VALUES (NULL,'Eminem');
 
@@ -248,8 +248,8 @@ testdb.pl:
     }
     print "\n";
   }
-  
-  
+
+
   sub get_cd_by_track {
     my $tracktitle = shift;
     print "get_cd_by_track($tracktitle):\n";
@@ -264,7 +264,7 @@ testdb.pl:
     my $cd = $rs->first;
     print $cd->title . "\n\n";
   }
-  
+
   sub get_cds_by_artist {
     my $artistname = shift;
     print "get_cds_by_artist($artistname):\n";
@@ -349,20 +349,20 @@ It should output:
 
 A reference implentation of the database and scripts in this example
 are available in the main distribution for DBIx::Class under the
-directory t/examples/Schema
+directory F<t/examples/Schema>.
 
 With these scripts we're relying on @INC looking in the current
 working directory.  You may want to add the MyDatabase namespaces to
 @INC in a different way when it comes to deployment.
 
-The testdb.pl script is an excellent start for testing your database
+The F<testdb.pl> script is an excellent start for testing your database
 model.
 
-This example uses load_namespaces to load in the appropriate Row classes
-from the MyDatabase::Main::Result namespace, and any required resultset
-classes from the MyDatabase::Main::ResultSet namespace (although we
-created the directory in the directions above we did not add, or need to
-add, any resultset classes).
+This example uses L<DBIx::Class::Schema/load_namespaces> to load in the
+appropriate L<Row|DBIx::Class::Row> classes from the MyDatabase::Main::Result namespace,
+and any required resultset classes from the MyDatabase::Main::ResultSet
+namespace (although we created the directory in the directions above we
+did not add, or need to add, any resultset classes).
 
 =head1 TODO
 
index d635342..d0f6634 100644 (file)
@@ -87,7 +87,7 @@ as part of the name, and make sure you give the one user you are going
 to connect with rights to read/write all the schemas/tables as
 necessary.
 
-=back 
+=back
 
 =head2 Relationships
 
@@ -112,7 +112,7 @@ for details.
 Create a C<belongs_to> relationship for the field containing the
 foreign key.  See L<DBIx::Class::Relationship/belongs_to>.
 
-=item .. define a foreign key relationship where the key field may contain NULL?  
+=item .. define a foreign key relationship where the key field may contain NULL?
 
 Just create a C<belongs_to> relationship, as above. If the column is
 NULL then the inflation to the foreign object will not happen. This
@@ -307,8 +307,8 @@ See the prefetch examples in the L<Cookbook|DBIx::Class::Manual::Cookbook>.
 
 =item .. fetch a whole column of data instead of a row?
 
-Call C<get_column> on a L<DBIx::Class::ResultSet>, this returns a
-L<DBIx::Class::ResultSetColumn>, see it's documentation and the
+Call C<get_column> on a L<DBIx::Class::ResultSet>. This returns a
+L<DBIx::Class::ResultSetColumn>. See its documentation and the
 L<Cookbook|DBIx::Class::Manual::Cookbook> for details.
 
 =item .. fetch a formatted column?
@@ -324,22 +324,17 @@ See the Cookbook for more details.
 
 =item .. fetch a single (or topmost) row?
 
-Sometimes you many only want a single record back from a search. A quick
-way to get that single row is to first run your search as usual:
-
-  ->search->(undef, { order_by => "id DESC" })
-
-Then call L<DBIx::Class::ResultSet/slice> and ask it only to return 1 row:
+See L<DBIx::Class::Manual::Cookbook/Retrieve_one_and_only_one_row_from_a_resultset>.
 
-  ->slice(0)
-
-These two calls can be combined into a single statement:
+A less readable way is to ask a regular search to return 1 row, using
+L<DBIx::Class::ResultSet/slice>:
 
   ->search->(undef, { order_by => "id DESC" })->slice(0)
 
-Why slice instead of L<DBIx::Class::ResultSet/first> or L<DBIx::Class::ResultSet/single>?
-If supported by the database, slice will use LIMIT/OFFSET to hint to the database that we
-really only need one row. This can result in a significant speed improvement.
+which (if supported by the database) will use LIMIT/OFFSET to hint to the
+database that we really only need one row. This can result in a significant
+speed improvement. The method using L<DBIx::Class::ResultSet/single> mentioned
+in the cookbook can do the same if you pass a C<rows> attribute to the search.
 
 =item .. refresh a row from storage?
 
@@ -410,17 +405,17 @@ scalar reference:
 
 But note that when using a scalar reference the column in the database
 will be updated but when you read the value from the object with e.g.
+
  ->somecolumn()
+
 you still get back the scalar reference to the string, B<not> the new
 value in the database. To get that you must refresh the row from storage
 using C<discard_changes()>. Or chain your function calls like this:
 
   ->update->discard_changes
- to update the database and refresh the object in one step.
+
+to update the database and refresh the object in one step.
+
 =item .. store JSON/YAML in a column and have it deflate/inflate automatically?
 
 You can use L<DBIx::Class::InflateColumn> to accomplish YAML/JSON storage transparently.
@@ -474,7 +469,7 @@ An another method is to use L<Moose> with your L<DBIx::Class> package.
        package MyTable;
 
        use Moose; # import Moose
-       use Moose::Util::TypeConstraint; # import Moose accessor type constraints 
+       use Moose::Util::TypeConstraint; # import Moose accessor type constraints
 
        extends 'DBIx::Class'; # Moose changes the way we define our parent (base) package
 
@@ -486,7 +481,7 @@ With either of these methods the resulting use of the accesssor would be
 
        my $row;
 
-       # assume that some where in here $row will get assigned to a MyTable row
+       # assume that somewhere in here $row will get assigned to a MyTable row
 
        $row->non_column_data('some string'); # would set the non_column_data accessor
 
@@ -494,7 +489,7 @@ With either of these methods the resulting use of the accesssor would be
 
        $row->update(); # would not inline the non_column_data accessor into the update
 
-       
+
 =item How do I use DBIx::Class objects in my TT templates?
 
 Like normal objects, mostly. However you need to watch out for TT
@@ -536,7 +531,7 @@ Look at the tips in L<DBIx::Class::Manual::Cookbook/"STARTUP SPEED">
 =item How do I reduce the overhead of database queries?
 
 You can reduce the overhead of object creation within L<DBIx::Class>
-using the tips in L<DBIx::Class::Manual::Cookbook/"Skip row object creation for faster results"> 
+using the tips in L<DBIx::Class::Manual::Cookbook/"Skip row object creation for faster results">
 and L<DBIx::Class::Manual::Cookbook/"Get raw data for blindingly fast results">
 
 =back
index 11911e7..dd37d0e 100644 (file)
@@ -11,7 +11,7 @@ better way?  You've come to the right place.
 =head1 THE DBIx::Class WAY
 
 Here are a few simple tips that will help you get your bearings with
-DBIx::Class.  
+DBIx::Class.
 
 =head2 Tables become Result classes
 
@@ -29,7 +29,7 @@ besides) The important thing to understand:
 =head2 It's all about the ResultSet
 
 So, we've got some ResultSources defined.  Now, we want to actually use those
-definitions to help us translate the queries we need into handy perl objects!  
+definitions to help us translate the queries we need into handy perl objects!
 
 Let's say we defined a ResultSource for an "album" table with three columns:
 "albumid", "artist", and "title".  Any time we want to query this table, we'll
@@ -39,18 +39,18 @@ results of:
   SELECT albumid, artist, title FROM album;
 
 Would be retrieved by creating a ResultSet object from the album table's
-ResultSource, likely by using the "search" method.  
+ResultSource, likely by using the "search" method.
 
 DBIx::Class doesn't limit you to creating only simple ResultSets -- if you
 wanted to do something like:
 
   SELECT title FROM album GROUP BY title;
 
-You could easily achieve it. 
+You could easily achieve it.
 
-The important thing to understand: 
+The important thing to understand:
 
-  Any time you would reach for a SQL query in DBI, you are 
+  Any time you would reach for a SQL query in DBI, you are
   creating a DBIx::Class::ResultSet.
 
 =head2 Search is like "prepare"
@@ -109,12 +109,9 @@ Next, create each of the classes you want to load as specified above:
 
 Load any components required by each class with the load_components() method.
 This should consist of "Core" plus any additional components you want to use.
-For example, if you want serial/auto-incrementing primary keys:
-
-  __PACKAGE__->load_components(qw/ PK::Auto Core /);
+For example, if you want to force columns to use UTF-8 encoding:
 
-C<PK::Auto> is supported for many databases; see L<DBIx::Class::Storage::DBI>
-for more information.
+  __PACKAGE__->load_components(qw/ ForceUTF8 Core /);
 
 Set the table for your class:
 
@@ -142,7 +139,7 @@ of information that it may be useful to have -- just pass C<add_columns> a hash:
                               is_auto_increment => 0,
                               default_value => '',
                             },
-                          title  => 
+                          title  =>
                             { data_type => 'varchar',
                               size      => 256,
                               is_nullable => 0,
@@ -176,7 +173,8 @@ to describe a column which contains an ID of another Table, or C<has_many> to
 make a predefined accessor for fetching objects that contain this Table's
 foreign key:
 
-  __PACKAGE__->has_many('albums', 'My::Schema::Result::Artist', 'album_id');
+  # in My::Schema::Result::Artist
+  __PACKAGE__->has_many('albums', 'My::Schema::Result::Album', 'artist');
 
 See L<DBIx::Class::Relationship> for more information about the various types of
 available relationships and how you can design your own.
@@ -273,7 +271,7 @@ To create a new record in the database, you can use the C<create> method.  It
 returns an instance of C<My::Schema::Result::Album> that can be used to access the data
 in the new record:
 
-  my $new_album = $schema->resultset('Album')->create({ 
+  my $new_album = $schema->resultset('Album')->create({
     title  => 'Wish You Were Here',
     artist => 'Pink Floyd'
   });
index 700a1df..2a03c1a 100644 (file)
@@ -34,7 +34,7 @@ to fetch the tracks, or you can use a join. Compare:
 So, joins are a way of extending simple select statements to include
 fields from other, related, tables. There are various types of joins,
 depending on which combination of the data you wish to retrieve, see
-L<MySQL's doc on JOINs|http://dev.mysql.com/doc/refman/5.0/en/join.html>.
+MySQL's doc on JOINs: L<http://dev.mysql.com/doc/refman/5.0/en/join.html>.
 
 =head1 DEFINING JOINS AND RELATIONSHIPS
 
index b3fd102..9f2e253 100644 (file)
@@ -60,20 +60,20 @@ That's it, now you can change the position of your objects.
 
   #!/use/bin/perl
   use My::Item;
-  
+
   my $item = My::Item->create({ name=>'Matt S. Trout' });
   # If using grouping_column:
   my $item = My::Item->create({ name=>'Matt S. Trout', group_id=>1 });
-  
+
   my $rs = $item->siblings();
   my @siblings = $item->siblings();
-  
+
   my $sibling;
   $sibling = $item->first_sibling();
   $sibling = $item->last_sibling();
   $sibling = $item->previous_sibling();
   $sibling = $item->next_sibling();
-  
+
   $item->move_previous();
   $item->move_next();
   $item->move_first();
@@ -272,14 +272,14 @@ sub last_sibling {
     return defined $lsib ? $lsib : 0;
 }
 
-# an optimised method to get the last sibling position without inflating a row object
-sub _last_sibling_pos {
+# an optimized method to get the last sibling position value without inflating a row object
+sub _last_sibling_posval {
     my $self = shift;
     my $position_column = $self->position_column;
 
     my $cursor = $self->next_siblings->search(
         {},
-        { rows => 1, order_by => { '-desc' => $position_column }, columns => $position_column },
+        { rows => 1, order_by => { '-desc' => $position_column }, select => $position_column },
     )->cursor;
 
     my ($pos) = $cursor->next;
@@ -313,7 +313,7 @@ the last in the list.
 
 sub move_next {
     my $self = shift;
-    return 0 unless $self->next_siblings->count;
+    return 0 unless defined $self->_last_sibling_posval;  # quick way to check for no more siblings
     return $self->move_to ($self->_position + 1);
 }
 
@@ -341,7 +341,11 @@ on success, and 0 if the object is already the last one.
 
 sub move_last {
     my $self = shift;
-    return $self->move_to( $self->_group_rs->count );
+    my $last_posval = $self->_last_sibling_posval;
+
+    return 0 unless defined $last_posval;
+
+    return $self->move_to( $self->_position_from_value ($last_posval) );
 }
 
 =head2 move_to
@@ -436,18 +440,21 @@ sub move_to_group {
         $self->move_last;
 
         $self->set_inflated_columns({ %$to_group, $position_column => undef });
-        my $new_group_count = $self->_group_rs->count;
+        my $new_group_last_posval = $self->_last_sibling_posval;
+        my $new_group_last_position = $self->_position_from_value (
+          $new_group_last_posval
+        );
 
-        if ( not defined($to_position) or $to_position > $new_group_count) {
+        if ( not defined($to_position) or $to_position > $new_group_last_position) {
             $self->set_column(
-                $position_column => $new_group_count
-                    ? $self->_next_position_value ( $self->_last_sibling_pos )
+                $position_column => $new_group_last_position
+                    ? $self->_next_position_value ( $new_group_last_posval )
                     : $self->_initial_position_value
             );
         }
         else {
             my $bumped_pos_val = $self->_position_value ($to_position);
-            my @between = ($to_position, $new_group_count);
+            my @between = ($to_position, $new_group_last_position);
             $self->_shift_siblings (1, @between);   #shift right
             $self->set_column( $position_column => $bumped_pos_val );
         }
@@ -473,10 +480,10 @@ sub insert {
     my $position_column = $self->position_column;
 
     unless ($self->get_column($position_column)) {
-        my $lsib_pos = $self->_last_sibling_pos;
+        my $lsib_posval = $self->_last_sibling_posval;
         $self->set_column(
-            $position_column => (defined $lsib_pos
-                ? $self->_next_position_value ( $lsib_pos )
+            $position_column => (defined $lsib_posval
+                ? $self->_next_position_value ( $lsib_posval )
                 : $self->_initial_position_value
             )
         );
@@ -616,6 +623,27 @@ sub _position {
     return $self->get_column ($self->position_column);
 }
 
+=head2 _position_from_value
+
+  my $num_pos = $item->_position_of_value ( $pos_value )
+
+Returns the B<absolute numeric position> of an object with a B<position
+value> set to C<$pos_value>. By default simply returns C<$pos_value>.
+
+=cut
+sub _position_from_value {
+    my ($self, $val) = @_;
+
+    return 0 unless defined $val;
+
+#    #the right way to do this
+#    return $self -> _group_rs
+#                 -> search({ $self->position_column => { '<=', $val } })
+#                 -> count
+
+    return $val;
+}
+
 =head2 _position_value
 
   my $pos_value = $item->_position_value ( $pos )
index f282924..5be4833 100644 (file)
@@ -42,16 +42,16 @@ sub discard_changes {
   my ($self, $attrs) = @_;
   delete $self->{_dirty_columns};
   return unless $self->in_storage; # Don't reload if we aren't real!
-  
+
   if( my $current_storage = $self->get_from_storage($attrs)) {
-       
+
     # Set $self to the current.
        %$self = %$current_storage;
-       
+
     # Avoid a possible infinite loop with
     # sub DESTROY { $_[0]->discard_changes }
     bless $current_storage, 'Do::Not::Exist';
-    
+
     return $self;      
   } else {
     $self->in_storage(0);
index 67e4f68..e3b812b 100644 (file)
@@ -106,7 +106,7 @@ L<DBIx::Class::Relationship::Base>.
 All helper methods are called similar to the following template:
 
   __PACKAGE__->$method_name('relname', 'Foreign::Class', \%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>.
 
@@ -297,7 +297,7 @@ OR condition.
     'My::DBIC::Schema::Book', 
     { 'foreign.author_id' => 'self.id' },
   );
-  
+
   # OR (similar result, assuming related_class is storing our PK, in "author")
   # (the "author" is guessed at from "Author" in the class namespace)
   My::DBIC::Schema::Author->has_many(
index c10d4e4..eb03a3d 100644 (file)
@@ -6,6 +6,11 @@ use warnings;
 use Sub::Name ();
 use Class::Inspector ();
 
+our %_pod_inherit_config = 
+  (
+   class_map => { 'DBIx::Class::Relationship::Accessor' => 'DBIx::Class::Relationship' }
+  );
+
 sub register_relationship {
   my ($class, $rel, $info) = @_;
   if (my $acc_type = $info->{attrs}{accessor}) {
index 8c2e4fd..7cd3214 100644 (file)
@@ -83,18 +83,18 @@ command immediately before C<JOIN>.
 
 An arrayref containing a list of accessors in the foreign class to create in
 the main class. If, for example, you do the following:
-  
+
   MyDB::Schema::CD->might_have(liner_notes => 'MyDB::Schema::LinerNotes',
     undef, {
       proxy => [ qw/notes/ ],
     });
-  
+
 Then, assuming MyDB::Schema::LinerNotes has an accessor named notes, you can do:
 
   my $cd = MyDB::Schema::CD->find(1);
   $cd->notes('Notes go here'); # set notes -- LinerNotes object is
                                # created if it doesn't exist
-  
+
 =item accessor
 
 Specifies the type of accessor that should be created for the relationship.
@@ -176,13 +176,13 @@ sub related_resultset {
   $self->throw_exception("Can't call *_related as class methods")
     unless ref $self;
   my $rel = shift;
-  my $rel_obj = $self->relationship_info($rel);
+  my $rel_info = $self->relationship_info($rel);
   $self->throw_exception( "No such relationship ${rel}" )
-    unless $rel_obj;
-  
+    unless $rel_info;
+
   return $self->{related_resultsets}{$rel} ||= do {
     my $attrs = (@_ > 1 && ref $_[$#_] eq 'HASH' ? pop(@_) : {});
-    $attrs = { %{$rel_obj->{attrs} || {}}, %$attrs };
+    $attrs = { %{$rel_info->{attrs} || {}}, %$attrs };
 
     $self->throw_exception( "Invalid query: @_" )
       if (@_ > 1 && (@_ % 2 == 1));
@@ -190,7 +190,7 @@ sub related_resultset {
 
     my $source = $self->result_source;
     my $cond = $source->_resolve_condition(
-      $rel_obj->{cond}, $rel, $self
+      $rel_info->{cond}, $rel, $self
     );
     if ($cond eq $DBIx::Class::ResultSource::UNRESOLVABLE_CONDITION) {
       my $reverse = $source->reverse_relationship_info($rel);
@@ -390,22 +390,22 @@ set them in the storage.
 
 sub set_from_related {
   my ($self, $rel, $f_obj) = @_;
-  my $rel_obj = $self->relationship_info($rel);
-  $self->throw_exception( "No such relationship ${rel}" ) unless $rel_obj;
-  my $cond = $rel_obj->{cond};
+  my $rel_info = $self->relationship_info($rel);
+  $self->throw_exception( "No such relationship ${rel}" ) unless $rel_info;
+  my $cond = $rel_info->{cond};
   $self->throw_exception(
     "set_from_related can only handle a hash condition; the ".
     "condition for $rel is of type ".
     (ref $cond ? ref $cond : 'plain scalar')
   ) unless ref $cond eq 'HASH';
   if (defined $f_obj) {
-    my $f_class = $rel_obj->{class};
+    my $f_class = $rel_info->{class};
     $self->throw_exception( "Object $f_obj isn't a ".$f_class )
       unless Scalar::Util::blessed($f_obj) and $f_obj->isa($f_class);
   }
   $self->set_columns(
     $self->result_source->_resolve_condition(
-       $rel_obj->{cond}, $f_obj, $rel));
+       $rel_info->{cond}, $f_obj, $rel));
   return 1;
 }
 
index a4dce6e..af68b7b 100644 (file)
@@ -7,6 +7,11 @@ package # hide from PAUSE
 use strict;
 use warnings;
 
+our %_pod_inherit_config = 
+  (
+   class_map => { 'DBIx::Class::Relationship::BelongsTo' => 'DBIx::Class::Relationship' }
+  );
+
 sub belongs_to {
   my ($class, $rel, $f_class, $cond, $attrs) = @_;
 
index 3d5da76..e5afd35 100644 (file)
@@ -4,6 +4,11 @@ package # hide from PAUSE
 use strict;
 use warnings;
 
+our %_pod_inherit_config = 
+  (
+   class_map => { 'DBIx::Class::Relationship::CascadeActions' => 'DBIx::Class::Relationship' }
+  );
+
 sub delete {
   my ($self, @rest) = @_;
   return $self->next::method(@rest) unless ref $self;
index 6bdefd4..d74a9a4 100644 (file)
@@ -4,6 +4,11 @@ package # hide from PAUSE
 use strict;
 use warnings;
 
+our %_pod_inherit_config = 
+  (
+   class_map => { 'DBIx::Class::Relationship::HasMany' => 'DBIx::Class::Relationship' }
+  );
+
 sub has_many {
   my ($class, $rel, $f_class, $cond, $attrs) = @_;
 
@@ -35,7 +40,7 @@ sub has_many {
     $class->throw_exception(
       "No such column ${f_key} on foreign class ${f_class} ($guess)"
     ) if $f_class_loaded && !$f_class->has_column($f_key);
-      
+
     $cond = { "foreign.${f_key}" => "self.${pri}" };
   }
 
index 543649b..4c910b8 100644 (file)
@@ -4,6 +4,11 @@ package # hide from PAUSE
 use strict;
 use warnings;
 
+our %_pod_inherit_config = 
+  (
+   class_map => { 'DBIx::Class::Relationship::HasOne' => 'DBIx::Class::Relationship' }
+  );
+
 sub might_have {
   shift->_has_one('LEFT' => @_);
 }
index bab7bb1..07a244a 100644 (file)
@@ -7,6 +7,11 @@ use warnings;
 use Carp::Clan qw/^DBIx::Class/;
 use Sub::Name ();
 
+our %_pod_inherit_config = 
+  (
+   class_map => { 'DBIx::Class::Relationship::ManyToMany' => 'DBIx::Class::Relationship' }
+  );
+
 sub many_to_many {
   my ($class, $meth, $rel, $f_rel, $rel_attrs) = @_;
 
@@ -107,7 +112,14 @@ EOW
         "{$set_meth} needs a list of objects or hashrefs"
       );
       my @to_set = (ref($_[0]) eq 'ARRAY' ? @{ $_[0] } : @_);
-      $self->search_related($rel, {})->delete;
+      # 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;
+      }
+      # add in the set rel objects
       $self->$add_meth($_, ref($_[1]) ? $_[1] : {}) for (@to_set);
     };
 
index b2cc547..7b76499 100644 (file)
@@ -6,6 +6,11 @@ use warnings;
 use Sub::Name ();
 use base qw/DBIx::Class/;
 
+our %_pod_inherit_config = 
+  (
+   class_map => { 'DBIx::Class::Relationship::ProxyMethods' => 'DBIx::Class::Relationship' }
+  );
+
 sub register_relationship {
   my ($class, $rel, $info) = @_;
   if (my $proxy_list = $info->{attrs}{proxy}) {
index 45251fa..6d019ad 100644 (file)
@@ -73,8 +73,15 @@ $mk_hash = sub {
 
         # if there is at least one defined column consider the resultset real
         # (and not an emtpy has_many rel containing one empty hashref)
+        # an empty arrayref is an empty multi-sub-prefetch - don't consider
+        # those either
         for (values %$hash) {
-            return $hash if defined $_;
+            if (ref $_ eq 'ARRAY') {
+              return $hash if @$_;
+            }
+            elsif (defined $_) {
+              return $hash;
+            }
         }
 
         return undef;
@@ -116,6 +123,12 @@ C<$first> will B<not> be a hashref, it will be a normal CD row since
 HashRefInflator only affects resultsets at inflation time, and prefetch causes
 relations to be inflated when the master C<$artist> row is inflated.
 
+=item *
+
+Column value inflation, e.g., using modules like
+L<DBIx::Class::InflateColumn::DateTime>, is not performed.
+The returned hash contains the raw database values.
+
 =back
 
 =cut
index 515e45f..9fba9fc 100644 (file)
@@ -513,6 +513,14 @@ sub find {
     my $unique_query = $self->_build_unique_query($input_query, \@unique_cols);
     $query = $self->_add_alias($unique_query, $alias);
   }
+  elsif ($self->{attrs}{accessor} and $self->{attrs}{accessor} eq 'single') {
+    # This means that we got here after a merger of relationship conditions
+    # in ::Relationship::Base::search_related (the row method), and furthermore
+    # the relationship is of the 'single' type. This means that the condition
+    # provided by the relationship (already attached to $self) is sufficient,
+    # as there can be only one row in the databse that would satisfy the 
+    # relationship
+  }
   else {
     my @unique_queries = $self->_unique_queries($input_query, $attrs);
     $query = @unique_queries
@@ -521,27 +529,14 @@ sub find {
   }
 
   # Run the query
-  if (keys %$attrs) {
-    my $rs = $self->search($query, $attrs);
-    if (keys %{$rs->_resolved_attrs->{collapse}}) {
-      my $row = $rs->next;
-      carp "Query returned more than one row" if $rs->next;
-      return $row;
-    }
-    else {
-      return $rs->single;
-    }
+  my $rs = $self->search ($query, $attrs);
+  if (keys %{$rs->_resolved_attrs->{collapse}}) {
+    my $row = $rs->next;
+    carp "Query returned more than one row" if $rs->next;
+    return $row;
   }
   else {
-    if (keys %{$self->_resolved_attrs->{collapse}}) {
-      my $rs = $self->search($query);
-      my $row = $rs->next;
-      carp "Query returned more than one row" if $rs->next;
-      return $row;
-    }
-    else {
-      return $self->single($query);
-    }
+    return $rs->single;
   }
 }
 
@@ -698,10 +693,14 @@ a warning:
 
   Query returned more than one row
 
-In this case, you should be using L</first> or L</find> instead, or if you really
+In this case, you should be using L</next> or L</find> instead, or if you really
 know what you are doing, use the L</rows> attribute to explicitly limit the size
 of the resultset.
 
+This method will also throw an exception if it is called on a resultset prefetching
+has_many, as such a prefetch implies fetching multiple rows from the database in
+order to assemble the resulting object.
+
 =back
 
 =cut
@@ -714,6 +713,12 @@ sub single {
 
   my $attrs = $self->_resolved_attrs_copy;
 
+  if (keys %{$attrs->{collapse}}) {
+    $self->throw_exception(
+      'single() can not be used on resultsets prefetching has_many. Use find( \%cond ) or next() instead'
+    );
+  }
+
   if ($where) {
     if (defined $attrs->{where}) {
       $attrs->{where} = {
@@ -952,7 +957,9 @@ sub next {
 
 sub _construct_object {
   my ($self, @row) = @_;
-  my $info = $self->_collapse_result($self->{_attrs}{as}, \@row);
+
+  my $info = $self->_collapse_result($self->{_attrs}{as}, \@row)
+    or return ();
   my @new = $self->result_class->inflate_result($self->result_source, @$info);
   @new = $self->{_attrs}{record_filter}->(@new)
     if exists $self->{_attrs}{record_filter};
@@ -962,6 +969,19 @@ sub _construct_object {
 sub _collapse_result {
   my ($self, $as_proto, $row) = @_;
 
+  # if the first row that ever came in is totally empty - this means we got
+  # hit by a smooth^Wempty left-joined resultset. Just noop in that case
+  # instead of producing a {}
+  #
+  my $has_def;
+  for (@$row) {
+    if (defined $_) {
+      $has_def++;
+      last;
+    }
+  }
+  return undef unless $has_def;
+
   my @copy = @$row;
 
   # 'foo'         => [ undef, 'foo' ]
@@ -1144,17 +1164,209 @@ sub count {
   return $self->search(@_)->count if @_ and defined $_[0];
   return scalar @{ $self->get_cache } if $self->get_cache;
 
-  my $meth = $self->_has_attr (qw/prefetch collapse distinct group_by/)
-    ? 'count_grouped'
-    : 'count'
-  ;
-
   my $attrs = $self->_resolved_attrs_copy;
+
+  # this is a little optimization - it is faster to do the limit
+  # adjustments in software, instead of a subquery
+  my $rows = delete $attrs->{rows};
+  my $offset = delete $attrs->{offset};
+
+  my $crs;
+  if ($self->_has_resolved_attr (qw/collapse group_by/)) {
+    $crs = $self->_count_subq_rs ($attrs);
+  }
+  else {
+    $crs = $self->_count_rs ($attrs);
+  }
+  my $count = $crs->next;
+
+  $count -= $offset if $offset;
+  $count = $rows if $rows and $rows < $count;
+  $count = 0 if ($count < 0);
+
+  return $count;
+}
+
+=head2 count_rs
+
+=over 4
+
+=item Arguments: $cond, \%attrs??
+
+=item Return Value: $count_rs
+
+=back
+
+Same as L</count> but returns a L<DBIx::Class::ResultSetColumn> object.
+This can be very handy for subqueries:
+
+  ->search( { amount => $some_rs->count_rs->as_query } )
+
+As with regular resultsets the SQL query will be executed only after
+the resultset is accessed via L</next> or L</all>. That would return
+the same single value obtainable via L</count>.
+
+=cut
+
+sub count_rs {
+  my $self = shift;
+  return $self->search(@_)->count_rs if @_;
+
+  # this may look like a lack of abstraction (count() does about the same)
+  # but in fact an _rs *must* use a subquery for the limits, as the
+  # software based limiting can not be ported if this $rs is to be used
+  # in a subquery itself (i.e. ->as_query)
+  if ($self->_has_resolved_attr (qw/collapse group_by offset rows/)) {
+    return $self->_count_subq_rs;
+  }
+  else {
+    return $self->_count_rs;
+  }
+}
+
+#
+# returns a ResultSetColumn object tied to the count query
+#
+sub _count_rs {
+  my ($self, $attrs) = @_;
+
   my $rsrc = $self->result_source;
+  $attrs ||= $self->_resolved_attrs;
+
+  my $tmp_attrs = { %$attrs };
+
+  # take off any limits, record_filter is cdbi, and no point of ordering a count 
+  delete $tmp_attrs->{$_} for (qw/select as rows offset order_by record_filter/);
 
-  return $rsrc->storage->$meth ($rsrc, $attrs);
+  # overwrite the selector (supplied by the storage)
+  $tmp_attrs->{select} = $rsrc->storage->_count_select ($rsrc, $tmp_attrs);
+  $tmp_attrs->{as} = 'count';
+
+  # read the comment on top of the actual function to see what this does
+  $tmp_attrs->{from} = $self->_switch_to_inner_join_if_needed (
+    $tmp_attrs->{from}, $tmp_attrs->{alias}
+  );
+
+  my $tmp_rs = $rsrc->resultset_class->new($rsrc, $tmp_attrs)->get_column ('count');
+
+  return $tmp_rs;
 }
 
+#
+# same as above but uses a subquery
+#
+sub _count_subq_rs {
+  my ($self, $attrs) = @_;
+
+  my $rsrc = $self->result_source;
+  $attrs ||= $self->_resolved_attrs_copy;
+
+  my $sub_attrs = { %$attrs };
+
+  # extra selectors do not go in the subquery and there is no point of ordering it
+  delete $sub_attrs->{$_} for qw/collapse prefetch_select select as order_by/;
+
+  # if we prefetch, we group_by primary keys only as this is what we would get out of the rs via ->next/->all
+  # clobber old group_by regardless
+  if ( keys %{$attrs->{collapse}} ) {
+    $sub_attrs->{group_by} = [ map { "$attrs->{alias}.$_" } ($rsrc->primary_columns) ]
+  }
+
+  $sub_attrs->{select} = $rsrc->storage->_subq_count_select ($rsrc, $sub_attrs);
+
+  # read the comment on top of the actual function to see what this does
+  $sub_attrs->{from} = $self->_switch_to_inner_join_if_needed (
+    $sub_attrs->{from}, $sub_attrs->{alias}
+  );
+
+  # this is so that ordering can be thrown away in things like Top limit
+  $sub_attrs->{-for_count_only} = 1;
+
+  my $sub_rs = $rsrc->resultset_class->new ($rsrc, $sub_attrs);
+
+  $attrs->{from} = [{
+    -alias => 'count_subq',
+    -source_handle => $rsrc->handle,
+    count_subq => $sub_rs->as_query,
+  }];
+
+  # the subquery replaces this
+  delete $attrs->{$_} for qw/where bind collapse group_by having having_bind rows offset/;
+
+  return $self->_count_rs ($attrs);
+}
+
+
+# The DBIC relationship chaining implementation is pretty simple - every
+# new related_relationship is pushed onto the {from} stack, and the {select}
+# window simply slides further in. This means that when we count somewhere
+# in the middle, we got to make sure that everything in the join chain is an
+# actual inner join, otherwise the count will come back with unpredictable
+# results (a resultset may be generated with _some_ rows regardless of if
+# the relation which the $rs currently selects has rows or not). E.g.
+# $artist_rs->cds->count - normally generates:
+# SELECT COUNT( * ) FROM artist me LEFT JOIN cd cds ON cds.artist = me.artistid
+# which actually returns the number of artists * (number of cds || 1)
+#
+# So what we do here is crawl {from}, determine if the current alias is at
+# the top of the stack, and if not - make sure the chain is inner-joined down
+# to the root.
+#
+sub _switch_to_inner_join_if_needed {
+  my ($self, $from, $alias) = @_;
+
+  # subqueries and other oddness is 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
+  );
+
+  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 wrong
+  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
+  # anyway, and deep cloning is just too fucking expensive
+  # So replace the inner hashref manually
+  my @new_from = ($from->[0]);
+  my $sw_idx = { map { $_ => 1 } @$switch_branch };
+
+  for my $j (@{$from}[1 .. $#$from]) {
+    my $jalias = $j->[0]{-alias};
+
+    if ($sw_idx->{$jalias}) {
+      my %attrs = %{$j->[0]};
+      delete $attrs{-join_type};
+      push @new_from, [
+        \%attrs,
+        @{$j}[ 1 .. $#$j ],
+      ];
+    }
+    else {
+      push @new_from, $j;
+    }
+  }
+
+  return \@new_from;
+}
+
+
 sub _bool {
   return 1;
 }
@@ -1201,13 +1413,12 @@ sub all {
 
   my @obj;
 
-  # TODO: don't call resolve here
   if (keys %{$self->_resolved_attrs->{collapse}}) {
-#  if ($self->{attrs}{prefetch}) {
-      # Using $self->cursor->all is really just an optimisation.
-      # If we're collapsing has_many prefetches it probably makes
-      # very little difference, and this is cleaner than hacking
-      # _construct_object to survive the approach
+    # Using $self->cursor->all is really just an optimisation.
+    # If we're collapsing has_many prefetches it probably makes
+    # very little difference, and this is cleaner than hacking
+    # _construct_object to survive the approach
+    $self->cursor->reset;
     my @row = $self->cursor->next;
     while (@row) {
       push(@obj, $self->_construct_object(@row));
@@ -1220,6 +1431,7 @@ sub all {
   }
 
   $self->set_cache(\@obj) if $self->{attrs}{cache};
+
   return @obj;
 }
 
@@ -1234,6 +1446,8 @@ sub all {
 =back
 
 Resets the resultset's cursor, so you can iterate through the elements again.
+Implicitly resets the storage cursor, so a subsequent L</next> will trigger
+another query.
 
 =cut
 
@@ -1276,15 +1490,15 @@ sub _rs_update_delete {
 
   my $rsrc = $self->result_source;
 
-  my $needs_group_by_subq = $self->_has_attr (qw/prefetch distinct join seen_join group_by/);
-  my $needs_subq = $self->_has_attr (qw/row offset page/);
+  my $needs_group_by_subq = $self->_has_resolved_attr (qw/collapse group_by -join/);
+  my $needs_subq = $self->_has_resolved_attr (qw/row offset/);
 
   if ($needs_group_by_subq or $needs_subq) {
 
     # make a new $rs selecting only the PKs (that's all we really need)
     my $attrs = $self->_resolved_attrs_copy;
 
-    delete $attrs->{$_} for qw/prefetch collapse select +select as +as columns +columns/;
+    delete $attrs->{$_} for qw/collapse select as/;
     $attrs->{columns} = [ map { "$attrs->{alias}.$_" } ($self->result_source->primary_columns) ];
 
     if ($needs_group_by_subq) {
@@ -1504,8 +1718,9 @@ In void context, C<insert_bulk> in L<DBIx::Class::Storage::DBI> is used
 to insert the data, as this is a faster method.
 
 Otherwise, each set of data is inserted into the database using
-L<DBIx::Class::ResultSet/create>, and a arrayref of the resulting row
-objects is returned.
+L<DBIx::Class::ResultSet/create>, and the resulting objects are
+accumulated into an array. The array itself, or an array reference
+is returned depending on scalar or list context.
 
 Example:  Assuming an Artist Class that has many CDs Classes relating:
 
@@ -1571,7 +1786,7 @@ sub populate {
     foreach my $item (@$data) {
       push(@created, $self->create($item));
     }
-    return @created;
+    return wantarray ? @created : \@created;
   } else {
     my ($first, @rest) = @$data;
 
@@ -1808,22 +2023,31 @@ sub _is_deterministic_value {
   return 0;
 }
 
-# _has_attr
+# _has_resolved_attr
 #
 # determines if the resultset defines at least one
 # of the attributes supplied
 #
 # used to determine if a subquery is neccessary
+#
+# supports some virtual attributes:
+#   -join
+#     This will scan for any joins being present on the resultset.
+#     It is not a mere key-search but a deep inspection of {from}
+#
 
-sub _has_attr {
+sub _has_resolved_attr {
   my ($self, @attr_names) = @_;
 
   my $attrs = $self->_resolved_attrs;
 
-  my $join_check_req;
+  my %extra_checks;
 
   for my $n (@attr_names) {
-    ++$join_check_req if $n =~ /join/;
+    if (grep { $n eq $_ } (qw/-join/) ) {
+      $extra_checks{$n}++;
+      next;
+    }
 
     my $attr =  $attrs->{$n};
 
@@ -1840,9 +2064,9 @@ sub _has_attr {
     }
   }
 
-  # a join can be expressed as a multi-level from
+  # a resolved join is expressed as a multi-level from
   return 1 if (
-    $join_check_req
+    $extra_checks{-join}
       and
     ref $attrs->{from} eq 'ARRAY'
       and
@@ -1927,7 +2151,19 @@ B<NOTE>: This feature is still experimental.
 
 sub as_query {
   my $self = shift;
-  return $self->result_source->storage->as_query($self->_resolved_attrs);
+
+  my $attrs = $self->_resolved_attrs_copy;
+
+  # For future use:
+  #
+  # in list ctx:
+  # my ($sql, \@bind, \%dbi_bind_attrs) = _select_args_to_query (...)
+  # $sql also has no wrapping parenthesis in list ctx
+  #
+  my $sqlbind = $self->result_source->storage
+    ->_select_args_to_query ($attrs->{from}, $attrs->{select}, $attrs->{where}, $attrs);
+
+  return $sqlbind;
 }
 
 =head2 find_or_new
@@ -1996,12 +2232,15 @@ store. If the appropriate relationships are set up, foreign key fields
 can also be passed an object representing the foreign row, and the
 value will be set to its primary key.
 
-To create related objects, pass a hashref for the value if the related
-item is a foreign key relationship (L<DBIx::Class::Relationship/belongs_to>),
-and use the name of the relationship as the key. (NOT the name of the field,
-necessarily). For C<has_many> and C<has_one> relationships, pass an arrayref
-of hashrefs containing the data for each of the rows to create in the foreign
-tables, again using the relationship name as the key.
+To create related objects, pass a hashref of related-object column values
+B<keyed on the relationship name>. If the relationship is of type C<multi>
+(L<DBIx::Class::Relationship/has_many>) - pass an arrayref of hashrefs.
+The process will correctly identify columns holding foreign keys, and will
+transparrently populate them from the keys of the corresponding relation.
+This can be applied recursively, and will work correctly for a structure
+with an arbitrary depth and width, as long as the relationships actually
+exists and the correct column data has been supplied.
+
 
 Instead of hashrefs of plain related data (key/value pairs), you may
 also pass new or inserted objects. New objects (not inserted yet, see
@@ -2058,7 +2297,7 @@ sub create {
 =back
 
   $cd->cd_to_producer->find_or_create({ producer => $producer },
-                                      { key => 'primary });
+                                      { key => 'primary' });
 
 Tries to find a record based on its primary key or unique constraints; if none
 is found, creates one and returns that instead.
@@ -2320,14 +2559,14 @@ sub related_resultset {
 
   $self->{related_resultsets} ||= {};
   return $self->{related_resultsets}{$rel} ||= do {
-    my $rel_obj = $self->result_source->relationship_info($rel);
+    my $rel_info = $self->result_source->relationship_info($rel);
 
     $self->throw_exception(
       "search_related: result source '" . $self->result_source->source_name .
         "' has no such relationship $rel")
-      unless $rel_obj;
+      unless $rel_info;
 
-    my ($from,$seen) = $self->_resolve_from($rel);
+    my ($from,$seen) = $self->_chain_relationship($rel);
 
     my $join_count = $seen->{$rel};
     my $alias = ($join_count > 1 ? join('_', $rel, $join_count) : $rel);
@@ -2425,8 +2664,13 @@ sub current_source_alias {
 # in order to properly resolve prefetch aliases (any alias
 # with a relation_chain_depth less than the depth of the
 # current prefetch is not considered)
-sub _resolve_from {
-  my ($self, $extra_join) = @_;
+#
+# The increments happen in 1/2s to make it easier to correlate the
+# join depth with the join path. An integer means a relationship
+# specified via a search_related, whereas a fraction means an added
+# join/prefetch via attributes
+sub _chain_relationship {
+  my ($self, $rel) = @_;
   my $source = $self->result_source;
   my $attrs = $self->{attrs};
 
@@ -2434,25 +2678,69 @@ sub _resolve_from {
       $attrs->{from}
         ||
       [{
-        -result_source => $source,
+        -source_handle => $source->handle,
         -alias => $attrs->{alias},
         $attrs->{alias} => $source->from,
       }]
   }];
 
   my $seen = { %{$attrs->{seen_join} || {} } };
+  my $jpath = ($attrs->{seen_join} && keys %{$attrs->{seen_join}}) 
+    ? $from->[-1][0]{-join_path} 
+    : [];
+
 
   # we need to take the prefetch the attrs into account before we
   # ->_resolve_join as otherwise they get lost - captainL
   my $merged = $self->_merge_attr( $attrs->{join}, $attrs->{prefetch} );
 
-  push @$from, $source->_resolve_join($merged, $attrs->{alias}, $seen) if ($merged);
+  my @requested_joins = $source->_resolve_join(
+    $merged,
+    $attrs->{alias},
+    $seen,
+    $jpath,
+  );
+
+  push @$from, @requested_joins;
+
+  $seen->{-relation_chain_depth} += 0.5;
+
+  # if $self already had a join/prefetch specified on it, the requested
+  # $rel might very well be already included. What we do in this case
+  # is effectively a no-op (except that we bump up the chain_depth on
+  # the join in question so we could tell it *is* the search_related)
+  my $already_joined;
 
-  ++$seen->{-relation_chain_depth};
 
-  push @$from, $source->_resolve_join($extra_join, $attrs->{alias}, $seen);
+  # we consider the last one thus reverse
+  for my $j (reverse @requested_joins) {
+    if ($rel eq $j->[0]{-join_path}[-1]) {
+      $j->[0]{-relation_chain_depth} += 0.5;
+      $already_joined++;
+      last;
+    }
+  }
+
+# alternative way to scan the entire chain - not backwards compatible
+#  for my $j (reverse @$from) {
+#    next unless ref $j eq 'ARRAY';
+#    if ($j->[0]{-join_path} && $j->[0]{-join_path}[-1] eq $rel) {
+#      $j->[0]{-relation_chain_depth} += 0.5;
+#      $already_joined++;
+#      last;
+#    }
+#  }
+
+  unless ($already_joined) {
+    push @$from, $source->_resolve_join(
+      $rel,
+      $attrs->{alias},
+      $seen,
+      $jpath,
+    );
+  }
 
-  ++$seen->{-relation_chain_depth};
+  $seen->{-relation_chain_depth} += 0.5;
 
   return ($from,$seen);
 }
@@ -2544,72 +2832,82 @@ sub _resolved_attrs {
   }
 
   $attrs->{from} ||= [ {
-    -result_source => $source,
+    -source_handle => $source->handle,
     -alias => $self->{attrs}{alias},
     $self->{attrs}{alias} => $source->from,
   } ];
 
-  if ( exists $attrs->{join} || exists $attrs->{prefetch} ) {
+  if ( $attrs->{join} || $attrs->{prefetch} ) {
+
+    $self->throw_exception ('join/prefetch can not be used with a literal scalarref {from}')
+      if ref $attrs->{from} ne 'ARRAY';
+
     my $join = delete $attrs->{join} || {};
 
     if ( defined $attrs->{prefetch} ) {
       $join = $self->_merge_attr( $join, $attrs->{prefetch} );
-
     }
 
     $attrs->{from} =    # have to copy here to avoid corrupting the original
       [
-      @{ $attrs->{from} },
-      $source->_resolve_join(
-        $join, $alias, { %{ $attrs->{seen_join} || {} } }
-      )
+        @{ $attrs->{from} },
+        $source->_resolve_join(
+          $join,
+          $alias,
+          { %{ $attrs->{seen_join} || {} } },
+          ($attrs->{seen_join} && keys %{$attrs->{seen_join}})
+            ? $attrs->{from}[-1][0]{-join_path}
+            : []
+          ,
+        )
       ];
-
   }
 
-  if ( $attrs->{order_by} ) {
+  if ( defined $attrs->{order_by} ) {
     $attrs->{order_by} = (
       ref( $attrs->{order_by} ) eq 'ARRAY'
       ? [ @{ $attrs->{order_by} } ]
-      : [ $attrs->{order_by} ]
+      : [ $attrs->{order_by} || () ]
     );
   }
-  else {
-    $attrs->{order_by} = [];
-  }
-
-  # If the order_by is otherwise empty - we will use this for TOP limit
-  # emulation and the like.
-  # Although this is needed only if the order_by is not defined, it is
-  # actually cheaper to just populate this rather than properly examining
-  # order_by (stuf like [ {} ] and the like)
-  $attrs->{_virtual_order_by} = [ $self->result_source->primary_columns ];
 
+  if ($attrs->{group_by} and ! ref $attrs->{group_by}) {
+    $attrs->{group_by} = [ $attrs->{group_by} ];
+  }
 
-  my $collapse = $attrs->{collapse} || {};
+  $attrs->{collapse} ||= {};
   if ( my $prefetch = delete $attrs->{prefetch} ) {
     $prefetch = $self->_merge_attr( {}, $prefetch );
-    my @pre_order;
-    foreach my $p ( ref $prefetch eq 'ARRAY' ? @$prefetch : ($prefetch) ) {
-
-      # bring joins back to level of current class
-      my $join_map = $self->_joinpath_aliases ($attrs->{from}, $attrs->{seen_join});
-      my @prefetch =
-        $source->_resolve_prefetch( $p, $alias, $join_map, \@pre_order, $collapse );
-      push( @{ $attrs->{select} }, map { $_->[0] } @prefetch );
-      push( @{ $attrs->{as} },     map { $_->[1] } @prefetch );
-    }
-    push( @{ $attrs->{order_by} }, @pre_order );
+
+    my $prefetch_ordering = [];
+
+    my $join_map = $self->_joinpath_aliases ($attrs->{from}, $attrs->{seen_join});
+
+    my @prefetch =
+      $source->_resolve_prefetch( $prefetch, $alias, $join_map, $prefetch_ordering, $attrs->{collapse} );
+
+    $attrs->{prefetch_select} = [ map { $_->[0] } @prefetch ];
+    push @{ $attrs->{select} }, @{$attrs->{prefetch_select}};
+    push @{ $attrs->{as} }, (map { $_->[1] } @prefetch);
+
+    push( @{$attrs->{order_by}}, @$prefetch_ordering );
+    $attrs->{_collapse_order_by} = \@$prefetch_ordering;
   }
 
+
   if (delete $attrs->{distinct}) {
     $attrs->{group_by} ||= [ grep { !ref($_) || (ref($_) ne 'HASH') } @{$attrs->{select}} ];
   }
 
-  $attrs->{collapse} = $collapse;
-
-  if ( $attrs->{page} and not defined $attrs->{offset} ) {
-    $attrs->{offset} = ( $attrs->{rows} * ( $attrs->{page} - 1 ) );
+  # 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
+  if (my $page = delete $attrs->{page}) {
+    $attrs->{offset} = 
+      ($attrs->{rows} * ($page - 1))
+            +
+      ($attrs->{offset} || 0)
+    ;
   }
 
   return $self->{_attrs} = $attrs;
@@ -2621,13 +2919,21 @@ sub _joinpath_aliases {
   my $paths = {};
   return $paths unless ref $fromspec eq 'ARRAY';
 
+  my $cur_depth = $seen->{-relation_chain_depth} || 0;
+
+  if (int ($cur_depth) != $cur_depth) {
+    $self->throw_exception ("-relation_chain_depth is not an integer, something went horribly wrong ($cur_depth)");
+  }
+
   for my $j (@$fromspec) {
 
     next if ref $j ne 'ARRAY';
-    next if $j->[0]{-relation_chain_depth} < ( $seen->{-relation_chain_depth} || 0);
+    next if ($j->[0]{-relation_chain_depth} || 0) < $cur_depth;
+
+    my $jpath = $j->[0]{-join_path};
 
     my $p = $paths;
-    $p = $p->{$_} ||= {} for @{$j->[0]{-join_path}};
+    $p = $p->{$_} ||= {} for @{$jpath}[$cur_depth .. $#$jpath];
     push @{$p->{-join_aliases} }, $j->[0]{-alias};
   }
 
@@ -2784,10 +3090,15 @@ These are in no particular order:
 
 =back
 
-Which column(s) to order the results by. If a single column name, or
-an arrayref of names is supplied, the argument is passed through
-directly to SQL. The hashref syntax allows for connection-agnostic
-specification of ordering direction:
+Which column(s) to order the results by. 
+
+[The full list of suitable values is documented in
+L<SQL::Abstract/"ORDER BY CLAUSES">; the following is a summary of
+common options.]
+
+If a single column name, or an arrayref of names is supplied, the
+argument is passed through directly to SQL. The hashref syntax allows
+for connection-agnostic specification of ordering direction:
 
  For descending order:
 
@@ -3066,6 +3377,42 @@ with that artist is given below (assuming many-to-many from artists to tags):
 B<NOTE:> If you specify a C<prefetch> attribute, the C<join> and C<select>
 attributes will be ignored.
 
+B<CAVEATs>: Prefetch does a lot of deep magic. As such, it may not behave
+exactly as you might expect.
+
+=over 4
+
+=item * 
+
+Prefetch uses the L</cache> to populate the prefetched relationships. This
+may or may not be what you want.
+
+=item * 
+
+If you specify a condition on a prefetched relationship, ONLY those
+rows that match the prefetched condition will be fetched into that relationship.
+This means that adding prefetch to a search() B<may alter> what is returned by
+traversing a relationship. So, if you have C<< Artist->has_many(CDs) >> and you do
+
+  my $artist_rs = $schema->resultset('Artist')->search({
+      'cds.year' => 2008,
+  }, {
+      join => 'cds',
+  });
+
+  my $count = $artist_rs->first->cds->count;
+
+  my $artist_rs_prefetch = $artist_rs->search( {}, { prefetch => 'cds' } );
+
+  my $prefetch_count = $artist_rs_prefetch->first->cds->count;
+
+  cmp_ok( $count, '==', $prefetch_count, "Counts should be the same" );
+
+that cmp_ok() may or may not pass depending on the datasets involved. This
+behavior may or may not survive the 0.09 transition.
+
+=back
+
 =head2 page
 
 =over 4
@@ -3078,7 +3425,7 @@ Makes the resultset paged and specifies the page to retrieve. Effectively
 identical to creating a non-pages resultset and then calling ->page($page)
 on it.
 
-If L<rows> attribute is not specified it defualts to 10 rows per page.
+If L<rows> attribute is not specified it defaults to 10 rows per page.
 
 When you have a paged resultset, L</count> will only return the number
 of rows in the page. To get the total, use the L</pager> and call
@@ -3281,9 +3628,21 @@ with a father in the person table, we could explicitly use C<INNER JOIN>:
     # SELECT child.* FROM person child
     # INNER JOIN person father ON child.father_id = father.id
 
-If you need to express really complex joins or you need a subselect, you
+You can select from a subquery by passing a resultset to from as follows.
+
+    $schema->resultset('Artist')->search( 
+        undef, 
+        {   alias => 'artist2',
+            from  => [ { artist2 => $artist_rs->as_query } ],
+        } );
+
+    # and you'll get sql like this..
+    # SELECT artist2.artistid, artist2.name, artist2.rank, artist2.charfield FROM 
+    #   ( SELECT me.artistid, me.name, me.rank, me.charfield FROM artists me ) artist2
+
+If you need to express really complex joins, you
 can supply literal SQL to C<from> via a scalar reference. In this case
-the contents of the scalar will replace the table name asscoiated with the
+the contents of the scalar will replace the table name associated with the
 resultsource.
 
 WARNING: This technique might very well not work as expected on chained
index 037b771..4f48d33 100644 (file)
@@ -39,27 +39,48 @@ sub new {
 
   $rs->throw_exception("column must be supplied") unless $column;
 
-  my $new_parent_rs = $rs->search_rs; # we don't want to mess up the original, so clone it
+  my $orig_attrs = $rs->_resolved_attrs;
+  my $new_parent_rs = $rs->search_rs;
 
   # prefetch causes additional columns to be fetched, but we can not just make a new
   # rs via the _resolved_attrs trick - we need to retain the separation between
   # +select/+as and select/as. At the same time we want to preserve any joins that the
   # prefetch would otherwise generate.
-  my $init_attrs = $new_parent_rs->{attrs} ||= {};
-  delete $init_attrs->{collapse};
-  $init_attrs->{join} = $rs->_merge_attr( delete $init_attrs->{join}, delete $init_attrs->{prefetch} );
+
+  my $new_attrs = $new_parent_rs->{attrs} ||= {};
+  $new_attrs->{join} = $rs->_merge_attr( delete $new_attrs->{join}, delete $new_attrs->{prefetch} );
 
   # If $column can be found in the 'as' list of the parent resultset, use the
   # corresponding element of its 'select' list (to keep any custom column
   # definition set up with 'select' or '+select' attrs), otherwise use $column
   # (to create a new column definition on-the-fly).
-  my $attrs = $new_parent_rs->_resolved_attrs;
 
-  my $as_list = $attrs->{as} || [];
-  my $select_list = $attrs->{select} || [];
+  my $as_list = $orig_attrs->{as} || [];
+  my $select_list = $orig_attrs->{select} || [];
   my $as_index = List::Util::first { ($as_list->[$_] || "") eq $column } 0..$#$as_list;
   my $select = defined $as_index ? $select_list->[$as_index] : $column;
 
+  # {collapse} would mean a has_many join was injected, which in turn means
+  # we need to group IF WE CAN (only if the column in question is unique)
+  if (!$new_attrs->{group_by} && keys %{$orig_attrs->{collapse}}) {
+
+    # scan for a constraint that would contain our column only - that'd be proof
+    # enough it is unique
+    my $constraints = { $rs->result_source->unique_constraints };
+    for my $constraint_columns ( values %$constraints ) {
+
+      next unless @$constraint_columns == 1;
+
+      my $col = $constraint_columns->[0];
+      my $fqcol = join ('.', $new_attrs->{alias}, $col);
+
+      if ($col eq $select or $fqcol eq $select) {
+        $new_attrs->{group_by} = [ $select ];
+        last;
+      }
+    }
+  }
+
   my $new = bless { _select => $select, _as => $column, _parent_resultset => $new_parent_rs }, $class;
   return $new;
 }
@@ -317,7 +338,7 @@ value. Produces the following SQL:
 sub func {
   my ($self,$function) = @_;
   my $cursor = $self->func_rs($function)->cursor;
-  
+
   if( wantarray ) {
     return map { $_->[ 0 ] } $cursor->all;
   }
@@ -352,9 +373,9 @@ sub func_rs {
 =head2 throw_exception
 
 See L<DBIx::Class::Schema/throw_exception> for details.
-  
+
 =cut 
-    
+
 sub throw_exception {
   my $self=shift;
   if (ref $self && $self->{_parent_resultset}) {
index aa636a7..f50cffd 100644 (file)
@@ -24,12 +24,75 @@ DBIx::Class::ResultSource - Result source object
 
 =head1 SYNOPSIS
 
+  # Create a table based result source, in a result class.
+
+  package MyDB::Schema::Result::Artist;
+  use base qw/DBIx::Class/;
+
+  __PACKAGE__->load_components(qw/Core/);
+  __PACKAGE__->table('artist');
+  __PACKAGE__->add_columns(qw/ artistid name /);
+  __PACKAGE__->set_primary_key('artistid');
+  __PACKAGE__->has_many(cds => 'MyDB::Schema::Result::CD');
+
+  1;
+
+  # Create a query (view) based result source, in a result class
+  package MyDB::Schema::Result::Year2000CDs;
+
+  use DBIx::Class::ResultSource::View;
+
+  __PACKAGE__->load_components('Core');
+  __PACKAGE__->table_class('DBIx::Class::ResultSource::View');
+
+  __PACKAGE__->table('year2000cds');
+  __PACKAGE__->result_source_instance->is_virtual(1);
+  __PACKAGE__->result_source_instance->view_definition(
+      "SELECT cdid, artist, title FROM cd WHERE year ='2000'"
+      );
+
+
 =head1 DESCRIPTION
 
-A ResultSource is a component of a schema from which results can be directly
-retrieved, most usually a table (see L<DBIx::Class::ResultSource::Table>)
+A ResultSource is an object that represents a source of data for querying.
+
+This class is a base class for various specialised types of result
+sources, for example L<DBIx::Class::ResultSource::Table>. Table is the
+default result source type, so one is created for you when defining a
+result class as described in the synopsis above.
+
+More specifically, the L<DBIx::Class::Core> component pulls in the
+L<DBIx::Class::ResultSourceProxy::Table> as a base class, 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
+sources, you don't need to remember any of this.
+
+Result sources representing select queries, or views, can also be
+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 can retrieve the result source at runtime in the following ways:
+
+=over
+
+=item From a Schema object:
+
+   $schema->source($source_name);
+
+=item From a Row object:
+
+   $row->result_source;
+
+=item From a ResultSet object:
 
-Basic view support also exists, see L<<DBIx::Class::ResultSource::View>.
+   $rs->result_source;
+
+=back
 
 =head1 METHODS
 
@@ -69,9 +132,9 @@ sub new {
 
   $source->add_columns('col1' => \%col1_info, 'col2' => \%col2_info, ...);
 
-Adds columns to the result source. If supplied key => hashref pairs, uses
-the hashref as the column_info for that column. Repeated calls of this
-method will add more columns, not replace them.
+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.
 
 The column names given will be created as accessor methods on your
 L<DBIx::Class::Row> objects. You can change the name of the accessor
@@ -84,40 +147,62 @@ keys are currently recognised/used by DBIx::Class:
 
 =item accessor
 
+   { accessor => '_name' }
+
+   # example use, replace standard accessor with one of your own:
+   sub name {
+       my ($self, $value) = @_;
+
+       die "Name cannot contain digits!" if($value =~ /\d/);
+       $self->_name($value);
+
+       return $self->_name();
+   }
+
 Use this to set the name of the accessor method for this column. If unset,
 the name of the column will be used.
 
 =item data_type
 
-This contains the column type. It is automatically filled by the
-L<SQL::Translator::Producer::DBIx::Class::File> producer, and the
-L<DBIx::Class::Schema::Loader> module. If you do not enter a
-data_type, DBIx::Class will attempt to retrieve it from the
-database for you, using L<DBI>'s column_info method. The values of this
-key are typically upper-cased.
+   { data_type => 'integer' }
+
+This contains the column type. It is automatically filled if you use the
+L<SQL::Translator::Producer::DBIx::Class::File> producer, or the
+L<DBIx::Class::Schema::Loader> module. 
 
 Currently there is no standard set of values for the data_type. Use
 whatever your database supports.
 
 =item size
 
+   { size => 20 }
+
 The length of your column, if it is a column type that can have a size
-restriction. This is currently only used by L<DBIx::Class::Schema/deploy>.
+restriction. This is currently only used to create tables from your
+schema, see L<DBIx::Class::Schema/deploy>.
 
 =item is_nullable
 
-Set this to a true value for a columns that is allowed to contain
-NULL values. This is currently only used by L<DBIx::Class::Schema/deploy>.
+   { is_nullable => 1 }
+
+Set this to a true value for a columns 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>.
 
 =item is_auto_increment
 
+   { is_auto_increment => 1 }
+
 Set this to a true value for a column whose value is somehow
-automatically set. This is used to determine which columns to empty
-when cloning objects using L<DBIx::Class::Row/copy>. It is also used by
+automatically set, defaults to false. This is used to determine which
+columns to empty when cloning objects using
+L<DBIx::Class::Row/copy>. It is also used by
 L<DBIx::Class::Schema/deploy>.
 
 =item is_numeric
 
+   { is_numeric => 1 }
+
 Set this to a true or false value (not C<undef>) to explicitly specify
 if this column contains numeric data. This controls how set_column
 decides whether to consider a column dirty after an update: if
@@ -130,22 +215,29 @@ result will be cached in this attribute.
 
 =item is_foreign_key
 
+   { is_foreign_key => 1 }
+
 Set this to a true value for a column that contains a key from a
-foreign table. This is currently only used by
-L<DBIx::Class::Schema/deploy>.
+foreign table, defaults to false. This is currently only used to
+create tables from your schema, see L<DBIx::Class::Schema/deploy>.
 
 =item default_value
 
-Set this to the default value which will be inserted into a column
-by the database. Can contain either a value or a function (use a
+   { default_value => \'now()' }
+
+Set this to the default value which will be inserted into a column by
+the database. Can contain either a value or a function (use a
 reference to a scalar e.g. C<\'now()'> if you want a function). This
-is currently only used by L<DBIx::Class::Schema/deploy>.
+is currently only used to create tables from your schema, see
+L<DBIx::Class::Schema/deploy>.
 
 See the note on L<DBIx::Class::Row/new> for more information about possible
 issues related to db-side default values.
 
 =item sequence
 
+   { sequence => 'my_table_seq' }
+
 Set this on a primary key column to the name of the sequence used to
 generate a new key value. If not specified, L<DBIx::Class::PK::Auto>
 will attempt to retrieve the name of the sequence from the database
@@ -171,13 +263,13 @@ L<SQL::Translator::Producer::MySQL>.
 
 =over
 
-=item Arguments: $colname, [ \%columninfo ]
+=item Arguments: $colname, \%columninfo?
 
 =item Return value: 1/0 (true/false)
 
 =back
 
-  $source->add_column('col' => \%info?);
+  $source->add_column('col' => \%info);
 
 Add a single column and optional column info. Uses the same column
 info keys as L</add_columns>.
@@ -237,8 +329,8 @@ sub has_column {
   my $info = $source->column_info($col);
 
 Returns the column metadata hashref for a column, as originally passed
-to L</add_columns>. See the description of L</add_columns> for information
-on the contents of the hashref.
+to L</add_columns>. See L</add_columns> above for information on the
+contents of the hashref.
 
 =cut
 
@@ -362,14 +454,16 @@ sub remove_column { shift->remove_columns(@_); } # DO NOT CHANGE THIS TO GLOB
 
 =back
 
-Defines one or more columns as primary key for this source. Should be
+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>
 named C<primary>.
 
 The primary key columns are used by L<DBIx::Class::PK::Auto> to
-retrieve automatically created values from the database.
+retrieve automatically created values from the database. They are also
+used as default joining columns when specifying relationships, see
+L<DBIx::Class::Relationship>.
 
 =cut
 
@@ -408,7 +502,7 @@ sub primary_columns {
 
 =over 4
 
-=item Arguments: [ $name ], \@colnames
+=item Arguments: $name?, \@colnames
 
 =item Return value: undefined
 
@@ -426,11 +520,13 @@ Alternatively, you can specify only the columns:
 
   __PACKAGE__->add_unique_constraint([ qw/column1 column2/ ]);
 
-This will result in a unique constraint named C<table_column1_column2>, where
-C<table> is replaced with the table name.
+This will result in a unique constraint named
+C<table_column1_column2>, where C<table> is replaced with the table
+name.
 
-Unique constraints are used, for example, when you call
-L<DBIx::Class::ResultSet/find>. Only columns in the constraint are searched.
+Unique constraints are used, for example, when you pass the constraint
+name as the C<key> attribute to L<DBIx::Class::ResultSet/find>. Then
+only columns in the constraint are searched.
 
 Throws an error if any of the given column names do not yet exist on
 the result source.
@@ -499,7 +595,8 @@ sub name_unique_constraint {
 
   $source->unique_constraints();
 
-Read-only accessor which returns a hash of unique constraints on this source.
+Read-only accessor which returns a hash of unique constraints on this
+source.
 
 The hash is keyed by constraint name, and contains an arrayref of
 column names as values.
@@ -659,11 +756,15 @@ but is cached from then on unless resultset_class changes.
 
 =back
 
-  package My::ResultSetClass;
+  package My::Schema::ResultSet::Artist;
   use base 'DBIx::Class::ResultSet';
   ...
 
-  $source->resultset_class('My::ResultSet::Class');
+  # In the result class
+  __PACKAGE__->resultset_class('My::Schema::ResultSet::Artist');
+
+  # Or in code
+  $source->resultset_class('My::Schema::ResultSet::Artist');
 
 Set the class of the resultset. This is useful if you want to create your
 own resultset methods. Create your own class derived from
@@ -681,6 +782,10 @@ exists.
 
 =back
 
+  # In the result class
+  __PACKAGE__->resultset_attributes({ order_by => [ 'id' ] });
+
+  # Or in code
   $source->resultset_attributes({ order_by => [ 'id' ] });
 
 Store a collection of resultset attributes, that will be set on every
@@ -893,7 +998,7 @@ sub add_relationship {
   }
   return unless $f_source; # Can't test rel without f_source
 
-  eval { $self->_resolve_join($rel, 'me') };
+  eval { $self->_resolve_join($rel, 'me', {}, []) };
 
   if ($@) { # If the resolve failed, back out and re-throw the error
     delete $rels{$rel}; #
@@ -981,7 +1086,7 @@ opposing a C<has_many> relation. For definition of these look in
 L<DBIx::Class::Relationship>.
 
 The returned hashref is keyed by the name of the opposing
-relationship, and contains it's data in the same manner as
+relationship, and contains its data in the same manner as
 L</relationship_info>.
 
 =cut
@@ -1083,26 +1188,21 @@ sub resolve_join {
 
 # Returns the {from} structure used to express JOIN conditions
 sub _resolve_join {
-  my ($self, $join, $alias, $seen, $force_left, $jpath) = @_;
+  my ($self, $join, $alias, $seen, $jpath, $force_left) = @_;
 
   # we need a supplied one, because we do in-place modifications, no returns
   $self->throw_exception ('You must supply a seen hashref as the 3rd argument to _resolve_join')
-    unless $seen;
+    unless ref $seen eq 'HASH';
 
-  $force_left ||= { force => 0 };
+  $self->throw_exception ('You must supply a joinpath arrayref as the 4th argument to _resolve_join')
+    unless ref $jpath eq 'ARRAY';
 
-  # This isn't quite right, we should actually dive into $seen and reconstruct
-  # the entire path (the reference entry point would be the join conditional
-  # with depth == current_depth - 1. At this point however nothing depends on
-  # having the entire path, transcending related_resultset, so just leave it
-  # as is, hairy enough already.
-  $jpath ||= [];  
+  $jpath = [@$jpath];
 
   if (ref $join eq 'ARRAY') {
     return
       map {
-        local $force_left->{force} = $force_left->{force};
-        $self->_resolve_join($_, $alias, $seen, $force_left, [@$jpath]);
+        $self->_resolve_join($_, $alias, $seen, $jpath, $force_left);
       } @$join;
   } elsif (ref $join eq 'HASH') {
     return
@@ -1110,9 +1210,9 @@ sub _resolve_join {
         my $as = ($seen->{$_} ? join ('_', $_, $seen->{$_} + 1) : $_);  # the actual seen value will be incremented below
         local $force_left->{force} = $force_left->{force};
         (
-          $self->_resolve_join($_, $alias, $seen, $force_left, [@$jpath]),
+          $self->_resolve_join($_, $alias, $seen, [@$jpath], $force_left),
           $self->related_source($_)->_resolve_join(
-            $join->{$_}, $as, $seen, $force_left, [@$jpath, $_]
+            $join->{$_}, $as, $seen, [@$jpath, $_], $force_left
           )
         );
       } keys %$join;
@@ -1120,22 +1220,24 @@ sub _resolve_join {
     $self->throw_exception("No idea how to resolve join reftype ".ref $join);
   } else {
 
+    return() unless defined $join;
+
     my $count = ++$seen->{$join};
     my $as = ($count > 1 ? "${join}_${count}" : $join);
 
     my $rel_info = $self->relationship_info($join);
     $self->throw_exception("No such relationship ${join}") unless $rel_info;
     my $type;
-    if ($force_left->{force}) {
+    if ($force_left) {
       $type = 'left';
     } else {
       $type = $rel_info->{attrs}{join_type} || '';
-      $force_left->{force} = 1 if lc($type) eq 'left';
+      $force_left = 1 if lc($type) eq 'left';
     }
 
     my $rel_src = $self->related_source($join);
     return [ { $as => $rel_src->from,
-               -result_source => $rel_src,
+               -source_handle => $rel_src->handle,
                -join_type => $type,
                -join_path => [@$jpath, $join],
                -alias => $as,
@@ -1196,7 +1298,6 @@ our $UNRESOLVABLE_CONDITION = \'1 = 0';
 
 sub _resolve_condition {
   my ($self, $cond, $as, $for) = @_;
-  #warn %$cond;
   if (ref $cond eq 'HASH') {
     my %ret;
     foreach my $k (keys %{$cond}) {
@@ -1237,7 +1338,7 @@ sub _resolve_condition {
   } elsif (ref $cond eq 'ARRAY') {
     return [ map { $self->_resolve_condition($_, $as, $for) } @$cond ];
   } else {
-   die("Can't handle this yet :(");
+   die("Can't handle condition $cond yet :(");
   }
 }
 
@@ -1342,15 +1443,14 @@ sub _resolve_prefetch {
       "don't know how to resolve prefetch reftype ".ref($pre));
   }
   else {
-
     my $p = $alias_map;
     $p = $p->{$_} for (@$pref_path, $pre);
 
     $self->throw_exception (
-      "Unable to resolve prefetch $pre - join alias map does not contain an entry for path "
+      "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 );
index 0bcb0fc..a9c3755 100644 (file)
@@ -17,7 +17,7 @@ DBIx::Class::ResultSource::View - ResultSource object representing a view
 
 =head1 SYNOPSIS
 
-  package MyDB::Schema::Year2000CDs;
+  package MyDB::Schema::Result::Year2000CDs;
 
   use DBIx::Class::ResultSource::View;
 
index 7531954..4a402e9 100644 (file)
@@ -77,7 +77,7 @@ sub STORABLE_freeze {
     my ($self, $cloning) = @_;
 
     my $to_serialize = { %$self };
-    
+
     my $class = $self->schema->class($self->source_moniker);
     $to_serialize->{schema} = $class;
     return (Storable::freeze($to_serialize));
index 5cfe4f3..a983ce1 100644 (file)
@@ -67,7 +67,7 @@ Adds columns to the current class and creates accessors for them.
 =head2 table
 
   __PACKAGE__->table('tbl_name');
-  
+
 Gets or sets the table name.
 
 =cut
index a26f654..a6f5f77 100644 (file)
@@ -162,7 +162,7 @@ sub new {
   if ($attrs) {
     $new->throw_exception("attrs must be a hashref")
       unless ref($attrs) eq 'HASH';
-    
+
     my ($related,$inflated);
     ## Pretend all the rels are actual objects, unset below if not, for insert() to fix
     $new->{_rel_in_storage} = 1;
@@ -235,7 +235,7 @@ sub new {
       }
       $new->throw_exception("No such column $key on $class")
         unless $class->has_column($key);
-      $new->store_column($key => $attrs->{$key});          
+      $new->store_column($key => $attrs->{$key});
     }
 
     $new->{_relationship_data} = $related if $related;
@@ -283,7 +283,7 @@ sub insert {
   my $rollback_guard;
 
   # Check if we stored uninserted relobjs here in new()
-  my %related_stuff = (%{$self->{_relationship_data} || {}}, 
+  my %related_stuff = (%{$self->{_relationship_data} || {}},
                        %{$self->{_inflated_column} || {}});
 
   if(!$self->{_rel_in_storage}) {
@@ -332,7 +332,7 @@ sub insert {
 
   ## PK::Auto
   my @auto_pri = grep {
-                   !defined $self->get_column($_) || 
+                   !defined $self->get_column($_) ||
                    ref($self->get_column($_)) eq 'SCALAR'
                  } $self->primary_columns;
 
@@ -413,7 +413,7 @@ 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. 
+are used.
 
 Creating a row object using L<DBIx::Class::ResultSet/new>, or calling
 L</delete> on one, sets it to false.
@@ -519,14 +519,14 @@ values to locate the row.
 
 The object is still perfectly usable, but L</in_storage> will
 now return 0 and the object must be reinserted using L</insert>
-before it can be used to L</update> the row again. 
+before it can be used to L</update> the row again.
 
 If you delete an object in a class with a C<has_many> relationship, an
 attempt is made to delete all the related objects as well. To turn
 this behaviour off, pass C<< cascade_delete => 0 >> in the C<$attr>
 hashref of the relationship, see L<DBIx::Class::Relationship>. Any
 database-level cascade or restrict will take precedence over a
-DBIx-Class-based cascading delete. 
+DBIx-Class-based cascading delete.
 
 If you delete an object within a txn_do() (see L<DBIx::Class::Storage/txn_do>)
 and the transaction subsequently fails, the row object will remain marked as
@@ -600,7 +600,7 @@ sub get_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}));   
+      $self->_deflated_column($column, $self->{_inflated_column}{$column}));
   }
   $self->throw_exception( "No such column '${column}'" ) unless $self->has_column($column);
   return undef;
@@ -647,6 +647,8 @@ sub has_column_loaded {
 Returns all loaded column data as a hash, containing raw values. To
 get just one value for a particular column, use L</get_column>.
 
+See L</get_inflated_columns> to get the inflated values.
+
 =cut
 
 sub get_columns {
@@ -700,7 +702,7 @@ sub get_dirty_columns {
 Throws an exception if the column does not exist.
 
 Marks a column as having been changed regardless of whether it has
-really changed.  
+really changed.
 
 =cut
 sub make_column_dirty {
@@ -708,7 +710,21 @@ sub make_column_dirty {
 
   $self->throw_exception( "No such column '${column}'" )
     unless exists $self->{_column_data}{$column} || $self->has_column($column);
+
+  # the entire clean/dirty code relies on exists, not on true/false
+  return 1 if exists $self->{_dirty_columns}{$column};
+
   $self->{_dirty_columns}{$column} = 1;
+
+  # if we are just now making the column dirty, and if there is an inflated
+  # value, force it over the deflated one
+  if (exists $self->{_inflated_column}{$column}) {
+    $self->store_column($column,
+      $self->_deflated_column(
+        $column, $self->{_inflated_column}{$column}
+      )
+    );
+  }
 }
 
 =head2 get_inflated_columns
@@ -771,7 +787,10 @@ sub set_column {
   $self->store_column($column, $new_value);
 
   my $dirty;
-  if (defined $old_value xor defined $new_value) {
+  if (!$self->in_storage) { # no point tracking dirtyness on uninserted data
+    $dirty = 1;
+  }
+  elsif (defined $old_value xor defined $new_value) {
     $dirty = 1;
   }
   elsif (not defined $old_value) {  # both undef
@@ -783,8 +802,8 @@ sub set_column {
   else {  # do a numeric comparison if datatype allows it
     my $colinfo = $self->column_info ($column);
 
-    # cache for speed
-    if (not defined $colinfo->{is_numeric}) {
+    # cache for speed (the object may *not* have a resultsource instance)
+    if (not defined $colinfo->{is_numeric} && $self->_source_handle) {
       $colinfo->{is_numeric} =
         $self->result_source->schema->storage->is_datatype_numeric ($colinfo->{data_type})
           ? 1
@@ -813,7 +832,7 @@ sub set_column {
 
   $row->set_columns({ $col => $val, ... });
 
-=over 
+=over
 
 =item Arguments: \%columndata
 
@@ -848,7 +867,7 @@ sub set_columns {
 =back
 
 Sets more than one column value at once. Any inflated values are
-deflated and the raw values stored. 
+deflated and the raw values stored.
 
 Any related values passed as Row objects, using the relation name as a
 key, are reduced to the appropriate foreign key values and stored. If
@@ -892,7 +911,7 @@ sub set_inflated_columns {
       }
     }
   }
-  $self->set_columns($upd);    
+  $self->set_columns($upd);
 }
 
 =head2 copy
@@ -909,7 +928,11 @@ sub set_inflated_columns {
 
 Inserts a new row into the database, as a copy of the original
 object. If a hashref of replacement data is supplied, these will take
-precedence over data in the original.
+precedence over data in the original. Also any columns which have
+the L<column info attribute|DBIx::Class::ResultSource/add_columns>
+C<< is_auto_increment => 1 >> are explicitly removed before the copy,
+so that the database can insert its own autoincremented values into
+the new object.
 
 Relationships will be followed by the copy procedure B<only> if the
 relationship specifes a true value for its
@@ -934,7 +957,7 @@ sub copy {
   $new->set_inflated_columns($changes);
   $new->insert;
 
-  # Its possible we'll have 2 relations to the same Source. We need to make 
+  # 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 esle we'll violate unique
   # constraints
   my $rels_copied = {};
@@ -943,7 +966,7 @@ sub copy {
     my $rel_info = $self->result_source->relationship_info($rel);
 
     next unless $rel_info->{attrs}{cascade_copy};
-  
+
     my $resolved = $self->result_source->_resolve_condition(
       $rel_info->{cond}, $rel, $new
     );
@@ -955,7 +978,7 @@ sub copy {
       $copied->{$id_str} = 1;
       my $rel_copy = $related->copy($resolved);
     }
+
   }
   return $new;
 }
@@ -1030,7 +1053,6 @@ sub inflate_result {
   my $new = {
     _source_handle => $source_handle,
     _column_data => $me,
-    _in_storage => 1
   };
   bless $new, (ref $class || $class);
 
@@ -1042,14 +1064,25 @@ sub inflate_result {
       unless $pre_source;
     if (ref($pre_val->[0]) eq 'ARRAY') { # multi
       my @pre_objects;
-      foreach my $pre_rec (@$pre_val) {
-        unless ($pre_source->primary_columns == grep { exists $pre_rec->[0]{$_}
-           and defined $pre_rec->[0]{$_} } $pre_source->primary_columns) {
-          next;
+
+      for my $me_pref (@$pre_val) {
+
+        # the collapser currently *could* return bogus elements with all
+        # columns set to undef
+        my $has_def;
+        for (values %{$me_pref->[0]}) {
+          if (defined $_) {
+            $has_def++;
+            last;
+          }
         }
-        push(@pre_objects, $pre_source->result_class->inflate_result(
-                             $pre_source, @{$pre_rec}));
+        next unless $has_def;
+
+        push @pre_objects, $pre_source->result_class->inflate_result(
+          $pre_source, @$me_pref
+        );
       }
+
       $new->related_resultset($pre)->set_cache(\@pre_objects);
     } elsif (defined $pre_val->[0]) {
       my $fetched;
@@ -1067,11 +1100,13 @@ sub inflate_result {
       } elsif ($accessor eq 'filter') {
         $new->{_inflated_column}{$pre} = $fetched;
       } else {
-       $class->throw_exception("Prefetch not supported with accessor '$accessor'");
+       $class->throw_exception("Implicit prefetch (via select/columns) not supported with accessor '$accessor'");
       }
       $new->related_resultset($pre)->set_cache([ $fetched ]);
     }
   }
+
+  $new->in_storage (1);
   return $new;
 }
 
@@ -1240,11 +1275,11 @@ sub get_from_storage {
     my $self = shift @_;
     my $attrs = shift @_;
     my $resultset = $self->result_source->resultset;
-    
+
     if(defined $attrs) {
        $resultset = $resultset->search(undef, $attrs);
     }
-    
+
     return $resultset->find($self->{_orig_ident} || $self->ident_condition);
 }
 
@@ -1297,6 +1332,13 @@ This method can also be used to refresh from storage, retrieving any
 changes made since the row was last read from storage. Actually
 implemented in L<DBIx::Class::PK>
 
+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.
+
 =cut
 
 1;
index a454cd5..a1e6d1c 100644 (file)
@@ -1,6 +1,10 @@
 package # Hide from PAUSE
   DBIx::Class::SQLAHacks;
 
+# This module is a subclass of SQL::Abstract::Limit and includes a number
+# of DBIC-specific workarounds, not yet suitable for inclusion into the
+# SQLA core
+
 use base qw/SQL::Abstract::Limit/;
 use strict;
 use warnings;
@@ -12,12 +16,13 @@ BEGIN {
   no warnings qw/redefine/;
   no strict qw/refs/;
   for my $f (qw/carp croak/) {
+
     my $orig = \&{"SQL::Abstract::$f"};
     *{"SQL::Abstract::$f"} = sub {
 
       local $Carp::CarpLevel = 1;   # even though Carp::Clan ignores this, $orig will not
 
-      if (Carp::longmess() =~ /DBIx::Class::SQLAHacks::[\w]+\(\) called/) {
+      if (Carp::longmess() =~ /DBIx::Class::SQLAHacks::[\w]+ .+? called \s at/x) {
         __PACKAGE__->can($f)->(@_);
       }
       else {
@@ -27,6 +32,9 @@ BEGIN {
   }
 }
 
+
+# Tries to determine limit dialect.
+#
 sub new {
   my $self = shift->SUPER::new(@_);
 
@@ -39,14 +47,12 @@ sub new {
   $self;
 }
 
-
 # Some databases (sqlite) do not handle multiple parenthesis
-# around in/between arguments. A tentative x IN ( ( 1, 2 ,3) )
+# around in/between arguments. A tentative x IN ( (1, 2 ,3) )
 # is interpreted as x IN 1 or something similar.
 #
 # Since we currently do not have access to the SQLA AST, resort
 # to barbaric mutilation of any SQL supplied in literal form
-
 sub _strip_outer_paren {
   my ($self, $arg) = @_;
 
@@ -116,37 +122,190 @@ SQL
 sub _Top {
   my ( $self, $sql, $order, $rows, $offset ) = @_;
 
+  # mangle the input sql so it can be properly aliased in the outer queries
+  $sql =~ s/^ \s* SELECT \s+ (.+?) \s+ (?=FROM)//ix
+    or croak "Unrecognizable SELECT: $sql";
+  my $sql_select = $1;
+  my @sql_select = split (/\s*,\s*/, $sql_select);
+
+  # we can't support subqueries (in fact MSSQL can't) - croak
+  if (@sql_select != @{$self->{_dbic_rs_attrs}{select}}) {
+    croak (sprintf (
+      'SQL SELECT did not parse cleanly - retrieved %d comma separated elements, while '
+    . 'the resultset select attribure contains %d elements: %s',
+      scalar @sql_select,
+      scalar @{$self->{_dbic_rs_attrs}{select}},
+      $sql_select,
+    ));
+  }
+
+  my $name_sep = $self->name_sep || '.';
+  my $esc_name_sep = "\Q$name_sep\E";
+  my $col_re = qr/ ^ (?: (.+) $esc_name_sep )? ([^$esc_name_sep]+) $ /x;
+
+  my $rs_alias = $self->{_dbic_rs_attrs}{alias};
+  my $quoted_rs_alias = $self->_quote ($rs_alias);
+
+  # construct the new select lists, rename(alias) some columns if necessary
+  my (@outer_select, @inner_select, %seen_names, %col_aliases, %outer_col_aliases);
+
+  for (@{$self->{_dbic_rs_attrs}{select}}) {
+    next if ref $_;
+    my ($table, $orig_colname) = ( $_ =~ $col_re );
+    next unless $table;
+    $seen_names{$orig_colname}++;
+  }
+
+  for my $i (0 .. $#sql_select) {
+
+    my $colsel_arg = $self->{_dbic_rs_attrs}{select}[$i];
+    my $colsel_sql = $sql_select[$i];
+
+    # this may or may not work (in case of a scalarref or something)
+    my ($table, $orig_colname) = ( $colsel_arg =~ $col_re );
+
+    my $quoted_alias;
+    # do not attempt to understand non-scalar selects - alias numerically
+    if (ref $colsel_arg) {
+      $quoted_alias = $self->_quote ('column_' . (@inner_select + 1) );
+    }
+    # column name seen more than once - alias it
+    elsif ($orig_colname && ($seen_names{$orig_colname} > 1) ) {
+      $quoted_alias = $self->_quote ("${table}__${orig_colname}");
+    }
+
+    # we did rename - make a record and adjust
+    if ($quoted_alias) {
+      # alias inner
+      push @inner_select, "$colsel_sql AS $quoted_alias";
+
+      # push alias to outer
+      push @outer_select, $quoted_alias;
+
+      # Any aliasing accumulated here will be considered
+      # both for inner and outer adjustments of ORDER BY
+      $self->__record_alias (
+        \%col_aliases,
+        $quoted_alias,
+        $colsel_arg,
+        $table ? $orig_colname : undef,
+      );
+    }
+
+    # otherwise just leave things intact inside, and use the abbreviated one outside
+    # (as we do not have table names anymore)
+    else {
+      push @inner_select, $colsel_sql;
+
+      my $outer_quoted = $self->_quote ($orig_colname);  # it was not a duplicate so should just work
+      push @outer_select, $outer_quoted;
+      $self->__record_alias (
+        \%outer_col_aliases,
+        $outer_quoted,
+        $colsel_arg,
+        $table ? $orig_colname : undef,
+      );
+    }
+  }
+
+  my $outer_select = join (', ', @outer_select );
+  my $inner_select = join (', ', @inner_select );
+
+  %outer_col_aliases = (%outer_col_aliases, %col_aliases);
+
+  # deal with order
   croak '$order supplied to SQLAHacks limit emulators must be a hash'
     if (ref $order ne 'HASH');
 
   $order = { %$order }; #copy
 
-  my $last = $rows + $offset;
+  my $req_order = $order->{order_by};
 
-  my $req_order = $self->_order_by ($order->{order_by});
+  # examine normalized version, collapses nesting
+  my $limit_order;
+  if (scalar $self->_order_by_chunks ($req_order)) {
+    $limit_order = $req_order;
+  }
+  else {
+    $limit_order = [ map
+      { join ('', $rs_alias, $name_sep, $_ ) }
+      ( $self->{_dbic_rs_attrs}{_source_handle}->resolve->primary_columns )
+    ];
+  }
 
-  my $limit_order = $req_order ? $order->{order_by} : $order->{_virtual_order_by};
+  my ( $order_by_inner, $order_by_outer ) = $self->_order_directions($limit_order);
+  my $order_by_requested = $self->_order_by ($req_order);
 
-  delete $order->{$_} for qw/order_by _virtual_order_by/;
+  # generate the rest
+  delete $order->{order_by};
   my $grpby_having = $self->_order_by ($order);
 
-  my ( $order_by_inner, $order_by_outer ) = $self->_order_directions($limit_order);
+  # short circuit for counts - the ordering complexity is needless
+  if ($self->{_dbic_rs_attrs}{-for_count_only}) {
+    return "SELECT TOP $rows $inner_select $sql $grpby_having $order_by_outer";
+  }
+
+  # we can't really adjust the order_by columns, as introspection is lacking
+  # resort to simple substitution
+  for my $col (keys %outer_col_aliases) {
+    for ($order_by_requested, $order_by_outer) {
+      $_ =~ s/\s+$col\s+/ $outer_col_aliases{$col} /g;
+    }
+  }
+  for my $col (keys %col_aliases) {
+    $order_by_inner =~ s/\s+$col\s+/ $col_aliases{$col} /g;
+  }
 
-  $sql =~ s/^\s*(SELECT|select)//;
 
-  $sql = <<"SQL";
-  SELECT * FROM
-  (
-    SELECT TOP $rows * FROM
+  my $inner_lim = $rows + $offset;
+
+  $sql = "SELECT TOP $inner_lim $inner_select $sql $grpby_having $order_by_inner";
+
+  if ($offset) {
+    $sql = <<"SQL";
+
+    SELECT TOP $rows $outer_select FROM
     (
-        SELECT TOP $last $sql $grpby_having $order_by_inner
-    ) AS foo
+      $sql
+    ) $quoted_rs_alias
     $order_by_outer
-  ) AS bar
-  $req_order
+SQL
+
+  }
+
+  if ($order_by_requested) {
+    $sql = <<"SQL";
 
+    SELECT $outer_select FROM
+      ( $sql ) $quoted_rs_alias
+    $order_by_requested
 SQL
-    return $sql;
+
+  }
+
+  $sql =~ s/\s*\n\s*/ /g; # parsing out multiline statements is harder than a single line
+  return $sql;
+}
+
+# action at a distance to shorten Top code above
+sub __record_alias {
+  my ($self, $register, $alias, $fqcol, $col) = @_;
+
+  # record qualified name
+  $register->{$fqcol} = $alias;
+  $register->{$self->_quote($fqcol)} = $alias;
+
+  return unless $col;
+
+  # record unqualified name, undef (no adjustment) if a duplicate is found
+  if (exists $register->{$col}) {
+    $register->{$col} = undef;
+  }
+  else {
+    $register->{$col} = $alias;
+  }
+
+  $register->{$self->_quote($col)} = $register->{$col};
 }
 
 
@@ -158,6 +317,12 @@ sub _find_syntax {
   return $self->{_cached_syntax} ||= $self->SUPER::_find_syntax($syntax);
 }
 
+my $for_syntax = {
+  update => 'FOR UPDATE',
+  shared => 'FOR SHARE',
+};
+# Quotes table names, handles "limit" dialects (e.g. where rownum between x and
+# y), supports SELECT ... FOR UPDATE and SELECT ... FOR SHARE.
 sub select {
   my ($self, $table, $fields, $where, $order, @rest) = @_;
 
@@ -177,18 +342,14 @@ sub select {
   my ($sql, @where_bind) = $self->SUPER::select(
     $table, $self->_recurse_fields($fields), $where, $order, @rest
   );
-  $sql .= 
-    $self->{for} ?
-    (
-      $self->{for} eq 'update' ? ' FOR UPDATE' :
-      $self->{for} eq 'shared' ? ' FOR SHARE'  :
-      ''
-    ) :
-    ''
-  ;
+  if (my $for = delete $self->{_dbic_rs_attrs}{for}) {
+    $sql .= " $for_syntax->{$for}" if $for_syntax->{$for};
+  }
+
   return wantarray ? ($sql, @{$self->{from_bind}}, @where_bind, @{$self->{having_bind}}, @{$self->{order_bind}} ) : $sql;
 }
 
+# Quotes table names, and handles default inserts
 sub insert {
   my $self = shift;
   my $table = shift;
@@ -204,6 +365,7 @@ sub insert {
   $self->SUPER::insert($table, @_);
 }
 
+# Just quotes table names.
 sub update {
   my $self = shift;
   my $table = shift;
@@ -211,6 +373,7 @@ sub update {
   $self->SUPER::update($table, @_);
 }
 
+# Just quotes table names.
 sub delete {
   my $self = shift;
   my $table = shift;
@@ -240,28 +403,39 @@ sub _recurse_fields {
           ? ' AS col'.$self->{rownum_hack_count}++
           : '')
       } @$fields);
-  } elsif ($ref eq 'HASH') {
-    foreach my $func (keys %$fields) {
-      if ($func eq 'distinct') {
-        my $_fields = $fields->{$func};
-        if (ref $_fields eq 'ARRAY' && @{$_fields} > 1) {
-          croak (
-            'The select => { distinct => ... } syntax is not supported for multiple columns.'
-           .' Instead please use { group_by => [ qw/' . (join ' ', @$_fields) . '/ ] }'
-           .' or { select => [ qw/' . (join ' ', @$_fields) . '/ ], distinct => 1 }'
-          );
-        }
-        else {
-          $_fields = @{$_fields}[0] if ref $_fields eq 'ARRAY';
-          carp (
-            'The select => { distinct => ... } syntax will be deprecated in DBIC version 0.09,'
-           ." please use { group_by => '${_fields}' } or { select => '${_fields}', distinct => 1 }"
-          );
-        }
+  }
+  elsif ($ref eq 'HASH') {
+    my %hash = %$fields;
+    my ($select, $as);
+
+    if ($hash{-select}) {
+      $select = $self->_recurse_fields (delete $hash{-select});
+      $as = $self->_quote (delete $hash{-as});
+    }
+    else {
+      my ($func, $args) = each %hash;
+      delete $hash{$func};
+
+      if (lc ($func) eq 'distinct' && ref $args eq 'ARRAY' && @$args > 1) {
+        croak (
+          '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 }'
+        );
       }
-      return $self->_sqlcase($func)
-        .'( '.$self->_recurse_fields($fields->{$func}).' )';
+      $select = sprintf ('%s( %s )',
+        $self->_sqlcase($func),
+        $self->_recurse_fields($args)
+      );
     }
+
+    # there should be nothing left
+    if (keys %hash) {
+      croak "Malformed select argument - too many keys in hash: " . join (',', keys %$fields );
+    }
+
+    $select .= " AS $as" if $as;
+    return $select;
   }
   # Is the second check absolutely necessary?
   elsif ( $ref eq 'REF' and ref($$fields) eq 'ARRAY' ) {
@@ -279,9 +453,8 @@ sub _order_by {
 
     my $ret = '';
 
-    if (defined $arg->{group_by}) {
-      $ret = $self->_sqlcase(' group by ')
-        .$self->_recurse_fields($arg->{group_by}, { no_rownum_hack => 1 });
+    if (my $g = $self->_recurse_fields($arg->{group_by}, { no_rownum_hack => 1 }) ) {
+      $ret = $self->_sqlcase(' group by ') . $g;
     }
 
     if (defined $arg->{having}) {
@@ -429,12 +602,15 @@ sub limit_dialect {
     return $self->{limit_dialect};
 }
 
+# Set to an array-ref to specify separate left and right quotes for table names.
+# A single scalar is equivalen to [ $char, $char ]
 sub quote_char {
     my $self = shift;
     $self->{quote_char} = shift if @_;
     return $self->{quote_char};
 }
 
+# Character separating quoted table names.
 sub name_sep {
     my $self = shift;
     $self->{name_sep} = shift if @_;
@@ -442,50 +618,3 @@ sub name_sep {
 }
 
 1;
-
-__END__
-
-=pod
-
-=head1 NAME
-
-DBIx::Class::SQLAHacks - This module is a subclass of SQL::Abstract::Limit
-and includes a number of DBIC-specific workarounds, not yet suitable for
-inclusion into SQLA proper.
-
-=head1 METHODS
-
-=head2 new
-
-Tries to determine limit dialect.
-
-=head2 select
-
-Quotes table names, handles "limit" dialects (e.g. where rownum between x and
-y), supports SELECT ... FOR UPDATE and SELECT ... FOR SHARE.
-
-=head2 insert update delete
-
-Just quotes table names.
-
-=head2 limit_dialect
-
-Specifies the dialect of used for implementing an SQL "limit" clause for
-restricting the number of query results returned.  Valid values are: RowNum.
-
-See L<DBIx::Class::Storage::DBI/connect_info> for details.
-
-=head2 name_sep
-
-Character separating quoted table names.
-
-See L<DBIx::Class::Storage::DBI/connect_info> for details.
-
-=head2 quote_char
-
-Set to an array-ref to specify separate left and right quotes for table names.
-
-See L<DBIx::Class::Storage::DBI/connect_info> for details.
-
-=cut
-
diff --git a/lib/DBIx/Class/SQLAHacks/MSSQL.pm b/lib/DBIx/Class/SQLAHacks/MSSQL.pm
new file mode 100644 (file)
index 0000000..1b18b1e
--- /dev/null
@@ -0,0 +1,33 @@
+package # Hide from PAUSE
+  DBIx::Class::SQLAHacks::MSSQL;
+
+use base qw( DBIx::Class::SQLAHacks );
+use Carp::Clan qw/^DBIx::Class|^SQL::Abstract/;
+
+#
+# MSSQL is retarded wrt TOP (crappy limit) and ordering.
+# One needs to add a TOP to *all* ordered subqueries, if
+# TOP has been used in the statement at least once.
+# Do it here.
+#
+sub select {
+  my $self = shift;
+
+  my ($sql, @bind) = $self->SUPER::select (@_);
+
+  # ordering was requested and there are at least 2 SELECT/FROM pairs
+  # (thus subquery), and there is no TOP specified
+  if (
+    $sql =~ /\bSELECT\b .+? \bFROM\b .+? \bSELECT\b .+? \bFROM\b/isx
+      &&
+    $sql !~ /^ \s* SELECT \s+ TOP \s+ \d+ /xi
+      &&
+    scalar $self->_order_by_chunks ($_[3]->{order_by})
+  ) {
+    $sql =~ s/^ \s* SELECT \s/SELECT TOP 100 PERCENT /xi;
+  }
+
+  return wantarray ? ($sql, @bind) : $sql;
+}
+
+1;
index 4b945cc..b37ec5e 100644 (file)
@@ -8,7 +8,7 @@ use Carp::Clan qw/^DBIx::Class/;
 use Scalar::Util qw/weaken/;
 use File::Spec;
 use Sub::Name ();
-require Module::Find;
+use Module::Find();
 
 use base qw/DBIx::Class/;
 
@@ -239,16 +239,29 @@ sub load_namespaces {
     local *Class::C3::reinitialize = sub { };
     use warnings 'redefine';
 
-    # ensure classes are loaded and fetch properly sorted classes
+    # ensure classes are loaded and attached in inheritance order
     $class->ensure_class_loaded($_) foreach(values %results);
-    my @subclass_last = sort { $results{$a}->isa($results{$b}) } keys(%results);
-    
+    my %inh_idx;
+    my @subclass_last = sort {
+
+      ($inh_idx{$a} ||=
+        scalar @{mro::get_linear_isa( $results{$a} )}
+      )
+
+          <=>
+
+      ($inh_idx{$b} ||=
+        scalar @{mro::get_linear_isa( $results{$b} )}
+      )
+
+    } keys(%results);
+
     foreach my $result (@subclass_last) {
       my $result_class = $results{$result};
 
       my $rs_class = delete $resultsets{$result};
       my $rs_set = $class->_ns_get_rsrc_instance ($result_class)->resultset_class;
-      
+
       if($rs_set && $rs_set ne 'DBIx::Class::ResultSet') {
         if($rs_class && $rs_class ne $rs_set) {
           carp "We found ResultSet class '$rs_class' for '$result', but it seems "
@@ -285,8 +298,10 @@ sub load_namespaces {
 
 =back
 
-Alternative method to L</load_namespaces> which you should look at
-using if you can.
+L</load_classes> is an alternative method to L</load_namespaces>, both of
+which serve similar purposes, each with different advantages and disadvantages.
+In the general case you should use L</load_namespaces>, unless you need to
+be able to specify that only specific classes are loaded at runtime.
 
 With no arguments, this method uses L<Module::Find> to find all classes under
 the schema's namespace. Otherwise, this method loads the classes you specify
@@ -496,7 +511,7 @@ syntax on the C<@connectinfo> argument, or L<DBIx::Class::Storage> in
 general.
 
 Note that C<connect_info> expects an arrayref of arguments, but
-C<connect> does not. C<connect> wraps it's arguments in an arrayref
+C<connect> does not. C<connect> wraps its arguments in an arrayref
 before passing them to C<connect_info>.
 
 =head3 Overloading
@@ -740,7 +755,7 @@ i.e.,
     [ 2, 'Indie Band' ],
     ...
   ]);
-  
+
 Since wantarray context is basically the same as looping over $rs->create(...) 
 you won't see any performance benefits and in this case the method is more for
 convenience. Void context sends the column information directly to storage
@@ -791,10 +806,10 @@ Overload C<connection> to change the behaviour of C<connect>.
 sub connection {
   my ($self, @info) = @_;
   return $self if !@info && $self->storage;
-  
+
   my ($storage_class, $args) = ref $self->storage_type ? 
     ($self->_normalize_storage_type($self->storage_type),{}) : ($self->storage_type, {});
-    
+
   $storage_class = 'DBIx::Class::Storage'.$storage_class
     if $storage_class =~ m/^::/;
   eval "require ${storage_class};";
@@ -1109,6 +1124,19 @@ name format is: C<$dir$schema-$version-$type.sql>.
 You may override this method in your schema if you wish to use a different
 format.
 
+ WARNING
+
+ Prior to DBIx::Class version 0.08100 this method had a different signature:
+
+    my $filename = $table->ddl_filename($type, $dir, $version, $preversion)
+
+ In recent versions variables $dir and $version were reversed in order to
+ bring the signature in line with other Schema/Storage methods. If you 
+ really need to maintain backward compatibility, you can do the following
+ in any overriding methods:
+
+    ($dir, $version) = ($version, $dir) if ($DBIx::Class::VERSION < 0.08100);
+
 =cut
 
 sub ddl_filename {
@@ -1118,7 +1146,7 @@ sub ddl_filename {
   $filename =~ s/::/-/g;
   $filename = File::Spec->catfile($dir, "$filename-$version-$type.sql");
   $filename =~ s/$version/$preversion-$version/ if($preversion);
-  
+
   return $filename;
 }
 
@@ -1344,7 +1372,7 @@ more information.
     $self->throw_exception
       ("No arguments to load_classes and couldn't load ${base} ($@)")
         if $@;
-  
+
     if ($self eq $target) {
       # Pathological case, largely caused by the docs on early C::M::DBIC::Plain
       foreach my $moniker ($self->sources) {
@@ -1357,14 +1385,14 @@ more information.
       $self->connection(@info);
       return $self;
     }
-  
+
     my $schema = $self->compose_namespace($target, $base);
     {
       no strict 'refs';
       my $name = join '::', $target, 'schema';
       *$name = Sub::Name::subname $name, sub { $schema };
     }
-  
+
     $schema->connection(@info);
     foreach my $moniker ($schema->sources) {
       my $source = $schema->source($moniker);
index 49baa57..0874167 100644 (file)
@@ -308,7 +308,7 @@ sub upgrade
   # here to be sure.
   # XXX - just fix it
   $self->storage->sqlt_type;
-  
+
   my $upgrade_file = $self->ddl_filename(
                                          $self->storage->sqlt_type,
                                          $self->schema_version,
index 6a34606..30bc51f 100644 (file)
@@ -7,7 +7,7 @@ DBIx::Class::StartupCheck - Run environment checks on startup
 =head1 SYNOPSIS
 
   use DBIx::Class::StartupCheck;
-  
+
 =head1 DESCRIPTION
 
 This module used to check for, and if necessary issue a warning for, a
index a38cf47..e5c7d45 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use warnings;
 
 use base qw/DBIx::Class/;
+use mro 'c3';
 
 use Scalar::Util qw/weaken/;
 use Carp::Clan qw/^DBIx::Class/;
@@ -248,6 +249,9 @@ sub txn_begin { die "Virtual method!" }
 
 Issues a commit of the current transaction.
 
+It does I<not> perform an actual storage commit unless there's a DBIx::Class
+transaction currently in effect (i.e. you called L</txn_begin>).
+
 =cut
 
 sub txn_commit { die "Virtual method!" }
index c6f13f5..5a97bb3 100644 (file)
@@ -1,10 +1,12 @@
 package DBIx::Class::Storage::DBI;
 # -*- mode: cperl; cperl-indent-level: 2 -*-
 
+use strict;
+use warnings;
+
 use base 'DBIx::Class::Storage';
+use mro 'c3';
 
-use strict;    
-use warnings;
 use Carp::Clan qw/^DBIx::Class/;
 use DBI;
 use DBIx::Class::Storage::DBI::Cursor;
@@ -13,14 +15,15 @@ use Scalar::Util();
 use List::Util();
 
 __PACKAGE__->mk_group_accessors('simple' =>
-    qw/_connect_info _dbi_connect_info _dbh _sql_maker _sql_maker_opts
-       _conn_pid _conn_tid transaction_depth _dbh_autocommit savepoints/
+  qw/_connect_info _dbi_connect_info _dbh _sql_maker _sql_maker_opts
+     _conn_pid _conn_tid transaction_depth _dbh_autocommit savepoints/
 );
 
 # the values for these accessors are picked out (and deleted) from
 # the attribute hashref passed to connect_info
 my @storage_options = qw/
-  on_connect_do on_disconnect_do disable_sth_caching unsafe auto_savepoint
+  on_connect_call on_disconnect_call on_connect_do on_disconnect_do
+  disable_sth_caching unsafe auto_savepoint
 /;
 __PACKAGE__->mk_group_accessors('simple' => @storage_options);
 
@@ -89,8 +92,8 @@ recognized by DBIx::Class:
 
 =item *
 
-A single code reference which returns a connected 
-L<DBI database handle|DBI/connect> optionally followed by 
+A single code reference which returns a connected
+L<DBI database handle|DBI/connect> optionally followed by
 L<extra attributes|/DBIx::Class specific connection attributes> recognized
 by DBIx::Class:
 
@@ -109,7 +112,7 @@ mixed together:
     %extra_attributes,
   }];
 
-This is particularly useful for L<Catalyst> based applications, allowing the 
+This is particularly useful for L<Catalyst> based applications, allowing the
 following config (L<Config::General> style):
 
   <Model::DB>
@@ -128,7 +131,7 @@ Please note that the L<DBI> docs recommend that you always explicitly
 set C<AutoCommit> to either I<0> or I<1>.  L<DBIx::Class> further
 recommends that it be set to I<1>, and that you perform transactions
 via our L<DBIx::Class::Schema/txn_do> method.  L<DBIx::Class> will set it
-to I<1> if you do not do explicitly set it to zero.  This is the default 
+to I<1> if you do not do explicitly set it to zero.  This is the default
 for most DBDs. See L</DBIx::Class and AutoCommit> for details.
 
 =head3 DBIx::Class specific connection attributes
@@ -177,12 +180,97 @@ immediately before disconnecting from the database.
 Note, this only runs if you explicitly call L</disconnect> on the
 storage object.
 
+=item on_connect_call
+
+A more generalized form of L</on_connect_do> that calls the specified
+C<connect_call_METHOD> methods in your storage driver.
+
+  on_connect_do => 'select 1'
+
+is equivalent to:
+
+  on_connect_call => [ [ do_sql => 'select 1' ] ]
+
+Its values may contain:
+
+=over
+
+=item a scalar
+
+Will call the C<connect_call_METHOD> method.
+
+=item a code reference
+
+Will execute C<< $code->($storage) >>
+
+=item an array reference
+
+Each value can be a method name or code reference.
+
+=item an array of arrays
+
+For each array, the first item is taken to be the C<connect_call_> method name
+or code reference, and the rest are parameters to it.
+
+=back
+
+Some predefined storage methods you may use:
+
+=over
+
+=item do_sql
+
+Executes a SQL string or a code reference that returns a SQL string. This is
+what L</on_connect_do> and L</on_disconnect_do> use.
+
+It can take:
+
+=over
+
+=item a scalar
+
+Will execute the scalar as SQL.
+
+=item an arrayref
+
+Taken to be arguments to L<DBI/do>, the SQL string optionally followed by the
+attributes hashref and bind values.
+
+=item a code reference
+
+Will execute C<< $code->($storage) >> and execute the return array refs as
+above.
+
+=back
+
+=item datetime_setup
+
+Execute any statements necessary to initialize the database session to return
+and accept datetime/timestamp values used with
+L<DBIx::Class::InflateColumn::DateTime>.
+
+Only necessary for some databases, see your specific storage driver for
+implementation details.
+
+=back
+
+=item on_disconnect_call
+
+Takes arguments in the same form as L</on_connect_call> and executes them
+immediately before disconnecting from the database.
+
+Calls the C<disconnect_call_METHOD> methods as opposed to the
+C<connect_call_METHOD> methods called by L</on_connect_call>.
+
+Note, this only runs if you explicitly call L</disconnect> on the
+storage object.
+
 =item disable_sth_caching
 
 If set to a true value, this option will disable the caching of
 statement handles via L<DBI/prepare_cached>.
 
-=item limit_dialect 
+=item limit_dialect
 
 Sets the limit dialect. This is useful for JDBC-bridge among others
 where the remote SQL-dialect cannot be determined by the name of the
@@ -190,7 +278,7 @@ driver alone. See also L<SQL::Abstract::Limit>.
 
 =item quote_char
 
-Specifies what characters to use to quote table and column names. If 
+Specifies what characters to use to quote table and column names. If
 you use this you will want to specify L</name_sep> as well.
 
 C<quote_char> expects either a single character, in which case is it
@@ -202,8 +290,8 @@ SQL Server you should use C<< quote_char => [qw/[ ]/] >>.
 
 =item name_sep
 
-This only needs to be used in conjunction with C<quote_char>, and is used to 
-specify the charecter that seperates elements (schemas, tables, columns) from 
+This only needs to be used in conjunction with C<quote_char>, and is used to
+specify the charecter that seperates elements (schemas, tables, columns) from
 each other. In most cases this is simply a C<.>.
 
 The consequences of not supplying this value is that L<SQL::Abstract>
@@ -359,6 +447,34 @@ sub connect_info {
 
 This method is deprecated in favour of setting via L</connect_info>.
 
+=cut
+
+=head2 on_disconnect_do
+
+This method is deprecated in favour of setting via L</connect_info>.
+
+=cut
+
+sub _parse_connect_do {
+  my ($self, $type) = @_;
+
+  my $val = $self->$type;
+  return () if not defined $val;
+
+  my @res;
+
+  if (not ref($val)) {
+    push @res, [ 'do_sql', $val ];
+  } elsif (ref($val) eq 'CODE') {
+    push @res, $val;
+  } elsif (ref($val) eq 'ARRAY') {
+    push @res, map { [ 'do_sql', $_ ] } @$val;
+  } else {
+    $self->throw_exception("Invalid type for $type: ".ref($val));
+  }
+
+  return \@res;
+}
 
 =head2 dbh_do
 
@@ -506,8 +622,12 @@ sub disconnect {
   my ($self) = @_;
 
   if( $self->connected ) {
-    my $connection_do = $self->on_disconnect_do;
-    $self->_do_connection_actions($connection_do) if ref($connection_do);
+    my @actions;
+
+    push @actions, ( $self->on_disconnect_call || () );
+    push @actions, $self->_parse_connect_do ('on_disconnect_do');
+
+    $self->_do_connection_actions(disconnect_call_ => $_) for @actions;
 
     $self->_dbh->rollback unless $self->_dbh_autocommit;
     $self->_dbh->disconnect;
@@ -551,12 +671,20 @@ sub connected {
           $self->_verify_pid;
           return 0 if !$self->_dbh;
       }
-      return ($dbh->FETCH('Active') && $dbh->ping);
+      return ($dbh->FETCH('Active') && $self->_ping);
   }
 
   return 0;
 }
 
+sub _ping {
+  my $self = shift;
+
+  my $dbh = $self->_dbh or return 0;
+
+  return $dbh->ping;
+}
+
 # handle pid changes correctly
 #  NOTE: assumes $self->_dbh is a valid $dbh
 sub _verify_pid {
@@ -594,7 +722,7 @@ sub dbh {
 
 sub _sql_maker_args {
     my ($self) = @_;
-    
+
     return ( bindtype=>'columns', array_datatypes => 1, limit_dialect => $self->dbh, %{$self->_sql_maker_opts} );
 }
 
@@ -624,8 +752,12 @@ sub _populate_dbh {
   #  there is no transaction in progress by definition
   $self->{transaction_depth} = $self->_dbh_autocommit ? 0 : 1;
 
-  my $connection_do = $self->on_connect_do;
-  $self->_do_connection_actions($connection_do) if $connection_do;
+  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;
 }
 
 sub _determine_driver {
@@ -642,33 +774,51 @@ sub _determine_driver {
       ($driver) = $self->_dbi_connect_info->[0] =~ /dbi:([^:]+):/i;
     }
 
-    if ($self->load_optional_class("DBIx::Class::Storage::DBI::${driver}")) {
-      bless $self, "DBIx::Class::Storage::DBI::${driver}";
+    my $storage_class = "DBIx::Class::Storage::DBI::${driver}";
+    if ($self->load_optional_class($storage_class)) {
+      mro::set_mro($storage_class, 'c3');
+      bless $self, $storage_class;
       $self->_rebless();
     }
   }
 }
 
 sub _do_connection_actions {
-  my $self = shift;
-  my $connection_do = shift;
-
-  if (!ref $connection_do) {
-    $self->_do_query($connection_do);
-  }
-  elsif (ref $connection_do eq 'ARRAY') {
-    $self->_do_query($_) foreach @$connection_do;
-  }
-  elsif (ref $connection_do eq 'CODE') {
-    $connection_do->($self);
-  }
-  else {
-    $self->throw_exception (sprintf ("Don't know how to process conection actions of type '%s'", ref $connection_do) );
+  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;
+    }
+  } else {
+    $self->throw_exception (sprintf ("Don't know how to process conection actions of type '%s'", ref($call)) );
   }
 
   return $self;
 }
 
+sub connect_call_do_sql {
+  my $self = shift;
+  $self->_do_query(@_);
+}
+
+sub disconnect_call_do_sql {
+  my $self = shift;
+  $self->_do_query(@_);
+}
+
+# override in db-specific backend when necessary
+sub connect_call_datetime_setup { 1 }
+
 sub _do_query {
   my ($self, $action) = @_;
 
@@ -753,11 +903,11 @@ sub svp_begin {
 
   $self->throw_exception ("Your Storage implementation doesn't support savepoints")
     unless $self->can('_svp_begin');
-  
+
   push @{ $self->{savepoints} }, $name;
 
   $self->debugobj->svp_begin($name) if $self->debug;
-  
+
   return $self->_svp_begin($name);
 }
 
@@ -817,7 +967,7 @@ sub svp_rollback {
   }
 
   $self->debugobj->svp_rollback($name) if $self->debug;
-  
+
   return $self->_svp_rollback($name);
 }
 
@@ -910,37 +1060,6 @@ sub _prep_for_execute {
   return ($sql, \@bind);
 }
 
-=head2 as_query
-
-=over 4
-
-=item Arguments: $rs_attrs
-
-=item Return Value: \[ $sql, @bind ]
-
-=back
-
-Returns the SQL statement and bind vars that would result from the given
-ResultSet attributes (does not actually run a query)
-
-=cut
-
-sub as_query {
-  my ($self, $rs_attr) = @_;
-
-  my $sql_maker = $self->sql_maker;
-  local $sql_maker->{for};
-
-  # my ($op, $bind, $ident, $bind_attrs, $select, $cond, $order, $rows, $offset) = $self->_select_args(...);
-  my @args = $self->_select_args($rs_attr->{from}, $rs_attr->{select}, $rs_attr->{where}, $rs_attr);
-
-  # my ($sql, $bind) = $self->_prep_for_execute($op, $bind, $ident, [ $select, $cond, $order, $rows, $offset ]);
-  my ($sql, $bind) = $self->_prep_for_execute(
-    @args[0 .. 2],
-    [ @args[4 .. $#args] ],
-  );
-  return \[ "($sql)", @{ $bind || [] }];
-}
 
 sub _fix_bind_params {
     my ($self, @bind) = @_;
@@ -986,7 +1105,7 @@ sub _dbh_execute {
 
   my $sth = $self->sth($sql,$op);
 
-  my $placeholder_index = 1; 
+  my $placeholder_index = 1;
 
   foreach my $bound (@$bind) {
     my $attributes = {};
@@ -1045,7 +1164,7 @@ sub insert {
 }
 
 ## Still not quite perfect, and EXPERIMENTAL
-## Currently it is assumed that all values passed will be "normal", i.e. not 
+## Currently it is assumed that all values passed will be "normal", i.e. not
 ## scalar refs, or at least, all the same type as the first set, the statement is
 ## only prepped once.
 sub insert_bulk {
@@ -1054,7 +1173,7 @@ sub insert_bulk {
   my $table = $source->from;
   @colvalues{@$cols} = (0..$#$cols);
   my ($sql, @bind) = $self->sql_maker->insert($table, \%colvalues);
-  
+
   $self->_query_start( $sql, @bind );
   my $sth = $self->sth($sql);
 
@@ -1067,7 +1186,7 @@ sub insert_bulk {
   my $bind_attributes = $self->source_bind_attributes($source);
 
   ## Bind the values and execute
-  my $placeholder_index = 1; 
+  my $placeholder_index = 1;
 
   foreach my $bound (@bind) {
 
@@ -1115,7 +1234,7 @@ sub update {
   my $self = shift @_;
   my $source = shift @_;
   my $bind_attributes = $self->source_bind_attributes($source);
-  
+
   return $self->_execute('update' => [], $source, $bind_attributes, @_);
 }
 
@@ -1123,9 +1242,9 @@ sub update {
 sub delete {
   my $self = shift @_;
   my $source = shift @_;
-  
+
   my $bind_attrs = $self->source_bind_attributes($source);
-  
+
   return $self->_execute('delete' => [], $source, $bind_attrs, @_);
 }
 
@@ -1222,28 +1341,59 @@ sub _per_row_update_delete {
 
 sub _select {
   my $self = shift;
+
+  # localization is neccessary as
+  # 1) there is no infrastructure to pass this around before SQLA2
+  # 2) _select_args sets it and _prep_for_execute consumes it
   my $sql_maker = $self->sql_maker;
-  local $sql_maker->{for};
+  local $sql_maker->{_dbic_rs_attrs};
+
   return $self->_execute($self->_select_args(@_));
 }
 
-sub _select_args {
-  my ($self, $ident, $select, $condition, $attrs) = @_;
+sub _select_args_to_query {
+  my $self = shift;
 
-  my $for = delete $attrs->{for};
+  # localization is neccessary as
+  # 1) there is no infrastructure to pass this around before SQLA2
+  # 2) _select_args sets it and _prep_for_execute consumes it
   my $sql_maker = $self->sql_maker;
-  $sql_maker->{for} = $for;
+  local $sql_maker->{_dbic_rs_attrs};
 
-  my $order = { map
-    { $attrs->{$_} ? ( $_ => $attrs->{$_} ) : ()  }
-    (qw/order_by group_by having _virtual_order_by/ )
-  };
+  # my ($op, $bind, $ident, $bind_attrs, $select, $cond, $order, $rows, $offset)
+  #  = $self->_select_args($ident, $select, $cond, $attrs);
+  my ($op, $bind, $ident, $bind_attrs, @args) =
+    $self->_select_args(@_);
 
+  # my ($sql, $prepared_bind) = $self->_prep_for_execute($op, $bind, $ident, [ $select, $cond, $order, $rows, $offset ]);
+  my ($sql, $prepared_bind) = $self->_prep_for_execute($op, $bind, $ident, \@args);
+  $prepared_bind ||= [];
 
-  my $bind_attrs = {};
+  return wantarray
+    ? ($sql, $prepared_bind, $bind_attrs)
+    : \[ "($sql)", @$prepared_bind ]
+  ;
+}
+
+sub _select_args {
+  my ($self, $ident, $select, $where, $attrs) = @_;
 
-  my $alias2source = $self->_resolve_ident_sources ($ident);
+  my ($alias2source, $rs_alias) = $self->_resolve_ident_sources ($ident);
 
+  my $sql_maker = $self->sql_maker;
+  $sql_maker->{_dbic_rs_attrs} = {
+    %$attrs,
+    select => $select,
+    from => $ident,
+    where => $where,
+    $rs_alias
+      ? ( _source_handle => $alias2source->{$rs_alias}->handle )
+      : ()
+    ,
+  };
+
+  # calculate bind_attrs before possible $ident mangling
+  my $bind_attrs = {};
   for my $alias (keys %$alias2source) {
     my $bindtypes = $self->source_bind_attributes ($alias2source->{$alias}) || {};
     for my $col (keys %$bindtypes) {
@@ -1251,44 +1401,260 @@ sub _select_args {
       my $fqcn = join ('.', $alias, $col);
       $bind_attrs->{$fqcn} = $bindtypes->{$col} if $bindtypes->{$col};
 
-      # so that unqualified searches can be bound too
-      $bind_attrs->{$col} = $bind_attrs->{$fqcn} if $alias eq 'me';
+      # Unqialified column names are nice, but at the same time can be
+      # rather ambiguous. What we do here is basically go along with
+      # the loop, adding an unqualified column slot to $bind_attrs,
+      # alongside the fully qualified name. As soon as we encounter
+      # another column by that name (which would imply another table)
+      # we unset the unqualified slot and never add any info to it
+      # to avoid erroneous type binding. If this happens the users
+      # only choice will be to fully qualify his column name
+
+      if (exists $bind_attrs->{$col}) {
+        $bind_attrs->{$col} = {};
+      }
+      else {
+        $bind_attrs->{$col} = $bind_attrs->{$fqcn};
+      }
     }
   }
 
-  # This would be the point to deflate anything found in $condition
+  # adjust limits
+  if (
+    $attrs->{software_limit}
+      ||
+    $sql_maker->_default_limit_syntax eq "GenericSubQ"
+  ) {
+    $attrs->{software_limit} = 1;
+  }
+  else {
+    $self->throw_exception("rows attribute must be positive if present")
+      if (defined($attrs->{rows}) && !($attrs->{rows} > 0));
+
+    # MySQL actually recommends this approach.  I cringe.
+    $attrs->{rows} = 2**48 if not defined $attrs->{rows} and defined $attrs->{offset};
+  }
+
+  my @limit;
+
+  # see if we need to tear the prefetch apart (either limited has_many or grouped prefetch)
+  # otherwise delegate the limiting to the storage, unless software limit was requested
+  if (
+    ( $attrs->{rows} && keys %{$attrs->{collapse}} )
+       ||
+    ( $attrs->{group_by} && @{$attrs->{group_by}} &&
+      $attrs->{prefetch_select} && @{$attrs->{prefetch_select}} )
+  ) {
+    ($ident, $select, $where, $attrs)
+      = $self->_adjust_select_args_for_complex_prefetch ($ident, $select, $where, $attrs);
+  }
+  elsif (! $attrs->{software_limit} ) {
+    push @limit, $attrs->{rows}, $attrs->{offset};
+  }
+
+###
+  # This would be the point to deflate anything found in $where
   # (and leave $attrs->{bind} intact). Problem is - inflators historically
   # expect a row object. And all we have is a resultsource (it is trivial
   # to extract deflator coderefs via $alias2source above).
   #
   # I don't see a way forward other than changing the way deflators are
   # invoked, and that's just bad...
+###
 
-  my @args = ('select', $attrs->{bind}, $ident, $bind_attrs, $select, $condition, $order);
-  if ($attrs->{software_limit} ||
-      $sql_maker->_default_limit_syntax eq "GenericSubQ") {
-        $attrs->{software_limit} = 1;
-  } else {
-    $self->throw_exception("rows attribute must be positive if present")
-      if (defined($attrs->{rows}) && !($attrs->{rows} > 0));
+  my $order = { map
+    { $attrs->{$_} ? ( $_ => $attrs->{$_} ) : ()  }
+    (qw/order_by group_by having/ )
+  };
 
-    # MySQL actually recommends this approach.  I cringe.
-    $attrs->{rows} = 2**48 if not defined $attrs->{rows} and defined $attrs->{offset};
-    push @args, $attrs->{rows}, $attrs->{offset};
+  return ('select', $attrs->{bind}, $ident, $bind_attrs, $select, $where, $order, @limit);
+}
+
+#
+# This is the code producing joined subqueries like:
+# SELECT me.*, other.* FROM ( SELECT me.* FROM ... ) JOIN other ON ... 
+#
+sub _adjust_select_args_for_complex_prefetch {
+  my ($self, $from, $select, $where, $attrs) = @_;
+
+  $self->throw_exception ('Complex prefetches are not supported on resultsets with a custom from attribute')
+    if (ref $from ne 'ARRAY');
+
+  # copies for mangling
+  $from = [ @$from ];
+  $select = [ @$select ];
+  $attrs = { %$attrs };
+
+  # separate attributes
+  my $sub_attrs = { %$attrs };
+  delete $attrs->{$_} for qw/where bind rows offset group_by having/;
+  delete $sub_attrs->{$_} for qw/for collapse prefetch_select _collapse_order_by select as/;
+
+  my $alias = $attrs->{alias};
+  my $sql_maker = $self->sql_maker;
+
+  # create subquery select list - consider only stuff *not* brought in by the prefetch
+  my $sub_select = [];
+  for my $i (0 .. @{$attrs->{select}} - @{$attrs->{prefetch_select}} - 1) {
+    my $sel = $attrs->{select}[$i];
+
+    # alias any functions to the dbic-side 'as' label
+    # adjust the outer select accordingly
+    if (ref $sel eq 'HASH' && !$sel->{-select}) {
+      $sel = { -select => $sel, -as => $attrs->{as}[$i] };
+      $select->[$i] = join ('.', $attrs->{alias}, ($attrs->{as}[$i] || "select_$i") );
+    }
+
+    push @$sub_select, $sel;
+  }
+
+  # bring over all non-collapse-induced order_by into the inner query (if any)
+  # the outer one will have to keep them all
+  delete $sub_attrs->{order_by};
+  if (my $ord_cnt = @{$attrs->{order_by}} - @{$attrs->{_collapse_order_by}} ) {
+    $sub_attrs->{order_by} = [
+      @{$attrs->{order_by}}[ 0 .. $ord_cnt - 1]
+    ];
+  }
+
+  # mangle {from}
+  my $join_root = shift @$from;
+  my @outer_from = @$from;
+
+  my %inner_joins;
+  my %join_info = map { $_->[0]{-alias} => $_->[0] } (@$from);
+
+  # in complex search_related chains $alias may *not* be 'me'
+  # so always include it in the inner join, and also shift away
+  # from the outer stack, so that the two datasets actually do
+  # meet
+  if ($join_root->{-alias} ne $alias) {
+    $inner_joins{$alias} = 1;
+
+    while (@outer_from && $outer_from[0][0]{-alias} ne $alias) {
+      shift @outer_from;
+    }
+    if (! @outer_from) {
+      $self->throw_exception ("Unable to find '$alias' in the {from} stack, something is wrong");
+    }
+
+    shift @outer_from; # the new subquery will represent this alias, so get rid of it
+  }
+
+
+  # decide which parts of the join will remain on the inside
+  #
+  # this is not a very viable optimisation, but it was written
+  # before I realised this, so might as well remain. We can throw
+  # away _any_ branches of the join tree that are:
+  # 1) not mentioned in the condition/order
+  # 2) left-join leaves (or left-join leaf chains)
+  # Most of the join conditions will not satisfy this, but for real
+  # complex queries some might, and we might make some RDBMS happy.
+  #
+  #
+  # since we do not have introspectable SQLA, we fall back to ugly
+  # scanning of raw SQL for WHERE, and for pieces of ORDER BY
+  # in order to determine what goes into %inner_joins
+  # It may not be very efficient, but it's a reasonable stop-gap
+  {
+    # produce stuff unquoted, so it can be scanned
+    local $sql_maker->{quote_char};
+    my $sep = $self->_sql_maker_opts->{name_sep} || '.';
+    $sep = "\Q$sep\E";
+
+    my @order_by = (map
+      { ref $_ ? $_->[0] : $_ }
+      $sql_maker->_order_by_chunks ($sub_attrs->{order_by})
+    );
+
+    my $where_sql = $sql_maker->where ($where);
+    my $select_sql = $sql_maker->_recurse_fields ($sub_select);
+
+    # sort needed joins
+    for my $alias (keys %join_info) {
+
+      # any table alias found on a column name in where or order_by
+      # gets included in %inner_joins
+      # Also any parent joins that are needed to reach this particular alias
+      for my $piece ($select_sql, $where_sql, @order_by ) {
+        if ($piece =~ /\b $alias $sep/x) {
+          $inner_joins{$alias} = 1;
+        }
+      }
+    }
+  }
+
+  # scan for non-leaf/non-left joins and mark as needed
+  # also mark all ancestor joins that are needed to reach this particular alias
+  # (e.g.  join => { cds => 'tracks' } - tracks will bring cds too )
+  #
+  # traverse by the size of the -join_path i.e. reverse depth first
+  for my $alias (sort { @{$join_info{$b}{-join_path}} <=> @{$join_info{$a}{-join_path}} } (keys %join_info) ) {
+
+    my $j = $join_info{$alias};
+    $inner_joins{$alias} = 1 if (! $j->{-join_type} || ($j->{-join_type} !~ /^left$/i) );
+
+    if ($inner_joins{$alias}) {
+      $inner_joins{$_} = 1 for (@{$j->{-join_path}});
+    }
+  }
+
+  # construct the inner $from for the subquery
+  my $inner_from = [ $join_root ];
+  for my $j (@$from) {
+    push @$inner_from, $j if $inner_joins{$j->[0]{-alias}};
+  }
+
+  # if a multi-type join was needed in the subquery ("multi" is indicated by
+  # presence in {collapse}) - add a group_by to simulate the collapse in the subq
+  for my $alias (keys %inner_joins) {
+
+    # the dot comes from some weirdness in collapse
+    # remove after the rewrite
+    if ($attrs->{collapse}{".$alias"}) {
+      $sub_attrs->{group_by} ||= $sub_select;
+      last;
+    }
   }
-  return @args;
+
+  # generate the subquery
+  my $subq = $self->_select_args_to_query (
+    $inner_from,
+    $sub_select,
+    $where,
+    $sub_attrs
+  );
+
+  # put it in the new {from}
+  unshift @outer_from, {
+    -alias => $alias,
+    -source_handle => $join_root->{-source_handle},
+    $alias => $subq,
+  };
+
+  # 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 didin't want in the first place
+  #
+  # OTOH it can be seen as a plus: <ash> (notes that this query would make a DBA cry ;)
+  return (\@outer_from, $select, $where, $attrs);
 }
 
 sub _resolve_ident_sources {
   my ($self, $ident) = @_;
 
   my $alias2source = {};
+  my $rs_alias;
 
   # the reason this is so contrived is that $ident may be a {from}
   # structure, specifying multiple tables to join
   if ( Scalar::Util::blessed($ident) && $ident->isa("DBIx::Class::ResultSource") ) {
     # this is compat mode for insert/update/delete which do not deal with aliases
     $alias2source->{me} = $ident;
+    $rs_alias = 'me';
   }
   elsif (ref $ident eq 'ARRAY') {
 
@@ -1296,88 +1662,79 @@ sub _resolve_ident_sources {
       my $tabinfo;
       if (ref $_ eq 'HASH') {
         $tabinfo = $_;
+        $rs_alias = $tabinfo->{-alias};
       }
       if (ref $_ eq 'ARRAY' and ref $_->[0] eq 'HASH') {
         $tabinfo = $_->[0];
       }
 
-      $alias2source->{$tabinfo->{-alias}} = $tabinfo->{-result_source}
-        if ($tabinfo->{-result_source});
+      $alias2source->{$tabinfo->{-alias}} = $tabinfo->{-source_handle}->resolve
+        if ($tabinfo->{-source_handle});
     }
   }
 
-  return $alias2source;
+  return ($alias2source, $rs_alias);
 }
 
-sub count {
-  my ($self, $source, $attrs) = @_;
-
-  my $tmp_attrs = { %$attrs };
-
-  # take off any pagers, record_filter is cdbi, and no point of ordering a count
-  delete $tmp_attrs->{$_} for (qw/select as rows offset page order_by record_filter/);
-
-  # overwrite the selector
-  $tmp_attrs->{select} = { count => '*' };
-
-  my $tmp_rs = $source->resultset_class->new($source, $tmp_attrs);
-  my ($count) = $tmp_rs->cursor->next;
-
-  # if the offset/rows attributes are still present, we did not use
-  # a subquery, so we need to make the calculations in software
-  $count -= $attrs->{offset} if $attrs->{offset};
-  $count = $attrs->{rows} if $attrs->{rows} and $attrs->{rows} < $count;
-  $count = 0 if ($count < 0);
+# Takes $ident, \@column_names
+#
+# returns { $column_name => \%column_info, ... }
+# also note: this adds -result_source => $rsrc to the column info
+#
+# usage:
+#   my $col_sources = $self->_resolve_column_info($ident, [map $_->[0], @{$bind}]);
+sub _resolve_column_info {
+  my ($self, $ident, $colnames) = @_;
+  my ($alias2src, $root_alias) = $self->_resolve_ident_sources($ident);
 
-  return $count;
-}
+  my $sep = $self->_sql_maker_opts->{name_sep} || '.';
+  $sep = "\Q$sep\E";
 
-sub count_grouped {
-  my ($self, $source, $attrs) = @_;
+  my (%return, %converted);
+  foreach my $col (@$colnames) {
+    my ($alias, $colname) = $col =~ m/^ (?: ([^$sep]+) $sep)? (.+) $/x;
 
-  # copy for the subquery, we need to do some adjustments to it too
-  my $sub_attrs = { %$attrs };
+    # deal with unqualified cols - we assume the main alias for all
+    # unqualified ones, ugly but can't think of anything better right now
+    $alias ||= $root_alias;
 
-  # these can not go in the subquery, and there is no point of ordering it
-  delete $sub_attrs->{$_} for qw/prefetch collapse select as order_by/;
-
-  # if we prefetch, we group_by primary keys only as this is what we would get out of the rs via ->next/->all
-  # simply deleting group_by suffices, as the code below will re-fill it
-  # Note: we check $attrs, as $sub_attrs has collapse deleted
-  if (ref $attrs->{collapse} and keys %{$attrs->{collapse}} ) {
-    delete $sub_attrs->{group_by};
+    my $rsrc = $alias2src->{$alias};
+    $return{$col} = $rsrc && { %{$rsrc->column_info($colname)}, -result_source => $rsrc };
   }
+  return \%return;
+}
 
-  $sub_attrs->{group_by} ||= [ map { "$attrs->{alias}.$_" } ($source->primary_columns) ];
-  $sub_attrs->{select} = $self->_grouped_count_select ($source, $sub_attrs);
-
-  $attrs->{from} = [{
-    count_subq => $source->resultset_class->new ($source, $sub_attrs )->as_query
-  }];
-
-  # the subquery replaces this
-  delete $attrs->{$_} for qw/where bind prefetch collapse group_by having having_bind rows offset page pager/;
-
-  return $self->count ($source, $attrs);
+# Returns a counting SELECT for a simple count
+# query. Abstracted so that a storage could override
+# this to { count => 'firstcol' } or whatever makes
+# sense as a performance optimization
+sub _count_select {
+  #my ($self, $source, $rs_attrs) = @_;
+  return { count => '*' };
 }
 
+# Returns a SELECT which will end up in the subselect
+# There may or may not be a group_by, as the subquery
+# might have been called to accomodate a limit
 #
-# Returns a SELECT to go with a supplied GROUP BY
-# (caled by count_grouped so a group_by is present)
-# Most databases expect them to match, but some
-# choke in various ways.
+# Most databases would be happy with whatever ends up
+# here, but some choke in various ways.
 #
-sub _grouped_count_select {
-  my ($self, $source, $rs_args) = @_;
-  return $rs_args->{group_by};
+sub _subq_count_select {
+  my ($self, $source, $rs_attrs) = @_;
+  return $rs_attrs->{group_by} if $rs_attrs->{group_by};
+
+  my @pcols = map { join '.', $rs_attrs->{alias}, $_ } ($source->primary_columns);
+  return @pcols ? \@pcols : [ 1 ];
 }
 
+
 sub source_bind_attributes {
   my ($self, $source) = @_;
-  
+
   my $bind_attributes;
   foreach my $column ($source->columns) {
-  
+
     my $data_type = $source->column_info($column)->{data_type} || '';
     $bind_attributes->{$column} = $self->bind_attribute_by_data_type($data_type)
      if $data_type;
@@ -1626,13 +1983,13 @@ By default, C<\%sqlt_args> will have
 
  { add_drop_table => 1, ignore_constraint_names => 1, ignore_index_names => 1 }
 
-merged with the hash passed in. To disable any of those features, pass in a 
+merged with the hash passed in. To disable any of those features, pass in a
 hashref like the following
 
  { ignore_constraint_names => 0, # ... other options }
 
 
-Note that this feature is currently EXPERIMENTAL and may not work correctly 
+Note that this feature is currently EXPERIMENTAL and may not work correctly
 across all databases, or fully handle complex relationships.
 
 WARNING: Please check all SQL files created, before applying them.
@@ -1653,7 +2010,7 @@ sub create_ddl_dir {
   $version ||= $schema_version;
 
   $sqltargs = {
-    add_drop_table => 1, 
+    add_drop_table => 1,
     ignore_constraint_names => 1,
     ignore_index_names => 1,
     %{$sqltargs || {}}
@@ -1693,7 +2050,7 @@ sub create_ddl_dir {
     }
     print $file $output;
     close($file);
-  
+
     next unless ($preversion);
 
     require SQL::Translator::Diff;
@@ -1709,7 +2066,7 @@ sub create_ddl_dir {
       carp("Overwriting existing diff file - $difffile");
       unlink($difffile);
     }
-    
+
     my $source_schema;
     {
       my $t = SQL::Translator->new($sqltargs);
@@ -1728,7 +2085,7 @@ sub create_ddl_dir {
         unless ( $source_schema->name );
     }
 
-    # The "new" style of producers have sane normalization and can support 
+    # The "new" style of producers have sane normalization and can support
     # diffing a SQL file against a DBIC->SQLT schema. Old style ones don't
     # And we have to diff parsed SQL against parsed SQL.
     my $dest_schema = $sqlt_schema;
@@ -1749,12 +2106,12 @@ sub create_ddl_dir {
       $dest_schema->name( $filename )
         unless $dest_schema->name;
     }
-    
+
     my $diff = SQL::Translator::Diff::schema_diff($source_schema, $db,
                                                   $dest_schema,   $db,
                                                   $sqltargs
                                                  );
-    if(!open $file, ">$difffile") { 
+    if(!open $file, ">$difffile") {
       $self->throw_exception("Can't write to $difffile ($!)");
       next;
     }
@@ -1798,7 +2155,7 @@ sub deployment_statements {
   if(-f $filename)
   {
       my $file;
-      open($file, "<$filename") 
+      open($file, "<$filename")
         or $self->throw_exception("Can't open $filename ($!)");
       my @rows = <$file>;
       close($file);
@@ -1813,7 +2170,7 @@ sub deployment_statements {
   eval qq{use SQL::Translator::Producer::${type}};
   $self->throw_exception($@) if $@;
 
-  # sources needs to be a parser arg, but for simplicty allow at top level 
+  # sources needs to be a parser arg, but for simplicty allow at top level
   # coming in
   $sqltargs->{parser_args}{sources} = delete $sqltargs->{sources}
       if exists $sqltargs->{sources};
@@ -1918,7 +2275,7 @@ returned by databases that don't support replication.
 
 sub is_replicating {
     return;
-    
+
 }
 
 =head2 lag_behind_master
diff --git a/lib/DBIx/Class/Storage/DBI/AmbiguousGlob.pm b/lib/DBIx/Class/Storage/DBI/AmbiguousGlob.pm
new file mode 100644 (file)
index 0000000..37d1bd6
--- /dev/null
@@ -0,0 +1,44 @@
+package DBIx::Class::Storage::DBI::AmbiguousGlob;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Storage::DBI';
+use mro 'c3';
+
+=head1 NAME
+
+DBIx::Class::Storage::DBI::AmbiguousGlob - Storage component for RDBMS supporting multicolumn in clauses
+
+=head1 DESCRIPTION
+
+Some servers choke on things like:
+
+  COUNT(*) FROM (SELECT tab1.col, tab2.col FROM tab1 JOIN tab2 ... )
+
+claiming that col is a duplicate column (it loses the table specifiers by
+the time it gets to the *). Thus for any subquery count we select only the
+primary keys of the main table in the inner query. This hopefully still
+hits the indexes and keeps the server happy.
+
+At this point the only overriden method is C<_subq_count_select()>
+
+=cut
+
+sub _subq_count_select {
+  my ($self, $source, $rs_attrs) = @_;
+  my @pcols = map { join '.', $rs_attrs->{alias}, $_ } ($source->primary_columns);
+  return @pcols ? \@pcols : [ 1 ];
+}
+
+=head1 AUTHORS
+
+See L<DBIx::Class/CONTRIBUTORS>
+
+=head1 LICENSE
+
+You may distribute this code under the same terms as Perl itself.
+
+=cut
+
+1;
index bd2a20a..3d59e84 100644 (file)
@@ -1,10 +1,10 @@
 package DBIx::Class::Storage::DBI::Cursor;
 
-use base qw/DBIx::Class::Cursor/;
-
 use strict;
 use warnings;
 
+use base qw/DBIx::Class::Cursor/;
+
 =head1 NAME
 
 DBIx::Class::Storage::DBI::Cursor - Object representing a query cursor on a
@@ -68,7 +68,11 @@ sub _dbh_next {
   my ($storage, $dbh, $self) = @_;
 
   $self->_check_dbh_gen;
-  if ($self->{attrs}{rows} && $self->{pos} >= $self->{attrs}{rows}) {
+  if (
+    $self->{attrs}{software_limit}
+      && $self->{attrs}{rows}
+        && $self->{pos} >= $self->{attrs}{rows}
+  ) {
     $self->{sth}->finish if $self->{sth}->{Active};
     delete $self->{sth};
     $self->{done} = 1;
@@ -128,6 +132,7 @@ sub all {
         && ($self->{attrs}{offset} || $self->{attrs}{rows})) {
     return $self->next::method;
   }
+
   $self->{storage}->dbh_do($self->can('_dbh_all'), $self);
 }
 
index 4988c06..b36ab13 100644 (file)
@@ -4,8 +4,7 @@ use strict;
 use warnings;
 
 use base qw/DBIx::Class::Storage::DBI/;
-
-# __PACKAGE__->load_components(qw/PK::Auto/);
+use mro 'c3';
 
 sub _dbh_last_insert_id {
     my ($self, $dbh, $source, $col) = @_;
@@ -22,11 +21,11 @@ sub datetime_parser_type { "DateTime::Format::DB2"; }
 
 sub _sql_maker_opts {
     my ( $self, $opts ) = @_;
-    
+
     if ( $opts ) {
         $self->{_sql_maker_opts} = { %$opts };
     }
-                    
+
     return { limit_dialect => 'RowNumberOver', %{$self->{_sql_maker_opts}||{}} };
 }
 
index 40afe76..3a1f868 100644 (file)
@@ -3,7 +3,10 @@ package DBIx::Class::Storage::DBI::MSSQL;
 use strict;
 use warnings;
 
-use base qw/DBIx::Class::Storage::DBI/;
+use base qw/DBIx::Class::Storage::DBI::AmbiguousGlob DBIx::Class::Storage::DBI/;
+use mro 'c3';
+
+__PACKAGE__->sql_maker_class('DBIx::Class::SQLAHacks::MSSQL');
 
 sub _dbh_last_insert_id {
   my ($self, $dbh, $source, $col) = @_;
index 0a48805..2a757ea 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use warnings;
 
 use base 'DBIx::Class::Storage::DBI';
+use mro 'c3';
 
 =head1 NAME 
 
index 7027ad6..95f1cac 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use warnings;
 
 use base 'DBIx::Class::Storage::DBI';
+use mro 'c3';
 
 =head1 NAME 
 
index 09a7c6b..6f905af 100644 (file)
@@ -3,6 +3,7 @@ use strict;
 use warnings;
 
 use base qw/DBIx::Class::Storage::DBI/;
+use mro 'c3';
 
 sub _rebless {
     my ($self) = @_;
@@ -11,9 +12,11 @@ sub _rebless {
     unless ( $@ ) {
         # Translate the backend name into a perl identifier
         $dbtype =~ s/\W/_/gi;
-        my $class = "DBIx::Class::Storage::DBI::ODBC::${dbtype}";
-        eval "require $class";
-        bless $self, $class unless $@;
+        my $subclass = "DBIx::Class::Storage::DBI::ODBC::${dbtype}";
+        if ($self->load_optional_class($subclass) && !$self->isa($subclass)) {
+            bless $self, $subclass;
+            $self->_rebless;
+        }
     }
 }
 
index c0df4b1..1b1909a 100644 (file)
@@ -2,8 +2,10 @@ package DBIx::Class::Storage::DBI::ODBC::ACCESS;
 use strict;
 use warnings;
 
-use DBI;
 use base qw/DBIx::Class::Storage::DBI/;
+use mro 'c3';
+
+use DBI;
 
 my $ERR_MSG_START = __PACKAGE__ . ' failed: ';
 
@@ -38,11 +40,11 @@ sub last_insert_id {
 
 sub bind_attribute_by_data_type {
     my $self = shift;
-    
+
     my ( $data_type ) = @_;
-    
+
     return { TYPE => $data_type } if $data_type == DBI::SQL_LONGVARCHAR;
-    
+
     return;
 }
 
index 5b8b348..1bed7f5 100644 (file)
@@ -3,6 +3,7 @@ use strict;
 use warnings;
 
 use base qw/DBIx::Class::Storage::DBI::ODBC/;
+use mro 'c3';
 
 sub _dbh_last_insert_id {
     my ($self, $dbh, $source, $col) = @_;
@@ -22,7 +23,7 @@ sub _dbh_last_insert_id {
 
 sub _sql_maker_opts {
     my ($self) = @_;
-    
+
     $self->dbh_do(sub {
         my ($self, $dbh) = @_;
 
index 3a464fd..48f281b 100644 (file)
@@ -3,15 +3,70 @@ use strict;
 use warnings;
 
 use base qw/DBIx::Class::Storage::DBI::MSSQL/;
+use mro 'c3';
+
+use List::Util();
+
+sub insert_bulk {
+  my $self = shift;
+  my ($source, $cols, $data) = @_;
+
+  my $identity_insert = 0;
+
+  COLUMNS:
+  foreach my $col (@{$cols}) {
+    if ($source->column_info($col)->{is_auto_increment}) {
+      $identity_insert = 1;
+      last COLUMNS;
+    }
+  }
+
+  if ($identity_insert) {
+    my $table = $source->from;
+    $self->dbh->do("SET IDENTITY_INSERT $table ON");
+  }
+
+  $self->next::method(@_);
+
+  if ($identity_insert) {
+    my $table = $source->from;
+    $self->dbh->do("SET IDENTITY_INSERT $table OFF");
+  }
+}
 
 sub _prep_for_execute {
-    my $self = shift;
-    my ($op, $extra_bind, $ident, $args) = @_;
+  my $self = shift;
+  my ($op, $extra_bind, $ident, $args) = @_;
+
+# cast MONEY values properly
+  if ($op eq 'insert' || $op eq 'update') {
+    my $fields = $args->[0];
+    my $col_info = $self->_resolve_column_info($ident, [keys %$fields]);
+
+    for my $col (keys %$fields) {
+      if ($col_info->{$col}{data_type} =~ /^money\z/i) {
+        my $val = $fields->{$col};
+        $fields->{$col} = \['CAST(? AS MONEY)', [ $col => $val ]];
+      }
+    }
+  }
+
+  my ($sql, $bind) = $self->next::method (@_);
 
-    my ($sql, $bind) = $self->next::method (@_);
-    $sql .= ';SELECT SCOPE_IDENTITY()' if $op eq 'insert';
+  if ($op eq 'insert') {
+    $sql .= ';SELECT SCOPE_IDENTITY()';
+
+    my $col_info = $self->_resolve_column_info($ident, [map $_->[0], @{$bind}]);
+    if (List::Util::first { $_->{is_auto_increment} } (values %$col_info) ) {
+
+      my $table = $ident->from;
+      my $identity_insert_on = "SET IDENTITY_INSERT $table ON";
+      my $identity_insert_off = "SET IDENTITY_INSERT $table OFF";
+      $sql = "$identity_insert_on; $sql; $identity_insert_off";
+    }
+  }
 
-    return ($sql, $bind);
+  return ($sql, $bind);
 }
 
 sub _execute {
@@ -64,3 +119,4 @@ Marc Mims C<< <marc@questright.com> >>
 You may distribute this code under the same terms as Perl itself.
 
 =cut
+# vim: sw=2 sts=2
index f8f981b..1bb9f79 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use warnings;
 
 use base qw/DBIx::Class::Storage::DBI/;
+use mro 'c3';
 
 sub _rebless {
     my ($self) = @_;
index 2021056..1593978 100644 (file)
@@ -5,7 +5,7 @@ use warnings;
 
 =head1 NAME
 
-DBIx::Class::Storage::DBI::Oracle::Generic - Automatic primary key class for Oracle
+DBIx::Class::Storage::DBI::Oracle::Generic - Oracle Support for DBIx::Class
 
 =head1 SYNOPSIS
 
@@ -24,10 +24,7 @@ This class implements autoincrements for Oracle.
 =cut
 
 use base qw/DBIx::Class::Storage::DBI/;
-use Carp::Clan qw/^DBIx::Class/;
-
-# For ORA_BLOB => 113, ORA_CLOB => 112
-use DBD::Oracle qw( :ora_types );
+use mro 'c3';
 
 sub _dbh_last_insert_id {
   my ($self, $dbh, $source, @columns) = @_;
@@ -52,7 +49,7 @@ sub _dbh_get_autoinc_seq {
   };
 
   # trigger_body is a LONG
-  $dbh->{LongReadLen} = 64 * 1024 if ($dbh->{LongReadLen} < 64 * 1024);
+  local $dbh->{LongReadLen} = 64 * 1024 if ($dbh->{LongReadLen} < 64 * 1024);
 
   my $sth;
 
@@ -83,36 +80,18 @@ sub _sequence_fetch {
   return $id;
 }
 
-=head2 connected
-
-Returns true if we have an open (and working) database connection, false if it is not (yet)
-open (or does not work). (Executes a simple SELECT to make sure it works.)
-
-The reason this is needed is that L<DBD::Oracle>'s ping() does not do a real
-OCIPing but just gets the server version, which doesn't help if someone killed
-your session.
-
-=cut
-
-sub connected {
+sub _ping {
   my $self = shift;
 
-  if (not $self->next::method(@_)) {
-    return 0;
-  }
-  else {
-    my $dbh = $self->_dbh;
+  my $dbh = $self->_dbh or return 0;
 
-    local $dbh->{RaiseError} = 1;
+  local $dbh->{RaiseError} = 1;
 
-    eval {
-      my $ping_sth = $dbh->prepare_cached("select 1 from dual");
-      $ping_sth->execute;
-      $ping_sth->finish;
-    };
+  eval {
+    $dbh->do("select 1 from dual");
+  };
 
-    return $@ ? 0 : 1;
-  }
+  return $@ ? 0 : 1;
 }
 
 sub _dbh_execute {
@@ -157,7 +136,7 @@ Returns the sequence name for an autoincrement column
 
 sub get_autoinc_seq {
   my ($self, $source, $col) = @_;
-    
+
   $self->dbh_do('_dbh_get_autoinc_seq', $source, $col);
 }
 
@@ -183,9 +162,53 @@ L<DBIx::Class::InflateColumn::DateTime>.
 
 sub datetime_parser_type { return "DateTime::Format::Oracle"; }
 
+=head2 connect_call_datetime_setup
+
+Used as:
+
+    on_connect_call => 'datetime_setup'
+
+In L<DBIx::Class::Storage::DBI/connect_info> to set the session nls date, and
+timestamp values for use with L<DBIx::Class::InflateColumn::DateTime> and the
+necessary environment variables for L<DateTime::Format::Oracle>, which is used
+by it.
+
+Maximum allowable precision is used, unless the environment variables have
+already been set.
+
+These are the defaults used:
+
+  $ENV{NLS_DATE_FORMAT}         ||= 'YYYY-MM-DD HH24:MI:SS';
+  $ENV{NLS_TIMESTAMP_FORMAT}    ||= 'YYYY-MM-DD HH24:MI:SS.FF';
+  $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
+
+To get more than second precision with L<DBIx::Class::InflateColumn::DateTime>
+for your timestamps, use something like this:
+
+  use Time::HiRes 'time';
+  my $ts = DateTime->from_epoch(epoch => time);
+
+=cut
+
+sub connect_call_datetime_setup {
+  my $self = shift;
+
+  my $date_format = $ENV{NLS_DATE_FORMAT} ||= 'YYYY-MM-DD HH24:MI:SS';
+  my $timestamp_format = $ENV{NLS_TIMESTAMP_FORMAT} ||=
+    'YYYY-MM-DD HH24:MI:SS.FF';
+  my $timestamp_tz_format = $ENV{NLS_TIMESTAMP_TZ_FORMAT} ||=
+    'YYYY-MM-DD HH24:MI:SS.FF TZHTZM';
+
+  $self->_do_query("alter session set nls_date_format = '$date_format'");
+  $self->_do_query(
+"alter session set nls_timestamp_format = '$timestamp_format'");
+  $self->_do_query(
+"alter session set nls_timestamp_tz_format='$timestamp_tz_format'");
+}
+
 sub _svp_begin {
     my ($self, $name) = @_;
+
     $self->dbh->do("SAVEPOINT $name");
 }
 
@@ -208,6 +231,7 @@ table with more than one LOB column.
 
 sub source_bind_attributes 
 {
+       require DBD::Oracle;
        my $self = shift;
        my($source) = @_;
 
@@ -220,8 +244,9 @@ sub source_bind_attributes
                my %column_bind_attrs = $self->bind_attribute_by_data_type($data_type);
 
                if ($data_type =~ /^[BC]LOB$/i) {
-                       $column_bind_attrs{'ora_type'}
-                               = uc($data_type) eq 'CLOB' ? ORA_CLOB : ORA_BLOB;
+                       $column_bind_attrs{'ora_type'} = uc($data_type) eq 'CLOB' ?
+                               DBD::Oracle::ORA_CLOB() :
+                               DBD::Oracle::ORA_BLOB();
                        $column_bind_attrs{'ora_field'} = $column;
                }
 
@@ -241,11 +266,9 @@ sub _svp_rollback {
     $self->dbh->do("ROLLBACK TO SAVEPOINT $name")
 }
 
-=head1 AUTHORS
-
-Andy Grundman <andy@hybridized.org>
+=head1 AUTHOR
 
-Scott Connelly <scottsweep@yahoo.com>
+See L<DBIx::Class/CONTRIBUTORS>.
 
 =head1 LICENSE
 
index d4e7385..6604847 100644 (file)
@@ -1,10 +1,11 @@
 package DBIx::Class::Storage::DBI::Oracle::WhereJoins;
 
-use base qw( DBIx::Class::Storage::DBI::Oracle::Generic );
-
 use strict;
 use warnings;
 
+use base qw( DBIx::Class::Storage::DBI::Oracle::Generic );
+use mro 'c3';
+
 __PACKAGE__->sql_maker_class('DBIx::Class::SQLAHacks::OracleJoins');
 
 1;
index 41b2357..3935a49 100644 (file)
@@ -3,15 +3,14 @@ package DBIx::Class::Storage::DBI::Pg;
 use strict;
 use warnings;
 
-use DBD::Pg qw(:pg_types);
-
 use base qw/DBIx::Class::Storage::DBI::MultiColumnIn/;
+use mro 'c3';
 
-# __PACKAGE__->load_components(qw/PK::Auto/);
+use DBD::Pg qw(:pg_types);
 
-# Warn about problematic versions of DBD::Pg
-warn "DBD::Pg 1.49 is strongly recommended"
-  if ($DBD::Pg::VERSION < 1.49);
+# Ask for a DBD::Pg with array support
+warn "DBD::Pg 2.9.2 or greater is strongly recommended\n"
+  if ($DBD::Pg::VERSION < 2.009002);  # pg uses (used?) version::qv()
 
 sub with_deferred_fk_checks {
   my ($self, $sub) = @_;
@@ -51,7 +50,7 @@ sub _dbh_get_autoinc_seq {
 
 sub get_autoinc_seq {
   my ($self,$source,$col) = @_;
-    
+
   my @pri = $source->primary_columns;
   my ($schema,$table) = $source->name =~ /^(.+)\.(.+)$/ ? ($1,$2)
     : (undef,$source->name);
@@ -72,7 +71,7 @@ sub bind_attribute_by_data_type {
     bytea => { pg_type => DBD::Pg::PG_BYTEA },
     blob  => { pg_type => DBD::Pg::PG_BYTEA },
   };
+
   if( defined $bind_attributes->{$data_type} ) {
     return $bind_attributes->{$data_type};
   }
index 259cdc5..ac9a877 100644 (file)
@@ -2,35 +2,35 @@ package DBIx::Class::Storage::DBI::Replicated;
 
 BEGIN {
   use Carp::Clan qw/^DBIx::Class/;
-       
+
   ## Modules required for Replication support not required for general DBIC
   ## use, so we explicitly test for these.
-       
+
   my %replication_required = (
-    Moose => '0.77',
-    MooseX::AttributeHelpers => '0.12',
-    MooseX::Types => '0.10',
-    namespace::clean => '0.11',
-    Hash::Merge => '0.11'
+    'Moose' => '0.87',
+    'MooseX::AttributeHelpers' => '0.21',
+    'MooseX::Types' => '0.16',
+    'namespace::clean' => '0.11',
+    'Hash::Merge' => '0.11'
   );
-       
+
   my @didnt_load;
-  
+
   for my $module (keys %replication_required) {
        eval "use $module $replication_required{$module}";
        push @didnt_load, "$module $replication_required{$module}"
         if $@;
   }
-       
+
   croak("@{[ join ', ', @didnt_load ]} are missing and are required for Replication")
-    if @didnt_load;    
+    if @didnt_load;
 }
 
 use Moose;
 use DBIx::Class::Storage::DBI;
 use DBIx::Class::Storage::DBI::Replicated::Pool;
 use DBIx::Class::Storage::DBI::Replicated::Balancer;
-use DBIx::Class::Storage::DBI::Replicated::Types 'BalancerClassNamePart';
+use DBIx::Class::Storage::DBI::Replicated::Types qw/BalancerClassNamePart DBICSchema DBICStorageDBI/;
 use MooseX::Types::Moose qw/ClassName HashRef Object/;
 use Scalar::Util 'reftype';
 use Carp::Clan qw/^DBIx::Class/;
@@ -48,33 +48,45 @@ The Following example shows how to change an existing $schema to a replicated
 storage type, add some replicated (readonly) databases, and perform reporting
 tasks.
 
-  ## Change storage_type in your schema class
+You should set the 'storage_type attribute to a replicated type.  You should
+also define your arguments, such as which balancer you want and any arguments
+that the Pool object should get.
+
   $schema->storage_type( ['::DBI::Replicated', {balancer=>'::Random'}] );
-  
-  ## Add some slaves.  Basically this is an array of arrayrefs, where each
-  ## arrayref is database connect information
-  
+
+Next, you need to add in the Replicants.  Basically this is an array of 
+arrayrefs, where each arrayref is database connect information.  Think of these
+arguments as what you'd pass to the 'normal' $schema->connect method.
+
   $schema->storage->connect_replicants(
     [$dsn1, $user, $pass, \%opts],
     [$dsn2, $user, $pass, \%opts],
     [$dsn3, $user, $pass, \%opts],
   );
-  
-  ## Now, just use the $schema as normal
+
+Now, just use the $schema as you normally would.  Automatically all reads will
+be delegated to the replicants, while writes to the master.
+
   $schema->resultset('Source')->search({name=>'etc'});
-  
-  ## You can force a given query to use a particular storage using the search
-  ### attribute 'force_pool'.  For example:
-  
+
+You can force a given query to use a particular storage using the search
+attribute 'force_pool'.  For example:
+
   my $RS = $schema->resultset('Source')->search(undef, {force_pool=>'master'});
-  
-  ## Now $RS will force everything (both reads and writes) to use whatever was
-  ## setup as the master storage.  'master' is hardcoded to always point to the
-  ## Master, but you can also use any Replicant name.  Please see:
-  ## L<DBIx::Class::Storage::Replicated::Pool> and the replicants attribute for
-  ## More. Also see transactions and L</execute_reliably> for alternative ways
-  ## to force read traffic to the master.
-  
+
+Now $RS will force everything (both reads and writes) to use whatever was setup
+as the master storage.  'master' is hardcoded to always point to the Master, 
+but you can also use any Replicant name.  Please see:
+L<DBIx::Class::Storage::DBI::Replicated::Pool> and the replicants attribute for more.
+
+Also see transactions and L</execute_reliably> for alternative ways to
+force read traffic to the master.  In general, you should wrap your statements
+in a transaction when you are reading and writing to the same tables at the
+same time, since your replicants will often lag a bit behind the master.
+
+See L<DBIx::Class::Storage::DBI::Replicated::Instructions> for more help and
+walkthroughs.
+
 =head1 DESCRIPTION
 
 Warning: This class is marked BETA.  This has been running a production
@@ -100,7 +112,7 @@ selected algorithm.  The default algorithm is random weighted.
 =head1 NOTES
 
 The consistancy betweeen master and replicants is database specific.  The Pool
-gives you a method to validate it's replicants, removing and replacing them
+gives you a method to validate its replicants, removing and replacing them
 when they fail/pass predefined criteria.  Please make careful use of the ways
 to force a query to run against Master when needed.
 
@@ -108,12 +120,12 @@ to force a query to run against Master when needed.
 
 Replicated Storage has additional requirements not currently part of L<DBIx::Class>
 
-  Moose => 0.77
-  MooseX::AttributeHelpers => 0.12 
-  MooseX::Types => 0.10
-  namespace::clean => 0.11
-  Hash::Merge => 0.11
-  
+  Moose => '0.87',
+  MooseX::AttributeHelpers => '0.20',
+  MooseX::Types => '0.16',
+  namespace::clean => '0.11',
+  Hash::Merge => '0.11'
+
 You will need to install these modules manually via CPAN or make them part of the
 Makefile for your distribution.
 
@@ -129,7 +141,7 @@ The underlying L<DBIx::Class::Schema> object this storage is attaching
 
 has 'schema' => (
     is=>'rw',
-    isa=>'DBIx::Class::Schema',
+    isa=>DBICSchema,
     weak_ref=>1,
     required=>1,
 );
@@ -153,7 +165,7 @@ has 'pool_type' => (
 =head2 pool_args
 
 Contains a hashref of initialized information to pass to the Balancer object.
-See L<DBIx::Class::Storage::Replicated::Pool> for available arguments.
+See L<DBIx::Class::Storage::DBI::Replicated::Pool> for available arguments.
 
 =cut
 
@@ -186,7 +198,7 @@ has 'balancer_type' => (
 =head2 balancer_args
 
 Contains a hashref of initialized information to pass to the Balancer object.
-See L<DBIx::Class::Storage::Replicated::Balancer> for available arguments.
+See L<DBIx::Class::Storage::DBI::Replicated::Balancer> for available arguments.
 
 =cut
 
@@ -242,7 +254,7 @@ pool of databases that is allowed to handle write traffic.
 
 has 'master' => (
   is=> 'ro',
-  isa=>'DBIx::Class::Storage::DBI',
+  isa=>DBICStorageDBI,
   lazy_build=>1,
 );
 
@@ -288,7 +300,8 @@ has 'write_handler' => (
     create_ddl_dir
     deployment_statements
     datetime_parser
-    datetime_parser_type        
+    datetime_parser_type  
+    build_datetime_parser      
     last_insert_id
     insert
     insert_bulk
@@ -303,10 +316,19 @@ has 'write_handler' => (
     sth
     deploy
     with_deferred_fk_checks
-
+       dbh_do
     reload_row
+       with_deferred_fk_checks
     _prep_for_execute
-    
+
+       backup
+       is_datatype_numeric
+       _count_select
+       _subq_count_select
+       _subq_update_delete 
+       svp_rollback
+       svp_begin
+       svp_release
   /],
 );
 
@@ -366,10 +388,11 @@ around connect_info => sub {
     $res = $self->$next($info, @extra);
   }
 
-  # May have to reapply role if master will be reblessed to a more specific
-  # driver.
-  $self->master->_determine_driver;
-  DBIx::Class::Storage::DBI::Replicated::WithDSN->meta->apply($self->master);
+  # Make sure master is blessed into the correct class and apply role to it.
+  my $master = $self->master;
+  $master->_determine_driver;
+  Moose::Meta::Class->initialize(ref $master);
+  DBIx::Class::Storage::DBI::Replicated::WithDSN->meta->apply($master);
 
   $wantarray ? @res : $res;
 };
@@ -380,7 +403,7 @@ This class defines the following methods.
 
 =head2 BUILDARGS
 
-L<DBIx::Class::Schema> when instantiating it's storage passed itself as the
+L<DBIx::Class::Schema> when instantiating its storage passed itself as the
 first argument.  So we need to massage the arguments a bit so that all the
 bits get put into the correct places.
 
@@ -388,7 +411,7 @@ bits get put into the correct places.
 
 sub BUILDARGS {
   my ($class, $schema, $storage_type_args, @args) = @_;        
-  
+
   return {
        schema=>$schema, 
        %$storage_type_args,
@@ -405,7 +428,6 @@ Lazy builder for the L</master> attribute.
 sub _build_master {
   my $self = shift @_;
   my $master = DBIx::Class::Storage::DBI->new($self->schema);
-  DBIx::Class::Storage::DBI::Replicated::WithDSN->meta->apply($master);
   $master
 }
 
@@ -546,24 +568,24 @@ inserted something and need to get a resultset including it, etc.
 
 sub execute_reliably {
   my ($self, $coderef, @args) = @_;
-  
+
   unless( ref $coderef eq 'CODE') {
     $self->throw_exception('Second argument must be a coderef');
   }
-  
+
   ##Get copy of master storage
   my $master = $self->master;
-  
+
   ##Get whatever the current read hander is
   my $current = $self->read_handler;
-  
+
   ##Set the read handler to master
   $self->read_handler($master);
-  
+
   ## do whatever the caller needs
   my @result;
   my $want_array = wantarray;
-  
+
   eval {
     if($want_array) {
       @result = $coderef->(@args);
@@ -573,13 +595,13 @@ sub execute_reliably {
       $coderef->(@args);
     }       
   };
-  
+
   ##Reset to the original state
   $self->read_handler($current); 
-  
+
   ##Exception testing has to come last, otherwise you might leave the 
   ##read_handler set to master.
-  
+
   if($@) {
     $self->throw_exception("coderef returned an error: $@");
   } else {
@@ -591,14 +613,14 @@ sub execute_reliably {
 
 Sets the current $schema to be 'reliable', that is all queries, both read and
 write are sent to the master
-  
+
 =cut
 
 sub set_reliable_storage {
   my $self = shift @_;
   my $schema = $self->schema;
   my $write_handler = $self->schema->storage->write_handler;
-  
+
   $schema->storage->read_handler($write_handler);
 }
 
@@ -606,29 +628,16 @@ sub set_reliable_storage {
 
 Sets the current $schema to be use the </balancer> for all reads, while all
 writea are sent to the master only
-  
+
 =cut
 
 sub set_balanced_storage {
   my $self = shift @_;
   my $schema = $self->schema;
-  my $write_handler = $self->schema->storage->balancer;
-  
-  $schema->storage->read_handler($write_handler);
-}
+  my $balanced_handler = $self->schema->storage->balancer;
 
-=head2 around: txn_do ($coderef)
-
-Overload to the txn_do method, which is delegated to whatever the
-L<write_handler> is set to.  We overload this in order to wrap in inside a
-L</execute_reliably> method.
-
-=cut
-
-around 'txn_do' => sub {
-  my($txn_do, $self, $coderef, @args) = @_;
-  $self->execute_reliably(sub {$self->$txn_do($coderef, @args)}); 
-};
+  $schema->storage->read_handler($balanced_handler);
+}
 
 =head2 connected
 
@@ -802,7 +811,7 @@ sub cursor_class {
   }
   $self->master->cursor_class;
 }
-  
+
 =head1 GOTCHAS
 
 Due to the fact that replicants can lag behind a master, you must take care to
@@ -836,7 +845,7 @@ using the Schema clone method.
 
   my $new_schema = $schema->clone;
   $new_schema->set_reliable_storage;
-  
+
   ## $new_schema will use only the Master storage for all reads/writes while
   ## the $schema object will use replicated storage.
 
index 798c0ef..5c32f56 100644 (file)
@@ -3,7 +3,8 @@ package DBIx::Class::Storage::DBI::Replicated::Balancer;
 use Moose::Role;
 requires 'next_storage';
 use MooseX::Types::Moose qw/Int/;
-
+use DBIx::Class::Storage::DBI::Replicated::Pool;
+use DBIx::Class::Storage::DBI::Replicated::Types qw/DBICStorageDBI/;
 use namespace::clean -except => 'meta';
 
 =head1 NAME
@@ -13,7 +14,7 @@ DBIx::Class::Storage::DBI::Replicated::Balancer - A Software Load Balancer
 =head1 SYNOPSIS
 
 This role is used internally by L<DBIx::Class::Storage::DBI::Replicated>.
-    
+
 =head1 DESCRIPTION
 
 Given a pool (L<DBIx::Class::Storage::DBI::Replicated::Pool>) of replicated
@@ -48,7 +49,7 @@ ultimate fallback.
 
 has 'master' => (
   is=>'ro',
-  isa=>'DBIx::Class::Storage::DBI',
+  isa=>DBICStorageDBI,
   required=>1,
 );
 
@@ -74,13 +75,13 @@ databases is going to help you to scale traffic.
 
 This attribute returns the next slave to handle a read request.  Your L</pool>
 attribute has methods to help you shuffle through all the available replicants
-via it's balancer object.
+via its balancer object.
 
 =cut
 
 has 'current_replicant' => (
   is=> 'rw',
-  isa=>'DBIx::Class::Storage::DBI',
+  isa=>DBICStorageDBI,
   lazy_build=>1,
   handles=>[qw/
     select
@@ -169,10 +170,12 @@ the load evenly (hopefully) across existing capacity.
 
 around 'select' => sub {
   my ($select, $self, @args) = @_;
-  
+
   if (my $forced_pool = $args[-1]->{force_pool}) {
     delete $args[-1]->{force_pool};
     return $self->_get_forced_pool($forced_pool)->select(@args); 
+  } elsif($self->master->{transaction_depth}) {
+    return $self->master->select(@args);
   } else {
     $self->increment_storage;
     return $self->$select(@args);
@@ -189,10 +192,12 @@ the load evenly (hopefully) across existing capacity.
 
 around 'select_single' => sub {
   my ($select_single, $self, @args) = @_;
-  
+
   if (my $forced_pool = $args[-1]->{force_pool}) {
     delete $args[-1]->{force_pool};
     return $self->_get_forced_pool($forced_pool)->select_single(@args); 
+  } elsif($self->master->{transaction_depth}) {
+    return $self->master->select_single(@args);
   } else {
     $self->increment_storage;
     return $self->$select_single(@args);
@@ -224,7 +229,7 @@ sub _get_forced_pool {
     return $forced_pool;
   } elsif($forced_pool eq 'master') {
     return $self->master;
-  } elsif(my $replicant = $self->pool->replicants($forced_pool)) {
+  } elsif(my $replicant = $self->pool->replicants->{$forced_pool}) {
     return $replicant;
   } else {
     $self->master->throw_exception("$forced_pool is not a named replicant.");
@@ -233,7 +238,7 @@ sub _get_forced_pool {
 
 =head1 AUTHOR
 
-John Napiorkowski <john.napiorkowski@takkle.com>
+John Napiorkowski <jjnapiork@cpan.org>
 
 =head1 LICENSE
 
index b230346..d40fb63 100644 (file)
@@ -12,7 +12,7 @@ DBIx::Class::Storage::DBI::Replicated::Balancer::First - Just get the First Bala
 
 This class is used internally by L<DBIx::Class::Storage::DBI::Replicated>.  You
 shouldn't need to create instances of this class.
-    
+
 =head1 DESCRIPTION
 
 Given a pool (L<DBIx::Class::Storage::DBI::Replicated::Pool>) of replicated
index f23db75..1fc7b94 100644 (file)
@@ -13,7 +13,7 @@ DBIx::Class::Storage::DBI::Replicated::Balancer::Random - A 'random' Balancer
 
 This class is used internally by L<DBIx::Class::Storage::DBI::Replicated>.  You
 shouldn't need to create instances of this class.
-    
+
 =head1 DESCRIPTION
 
 Given a pool (L<DBIx::Class::Storage::DBI::Replicated::Pool>) of replicated
diff --git a/lib/DBIx/Class/Storage/DBI/Replicated/Introduction.pod b/lib/DBIx/Class/Storage/DBI/Replicated/Introduction.pod
new file mode 100644 (file)
index 0000000..c48f2a1
--- /dev/null
@@ -0,0 +1,185 @@
+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 document is not an overview of what replication is or why you should be
+using it.  It is not a document explaing how to setup MySQL native replication
+either.  Copious external resources are avialable for both.  This document
+presumes you have the basics down.
+  
+=head1 DESCRIPTION
+
+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
+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.
+
+Currently we have support for MySQL native replication, which is relatively
+easy to install and configure.  We also currently support single master to one
+or more replicants (also called 'slaves' in some documentation).  However the
+framework is not specifically tied to the MySQL framework and supporting other
+replication systems or topographies should be possible.  Please bring your
+patches and ideas to the #dbix-class IRC channel or the mailing list.
+
+For an easy way to start playing with MySQL native replication, see:
+L<MySQL::Sandbox>.
+
+If you are using this with a L<Catalyst> based appplication, you may also wish
+to see more recent updates to L<Catalyst::Model::DBIC::Schema>, which has 
+support for replication configuration options as well.
+
+=head1 REPLICATED STORAGE
+
+By default, when you start L<DBIx::Class>, your Schema (L<DBIx::Class::Schema>)
+is assigned a storage_type, which when fully connected will reflect your
+underlying storage engine as defined by your choosen database driver.  For
+example, if you connect to a MySQL database, your storage_type will be
+L<DBIx::Class::Storage::DBI::mysql>  Your storage type class will contain 
+database specific code to help smooth over the differences between databases
+and let L<DBIx::Class> do its thing.
+
+If you want to use replication, you will override this setting so that the
+replicated storage engine will 'wrap' your underlying storages and present to
+the end programmer a unified interface.  This wrapper storage class will
+delegate method calls to either a master database or one or more replicated
+databases based on if they are read only (by default sent to the replicants)
+or write (reserved for the master).  Additionally, the Replicated storage 
+will monitor the health of your replicants and automatically drop them should
+one exceed configurable parameters.  Later, it can automatically restore a
+replicant when its health is restored.
+
+This gives you a very robust system, since you can add or drop replicants
+and DBIC will automatically adjust itself accordingly.
+
+Additionally, if you need high data integrity, such as when you are executing
+a transaction, replicated storage will automatically delegate all database
+traffic to the master storage.  There are several ways to enable this high
+integrity mode, but wrapping your statements inside a transaction is the easy
+and canonical option. 
+
+=head1 PARTS OF REPLICATED STORAGE
+
+A replicated storage contains several parts.  First, there is the replicated
+storage itself (L<DBIx::Class::Storage::DBI::Replicated>).  A replicated storage
+takes a pool of replicants (L<DBIx::Class::Storage::DBI::Replicated::Pool>)
+and a software balancer (L<DBIx::Class::Storage::DBI::Replicated::Pool>).  The
+balancer does the job of splitting up all the read traffic amongst each
+replicant in the Pool. Currently there are two types of balancers, a Random one
+which chooses a Replicant in the Pool using a naive randomizer algorithm, and a
+First replicant, which just uses the first one in the Pool (and obviously is
+only of value when you have a single replicant).
+
+=head1 REPLICATED STORAGE CONFIGURATION
+
+All the parts of replication can be altered dynamically at runtime, which makes
+it possibly to create a system that automatically scales under load by creating
+more replicants as needed, perhaps using a cloud system such as Amazon EC2.
+However, for common use you can setup your replicated storage to be enabled at
+the time you connect the databases.  The following is a breakdown of how you
+may wish to do this.  Again, if you are using L<Catalyst>, I strongly recommend
+you use (or upgrade to) the latest L<Catalyst::Model::DBIC::Schema>, which makes
+this job even easier.
+
+First, you need to connect your L<DBIx::Class::Schema>.  Let's assume you have
+such a schema called, "MyApp::Schema".
+
+       use MyApp::Schema;
+       my $schema = MyApp::Schema->connect($dsn, $user, $pass);
+
+Next, you need to set the storage_type.
+
+       $schema->storage_type(
+               ::DBI::Replicated' => {
+                       balancer_type => '::Random',
+            balancer_args => {
+                               auto_validate_every => 5,
+                               master_read_weight => 1
+                       },
+                       pool_args => {
+                               maximum_lag =>2,
+                       },
+               }
+       );
+
+Let's break down the settings.  The method L<DBIx::Class::Schema/storage_type>
+takes one mandatory parameter, a scalar value, and an option second value which
+is a Hash Reference of configuration options for that storage.  In this case,
+we are setting the Replicated storage type using '::DBI::Replicated' as the
+first value.  You will only use a different value if you are subclassing the
+replicated storage, so for now just copy that first parameter.
+
+The second parameter contains a hash reference of stuff that gets passed to the
+replicated storage.  L<DBIx::Class::Storage::DBI::Replicated/balancer_type> is
+the type of software load balancer you will use to split up traffic among all
+your replicants.  Right now we have two options, "::Random" and "::First". You
+can review documentation for both at:
+
+L<DBIx::Class::Storage::DBI::Replicated::Balancer::First>,
+L<DBIx::Class::Storage::DBI::Replicated::Balancer::Random>.
+
+In this case we will have three replicants, so the ::Random option is the only
+one that makes sense.
+
+'balancer_args' get passed to the balancer when it's instantiated.  All
+balancers have the 'auto_validate_every' option.  This is the number of seconds
+we allow to pass between validation checks on a load balanced replicant. So
+the higher the number, the more possibility that your reads to the replicant 
+may be inconsistant with what's on the master.  Setting this number too low
+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
+allows you to let the master be read from.  I usually leave this off (default
+is off).
+
+The 'pool_args' are configuration options associated with the replicant pool.
+This object (L<DBIx::Class::Storage::DBI::Replicated::Pool>) manages all the
+declared replicants.  'maximum_lag' is the number of seconds a replicant is
+allowed to lag behind the master before being temporarily removed from the pool.
+Keep in mind that the Balancer option 'auto_validate_every' determins how often
+a replicant is tested against this condition, so the true possible lag can be
+higher than the number you set.  The default is zero.
+
+No matter how low you set the maximum_lag or the auto_validate_every settings,
+there is always the chance that your replicants will lag a bit behind the
+master for the supported replication system built into MySQL.  You can ensure
+reliabily reads by using a transaction, which will force both read and write
+activity to the master, however this will increase the load on your master
+database.
+
+After you've configured the replicated storage, you need to add the connection
+information for the replicants:
+
+       $schema->storage->connect_replicants(
+               [$dsn1, $user, $pass, \%opts],
+               [$dsn2, $user, $pass, \%opts],
+               [$dsn3, $user, $pass, \%opts],
+       );
+
+These replicants should be configured as slaves to the master using the
+instructions for MySQL native replication, or if you are just learning, you
+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 LICENSE
+
+You may distribute this code under the same terms as Perl itself.
+
+=cut
+
+1;
index 2f3b444..44481c4 100644 (file)
@@ -18,7 +18,7 @@ DBIx::Class::Storage::DBI::Replicated::Pool - Manage a pool of replicants
 
 This class is used internally by L<DBIx::Class::Storage::DBI::Replicated>.  You
 shouldn't need to create instances of this class.
-  
+
 =head1 DESCRIPTION
 
 In a replicated storage type, there is at least one replicant to handle the
@@ -34,7 +34,7 @@ This class defines the following attributes.
 This is a number which defines the maximum allowed lag returned by the
 L<DBIx::Class::Storage::DBI/lag_behind_master> method.  The default is 0.  In
 general, this should return a larger number when the replicant is lagging
-behind it's master, however the implementation of this is database specific, so
+behind its master, however the implementation of this is database specific, so
 don't count on this number having a fixed meaning.  For example, MySQL will
 return a number of seconds that the replicating database is lagging.
 
@@ -51,7 +51,7 @@ has 'maximum_lag' => (
 =head2 last_validated
 
 This is an integer representing a time since the last time the replicants were
-validated. It's nothing fancy, just an integer provided via the perl time 
+validated. It's nothing fancy, just an integer provided via the perl L<time|perlfunc/time>
 builtin.
 
 =cut
@@ -89,11 +89,11 @@ A hashref of replicant, with the key being the dsn and the value returning the
 actual replicant storage.  For example if the $dsn element is something like:
 
   "dbi:SQLite:dbname=dbfile"
-  
+
 You could access the specific replicant via:
 
   $schema->storage->replicants->{'dbname=dbfile'}
-  
+
 This attributes also supports the following helper methods:
 
 =over 4
@@ -125,14 +125,15 @@ removes the replicant under $key from the pool
 has 'replicants' => (
   is=>'rw',
   metaclass => 'Collection::Hash',
-  isa=>HashRef['DBIx::Class::Storage::DBI'],
+  isa=>HashRef['Object'],
   default=>sub {{}},
   provides  => {
     'set' => 'set_replicant',
-    'get' => 'get_replicant',            
+    'get' => 'get_replicant',
     'empty' => 'has_replicants',
     'count' => 'num_replicants',
     'delete' => 'delete_replicant',
+    'values' => 'all_replicant_storages',
   },
 );
 
@@ -151,7 +152,7 @@ and store it in the L</replicants> attribute.
 sub connect_replicants {
   my $self = shift @_;
   my $schema = shift @_;
-  
+
   my @newly_created = ();
   foreach my $connect_info (@_) {
     $connect_info = [ $connect_info ]
@@ -169,7 +170,7 @@ sub connect_replicants {
     $self->set_replicant( $key => $replicant);  
     push @newly_created, $replicant;
   }
-  
+
   return @newly_created;
 }
 
index 9c9f1c2..2e9f9dd 100644 (file)
@@ -14,7 +14,7 @@ DBIx::Class::Storage::DBI::Replicated::Replicant - A replicated DBI Storage Role
 =head1 SYNOPSIS
 
 This class is used internally by L<DBIx::Class::Storage::DBI::Replicated>.
-    
+
 =head1 DESCRIPTION
 
 Replicants are DBI Storages that follow a master DBI Storage.  Typically this
index c366ea5..4e75aa2 100644 (file)
@@ -1,32 +1,31 @@
 package # hide from PAUSE
   DBIx::Class::Storage::DBI::Replicated::Types;
 
-=head1 NAME
-
-DBIx::Class::Storage::DBI::Replicated::Types - Types used internally by
-L<DBIx::Class::Storage::DBI::Replicated>
-
-=cut
+# DBIx::Class::Storage::DBI::Replicated::Types - Types used internally by
+# L<DBIx::Class::Storage::DBI::Replicated>
 
 use MooseX::Types
-  -declare => [qw/BalancerClassNamePart Weight/];
+  -declare => [qw/BalancerClassNamePart Weight DBICSchema DBICStorageDBI/];
 use MooseX::Types::Moose qw/ClassName Str Num/;
 
 class_type 'DBIx::Class::Storage::DBI';
 class_type 'DBIx::Class::Schema';
 
+subtype DBICSchema, as 'DBIx::Class::Schema';
+subtype DBICStorageDBI, as 'DBIx::Class::Storage::DBI';
+
 subtype BalancerClassNamePart,
   as ClassName;
-    
+
 coerce BalancerClassNamePart,
   from Str,
   via {
     my $type = $_;
     if($type=~m/^::/) {
       $type = 'DBIx::Class::Storage::DBI::Replicated::Balancer'.$type;
-    }  
-    Class::MOP::load_class($type);  
-    $type;     
+    }
+    Class::MOP::load_class($type);
+    $type;
   };
 
 subtype Weight,
@@ -34,14 +33,12 @@ subtype Weight,
   where { $_ >= 0 },
   message { 'weight must be a decimal greater than 0' };
 
-=head1 AUTHOR
-
-  John Napiorkowski <john.napiorkowski@takkle.com>
-
-=head1 LICENSE
-
-You may distribute this code under the same terms as Perl itself.
-
-=cut
+# AUTHOR
+#
+#  John Napiorkowski <john.napiorkowski@takkle.com>
+#
+# LICENSE
+#
+#  You may distribute this code under the same terms as Perl itself.
 
 1;
index 69a3add..6025739 100644 (file)
@@ -13,7 +13,7 @@ information in trace output
 =head1 SYNOPSIS
 
 This class is used internally by L<DBIx::Class::Storage::DBI::Replicated>.
-    
+
 =head1 DESCRIPTION
 
 This role adds C<DSN: > info to storage debugging output.
@@ -31,7 +31,10 @@ Add C<DSN: > to debugging output.
 around '_query_start' => sub {
   my ($method, $self, $sql, @bind) = @_;
   my $dsn = $self->_dbi_connect_info->[0];
-  $self->$method("DSN: $dsn SQL: $sql", @bind);
+  my($op, $rest) = (($sql=~m/^(\w+)(.+)$/),'NOP', 'NO SQL');
+  my $storage_type = $self->can('active') ? 'REPLICANT' : 'MASTER';
+
+  $self->$method("$op [DSN_$storage_type=$dsn]$rest", @bind);
 };
 
 =head1 ALSO SEE
index 7fada30..1e5f298 100644 (file)
@@ -2,12 +2,14 @@ package DBIx::Class::Storage::DBI::SQLite;
 
 use strict;
 use warnings;
+
+use base qw/DBIx::Class::Storage::DBI/;
+use mro 'c3';
+
 use POSIX 'strftime';
 use File::Copy;
 use File::Spec;
 
-use base qw/DBIx::Class::Storage::DBI/;
-
 sub _dbh_last_insert_id {
   my ($self, $dbh, $source, $col) = @_;
   $dbh->func('last_insert_rowid');
index ec4fcf7..0a2cfb8 100644 (file)
@@ -3,7 +3,11 @@ package DBIx::Class::Storage::DBI::Sybase;
 use strict;
 use warnings;
 
-use base qw/DBIx::Class::Storage::DBI::NoBindVars/;
+use base qw/
+    DBIx::Class::Storage::DBI::Sybase::Base
+    DBIx::Class::Storage::DBI::NoBindVars
+/;
+use mro 'c3';
 
 sub _rebless {
     my $self = shift;
diff --git a/lib/DBIx/Class/Storage/DBI/Sybase/Base.pm b/lib/DBIx/Class/Storage/DBI/Sybase/Base.pm
new file mode 100644 (file)
index 0000000..be57610
--- /dev/null
@@ -0,0 +1,40 @@
+package # hide from PAUSE
+    DBIx::Class::Storage::DBI::Sybase::Base;
+
+use strict;
+use warnings;
+
+use base qw/DBIx::Class::Storage::DBI/;
+use mro 'c3';
+
+=head1 NAME
+
+DBIx::Class::Storage::DBI::Sybase::Base - Common functionality for drivers using
+DBD::Sybase
+
+=cut
+
+sub _ping {
+  my $self = shift;
+
+  my $dbh = $self->_dbh or return 0;
+
+  local $dbh->{RaiseError} = 1;
+  eval {
+    $dbh->do('select 1');
+  };
+
+  return $@ ? 0 : 1;
+}
+
+1;
+
+=head1 AUTHORS
+
+See L<DBIx::Class/CONTRIBUTORS>.
+
+=head1 LICENSE
+
+You may distribute this code under the same terms as Perl itself.
+
+=cut
index dfd7a27..bd833df 100644 (file)
@@ -15,6 +15,7 @@ carp 'Setting of storage_type is redundant as connections through DBD::Sybase'
 
 
 use base qw/DBIx::Class::Storage::DBI::Sybase::Microsoft_SQL_Server/;
+use mro 'c3';
 
 1;
 
index 58ac36f..600db7a 100644 (file)
@@ -4,9 +4,10 @@ use strict;
 use warnings;
 
 use base qw/
+  DBIx::Class::Storage::DBI::Sybase::Base
   DBIx::Class::Storage::DBI::ODBC::Microsoft_SQL_Server
-  DBIx::Class::Storage::DBI::Sybase
 /;
+use mro 'c3';
 
 1;
 
index 221548a..bce1a07 100644 (file)
@@ -3,16 +3,29 @@ package DBIx::Class::Storage::DBI::mysql;
 use strict;
 use warnings;
 
-use base qw/DBIx::Class::Storage::DBI::MultiColumnIn/;
+use base qw/
+  DBIx::Class::Storage::DBI::MultiColumnIn
+  DBIx::Class::Storage::DBI::AmbiguousGlob
+  DBIx::Class::Storage::DBI
+/;
+use mro 'c3';
 
 __PACKAGE__->sql_maker_class('DBIx::Class::SQLAHacks::MySQL');
 
 sub with_deferred_fk_checks {
   my ($self, $sub) = @_;
 
-  $self->dbh->do('SET foreign_key_checks=0');
+  $self->_do_query('SET FOREIGN_KEY_CHECKS = 0');
   $sub->();
-  $self->dbh->do('SET foreign_key_checks=1');
+  $self->_do_query('SET FOREIGN_KEY_CHECKS = 1');
+}
+
+sub connect_call_set_strict_mode {
+  my $self = shift;
+
+  # the @@sql_mode puts back what was previously set on the session handle
+  $self->_do_query(q|SET SQL_MODE = CONCAT('ANSI,TRADITIONAL,ONLY_FULL_GROUP_BY,', @@sql_mode)|);
+  $self->_do_query(q|SET SQL_AUTO_IS_NULL = 0|);
 }
 
 sub _dbh_last_insert_id {
@@ -41,7 +54,7 @@ sub _svp_rollback {
 
     $self->dbh->do("ROLLBACK TO SAVEPOINT $name")
 }
+
 sub is_replicating {
     my $status = shift->dbh->selectrow_hashref('show slave status');
     return ($status->{Slave_IO_Running} eq 'Yes') && ($status->{Slave_SQL_Running} eq 'Yes');
@@ -57,38 +70,30 @@ sub _subq_update_delete {
   return shift->_per_row_update_delete (@_);
 }
 
-# MySql chokes on things like:
-# COUNT(*) FROM (SELECT tab1.col, tab2.col FROM tab1 JOIN tab2 ... )
-# claiming that col is a duplicate column (it loses the table specifiers by
-# the time it gets to the *). Thus for any subquery count we select only the
-# primary keys of the main table in the inner query. This hopefully still
-# hits the indexes and keeps mysql happy.
-# (mysql does not care if the SELECT and the GROUP BY match)
-sub _grouped_count_select {
-  my ($self, $source, $rs_args) = @_;
-  my @pcols = map { join '.', $rs_args->{alias}, $_ } ($source->primary_columns);
-  return @pcols ? \@pcols : $rs_args->{group_by};
-}
-
 1;
 
 =head1 NAME
 
-DBIx::Class::Storage::DBI::mysql - Automatic primary key class for MySQL
+DBIx::Class::Storage::DBI::mysql - Storage::DBI class implementing MySQL specifics
 
 =head1 SYNOPSIS
 
-  # In your table classes
-  __PACKAGE__->load_components(qw/PK::Auto Core/);
-  __PACKAGE__->set_primary_key('id');
+Storage::DBI autodetects the underlying MySQL database, and re-blesses the
+C<$storage> object into this class.
+
+  my $schema = MyDb::Schema->connect( $dsn, $user, $pass, { set_strict_mode => 1 } );
 
 =head1 DESCRIPTION
 
-This class implements autoincrements for MySQL.
+This class implements MySQL specific bits of L<DBIx::Class::Storage::DBI>.
+
+It also provides a one-stop on-connect macro C<set_strict_mode> which sets
+session variables such that MySQL behaves more predictably as far as the
+SQL standard is concerned.
 
 =head1 AUTHORS
 
-Matt S. Trout <mst@shadowcatsystems.co.uk>
+See L<DBIx::Class/CONTRIBUTORS>
 
 =head1 LICENSE
 
index a81f606..c8162bf 100644 (file)
@@ -16,7 +16,7 @@ DBIx::Class::Storage::Statistics - SQL Statistics
 =head1 DESCRIPTION
 
 This class is called by DBIx::Class::Storage::DBI as a means of collecting
-statistics on it's actions.  Using this class alone merely prints the SQL
+statistics on its actions.  Using this class alone merely prints the SQL
 executed, the fact that it completes and begin/end notification for
 transactions.
 
index f060f81..cc18743 100644 (file)
@@ -25,7 +25,7 @@ DBIx::Class::UTF8Columns - Force UTF8 (Unicode) flag on columns
     package Artist;
     __PACKAGE__->load_components(qw/UTF8Columns Core/);
     __PACKAGE__->utf8_columns(qw/name description/);
-    
+
     # then belows return strings with utf8 flag
     $artist->name;
     $artist->get_column('description');
index 97e333c..9f45d1b 100644 (file)
@@ -14,6 +14,7 @@ $DEBUG = 0 unless defined $DEBUG;
 
 use Exporter;
 use SQL::Translator::Utils qw(debug normalize_name);
+use Carp::Clan qw/^SQL::Translator|^DBIx::Class/;
 
 use base qw(Exporter);
 
@@ -34,11 +35,11 @@ sub parse {
     my $dbicschema    = $args->{'DBIx::Class::Schema'} ||  $args->{"DBIx::Schema"} ||$data;
     $dbicschema     ||= $args->{'package'};
     my $limit_sources = $args->{'sources'};
-    
-    die 'No DBIx::Class::Schema' unless ($dbicschema);
+
+    croak 'No DBIx::Class::Schema' unless ($dbicschema);
     if (!ref $dbicschema) {
       eval "use $dbicschema;";
-      die "Can't load $dbicschema ($@)" if($@);
+      croak "Can't load $dbicschema ($@)" if($@);
     }
 
     my $schema      = $tr->schema;
@@ -47,12 +48,11 @@ sub parse {
     $schema->name( ref($dbicschema) . " v" . ($dbicschema->schema_version || '1.x'))
       unless ($schema->name);
 
-    my %seen_tables;
-
     my @monikers = sort $dbicschema->sources;
     if ($limit_sources) {
         my $ref = ref $limit_sources || '';
-        die "'sources' parameter must be an array or hash ref" unless $ref eq 'ARRAY' || ref eq 'HASH';
+        $dbicschema->throw_exception ("'sources' parameter must be an array or hash ref")
+          unless( $ref eq 'ARRAY' || ref eq 'HASH' );
 
         # limit monikers to those specified in 
         my $sources;
@@ -76,21 +76,23 @@ sub parse {
       }
     }
 
+    my %tables;
     foreach my $moniker (sort @table_monikers)
     {
         my $source = $dbicschema->source($moniker);
-        
+        my $table_name = $source->name;
+
         # Skip custom query sources
-        next if ref($source->name);
+        next if ref $table_name;
 
-        # Its possible to have multiple DBIC source using same table
-        next if $seen_tables{$source->name}++;
+        # Its possible to have multiple DBIC sources using the same table
+        next if $tables{$table_name};
 
-        my $table = $schema->add_table(
-                                       name => $source->name,
+        $tables{$table_name}{source} = $source;
+        my $table = $tables{$table_name}{object} = SQL::Translator::Schema::Table->new(
+                                       name => $table_name,
                                        type => 'TABLE',
-                                       ) || die $schema->error;
-        my $colcount = 0;
+                                       );
         foreach my $col ($source->columns)
         {
             # assuming column_info in dbic is the same as DBI (?)
@@ -106,7 +108,8 @@ sub parse {
             if ($colinfo{is_nullable}) {
               $colinfo{default} = '' unless exists $colinfo{default};
             }
-            my $f = $table->add_field(%colinfo) || die $table->error;
+            my $f = $table->add_field(%colinfo)
+              || $dbicschema->throw_exception ($table->error);
         }
         $table->primary_key($source->primary_columns);
 
@@ -125,7 +128,7 @@ sub parse {
         my @rels = $source->relationships();
 
         my %created_FK_rels;
-        
+
         # global add_fk_index set in parser_args
         my $add_fk_index = (exists $args->{add_fk_index} && ($args->{add_fk_index} == 0)) ? 0 : 1;
 
@@ -146,7 +149,7 @@ sub parse {
             my $idx;
             my %other_columns_idx = map {'foreign.'.$_ => ++$idx } $othertable->columns;            
             my @cond = sort { $other_columns_idx{$a} cmp $other_columns_idx{$b} } keys(%{$rel_info->{cond}}); 
-      
+
             # Get the key information, mapping off the foreign/self markers
             my @refkeys = map {/^\w+\.(\w+)$/} @cond;
             my @keys = map {$rel_info->{cond}->{$_} =~ /^\w+\.(\w+)$/} @cond;
@@ -177,7 +180,7 @@ sub parse {
                         $cascade->{$c} = $rel_info->{attrs}{"on_$c"};
                     }
                     else {
-                        warn "SQLT attribute 'on_$c' was supplied for relationship '$moniker/$rel', which does not appear to be a foreign constraint. "
+                        carp "SQLT attribute 'on_$c' was supplied for relationship '$moniker/$rel', which does not appear to be a foreign constraint. "
                             . "If you are sure that SQLT must generate a constraint for this relationship, add 'is_foreign_key_constraint => 1' to the attributes.\n";
                     }
                 }
@@ -195,17 +198,21 @@ sub parse {
                 my $key_test = join("\x00", @keys);
                 next if $created_FK_rels{$rel_table}->{$key_test};
 
-                my $is_deferrable = $rel_info->{attrs}{is_deferrable};
-                
-                # global parser_args add_fk_index param can be overridden on the rel def
-                my $add_fk_index_rel = (exists $rel_info->{attrs}{add_fk_index}) ? $rel_info->{attrs}{add_fk_index} : $add_fk_index;
+                if (scalar(@keys)) {
+
+                  $created_FK_rels{$rel_table}->{$key_test} = 1;
 
+                  my $is_deferrable = $rel_info->{attrs}{is_deferrable};
+
+                  # do not consider deferrable constraints and self-references
+                  # for dependency calculations
+                  if (! $is_deferrable and $rel_table ne $table_name) {
+                    $tables{$table_name}{foreign_table_deps}{$rel_table}++;
+                  }
 
-                $created_FK_rels{$rel_table}->{$key_test} = 1;
-                if (scalar(@keys)) {
                   $table->add_constraint(
                                     type             => 'foreign_key',
-                                    name             => join('_', $table->name, 'fk', @keys),
+                                    name             => join('_', $table_name, 'fk', @keys),
                                     fields           => \@keys,
                                     reference_fields => \@refkeys,
                                     reference_table  => $rel_table,
@@ -213,10 +220,13 @@ sub parse {
                                     on_update        => uc ($cascade->{update} || ''),
                                     (defined $is_deferrable ? ( deferrable => $is_deferrable ) : ()),
                   );
-                    
+
+                  # global parser_args add_fk_index param can be overridden on the rel def
+                  my $add_fk_index_rel = (exists $rel_info->{attrs}{add_fk_index}) ? $rel_info->{attrs}{add_fk_index} : $add_fk_index;
+
                   if ($add_fk_index_rel) {
                       my $index = $table->add_index(
-                                                    name   => join('_', $table->name, 'idx', @keys),
+                                                    name   => join('_', $table_name, 'idx', @keys),
                                                     fields => \@keys,
                                                     type   => 'NORMAL',
                                                     );
@@ -224,31 +234,48 @@ sub parse {
               }
             }
         }
-               
-        $source->_invoke_sqlt_deploy_hook($table);
+
     }
 
+    # attach the tables to the schema in dependency order
+    my $dependencies = {
+      map { $_ => _resolve_deps ($_, \%tables) } (keys %tables)
+    };
+    for my $table (sort
+      {
+        keys %{$dependencies->{$a} || {} } <=> keys %{ $dependencies->{$b} || {} }
+          ||
+        $a cmp $b
+      }
+      (keys %tables)
+    ) {
+      $schema->add_table ($tables{$table}{object});
+      $tables{$table}{source} -> _invoke_sqlt_deploy_hook( $tables{$table}{object} );
+    }
+
+
+    my %views;
     foreach my $moniker (sort @view_monikers)
     {
         my $source = $dbicschema->source($moniker);
+        my $view_name = $source->name;
+
         # Skip custom query sources
-        next if ref($source->name);
+        next if ref $view_name;
 
         # Its possible to have multiple DBIC source using same table
-        next if $seen_tables{$source->name}++;
+        next if $views{$view_name}++;
 
-        my $view = $schema->add_view(
-          name => $source->name,
+        my $view = $schema->add_view (
+          name => $view_name,
           fields => [ $source->columns ],
           $source->view_definition ? ( 'sql' => $source->view_definition ) : ()
-        );
-        if ($source->result_class->can('sqlt_deploy_hook')) {
-          $source->result_class->sqlt_deploy_hook($view);
-        }
+        ) || $dbicschema->throw_exception ($schema->error);
 
         $source->_invoke_sqlt_deploy_hook($view);
     }
 
+
     if ($dbicschema->can('sqlt_deploy_hook')) {
       $dbicschema->sqlt_deploy_hook($schema);
     }
@@ -256,6 +283,41 @@ sub parse {
     return 1;
 }
 
+#
+# Quick and dirty dependency graph calculator
+#
+sub _resolve_deps {
+  my ($table, $tables, $seen) = @_;
+
+  my $ret = {};
+  $seen ||= {};
+
+  # copy and bump all deps by one (so we can reconstruct the chain)
+  my %seen = map { $_ => $seen->{$_} + 1 } (keys %$seen);
+  $seen{$table} = 1;
+
+  for my $dep (keys %{$tables->{$table}{foreign_table_deps}} ) {
+
+    if ($seen->{$dep}) {
+
+      # warn and remove the circular constraint so we don't get flooded with the same warning over and over
+      #carp sprintf ("Circular dependency detected, schema may not be deployable:\n%s\n",
+      #  join (' -> ', (sort { $seen->{$b} <=> $seen->{$a} } (keys %$seen) ), $table, $dep )
+      #);
+      #delete $tables->{$table}{foreign_table_deps}{$dep};
+
+      return {};
+    }
+
+    my $subdeps = _resolve_deps ($dep, $tables, \%seen);
+    $ret->{$_} += $subdeps->{$_} for ( keys %$subdeps );
+
+    ++$ret->{$dep};
+  }
+
+  return $ret;
+}
+
 1;
 
 =head1 NAME
@@ -275,7 +337,7 @@ from a DBIx::Class::Schema instance
  ## Standalone
  use MyApp::Schema;
  use SQL::Translator;
+
  my $schema = MyApp::Schema->connect;
  my $trans  = SQL::Translator->new (
       parser      => 'SQL::Translator::Parser::DBIx::Class',
@@ -291,7 +353,7 @@ This class requires L<SQL::Translator> installed to work.
 C<SQL::Translator::Parser::DBIx::Class> reads a DBIx::Class schema,
 interrogates the columns, and stuffs it all in an $sqlt_schema object.
 
-It's primary use is in deploying database layouts described as a set
+Its primary use is in deploying database layouts described as a set
 of L<DBIx::Class> classes, to a database. To do this, see
 L<DBIx::Class::Schema/deploy>.
 
index 4132c73..5099a13 100644 (file)
@@ -128,7 +128,7 @@ __PACKAGE__->table('${tname}');
                 $tableextras{$table->name} .= "\n__PACKAGE__->belongs_to('" .
                     $cont->fields->[0]->name . "', '" .
                     "${dbixschema}::" . $cont->reference_table . "');\n";
-                
+
                 my $other = "\n__PACKAGE__->has_many('" .
                     "get_" . $table->name. "', '" .
                     "${dbixschema}::" . $table->name. "', '" .
index fe8516b..65e61a1 100644 (file)
@@ -106,6 +106,7 @@ my $exceptions = {
     'DBIx::Class::ResultSetManager'                     => { skip => 1 },
     'DBIx::Class::ResultSourceProxy'                    => { skip => 1 },
     'DBIx::Class::Storage::DBI'                         => { skip => 1 },
+    'DBIx::Class::Storage::DBI::Replicated::Types'      => { skip => 1 },
     'DBIx::Class::Storage::DBI::DB2'                    => { skip => 1 },
     'DBIx::Class::Storage::DBI::MSSQL'                  => { skip => 1 },
     'DBIx::Class::Storage::DBI::Sybase::MSSQL'          => { skip => 1 },
@@ -116,7 +117,9 @@ my $exceptions = {
     'DBIx::Class::Storage::DBI::Pg'                     => { skip => 1 },
     'DBIx::Class::Storage::DBI::SQLite'                 => { skip => 1 },
     'DBIx::Class::Storage::DBI::mysql'                  => { skip => 1 },
+    'DBIx::Class::SQLAHacks'                            => { skip => 1 },
     'DBIx::Class::SQLAHacks::MySQL'                     => { skip => 1 },
+    'DBIx::Class::SQLAHacks::MSSQL'                     => { skip => 1 },
     'SQL::Translator::Parser::DBIx::Class'              => { skip => 1 },
     'SQL::Translator::Producer::DBIx::Class::File'      => { skip => 1 },
 
index 9f0da40..4cd85a0 100644 (file)
@@ -4,12 +4,7 @@ use strict;
 use warnings;
 use Test::More;
 
-BEGIN {
-    eval "use DBD::SQLite";
-    plan $@
-        ? ( skip_all => 'needs DBD::SQLite for testing' )
-        : ( tests => 12 );
-}
+plan tests => 12;
 
 use lib qw(t/lib);
 
index f63b74c..242e623 100644 (file)
@@ -10,11 +10,30 @@ my $schema = DBICTest->init_schema;
 
 # Trick the sqlite DB to use Top limit emulation
 # We could test all of this via $sq->$op directly,
-# but some conditions needs a $rsrc
+# but some conditions need a $rsrc
 delete $schema->storage->_sql_maker->{_cached_syntax};
 $schema->storage->_sql_maker->limit_dialect ('Top');
 
-my $rs = $schema->resultset ('FourKeys')->search ({}, { rows => 1, offset => 3 });
+my $rs = $schema->resultset ('BooksInLibrary')->search ({}, { prefetch => 'owner', rows => 1, offset => 3 });
+
+sub default_test_order {
+   my $order_by = shift;
+   is_same_sql_bind(
+      $rs->search ({}, {order_by => $order_by})->as_query,
+      "(SELECT
+        TOP 1 me__id, source, owner, title, price, owner__id, name FROM
+         (SELECT
+           TOP 4 me.id AS me__id, me.source, me.owner, me.title, me.price, owner.id AS owner__id, owner.name
+           FROM books me
+           JOIN owners owner ON
+           owner.id = me.owner
+           WHERE ( source = ? )
+           ORDER BY me__id ASC
+         ) me ORDER BY me__id DESC
+       )",
+    [ [ source => 'Library' ] ],
+  );
+}
 
 sub test_order {
   my $args = shift;
@@ -26,24 +45,29 @@ sub test_order {
 
   is_same_sql_bind(
     $rs->search ({}, {order_by => $args->{order_by}})->as_query,
-    "(
-      SELECT * FROM (
-        SELECT TOP 1 * FROM (
-          SELECT TOP 4 me.foo, me.bar, me.hello, me.goodbye, me.sensors, me.read_count FROM fourkeys me ORDER BY $args->{order_inner}
-        ) foo ORDER BY $args->{order_outer}
-      ) bar
-      $req_order
+    "(SELECT
+      me__id, source, owner, title, price, owner__id, name FROM
+      (SELECT
+        TOP 1 me__id, source, owner, title, price, owner__id, name FROM
+         (SELECT
+           TOP 4 me.id AS me__id, me.source, me.owner, me.title, me.price, owner.id AS owner__id, owner.name FROM
+           books me
+           JOIN owners owner ON owner.id = me.owner
+           WHERE ( source = ? )
+           ORDER BY $args->{order_inner}
+         ) me ORDER BY $args->{order_outer}
+      ) me $req_order
     )",
-    [],
+    [ [ source => 'Library' ] ],
   );
 }
 
 my @tests = (
   {
-    order_by => \ 'foo DESC',
+    order_by => \'foo DESC',
     order_req => 'foo DESC',
     order_inner => 'foo DESC',
-    order_outer => 'foo ASC' 
+    order_outer => 'foo ASC'
   },
   {
     order_by => { -asc => 'foo'  },
@@ -91,48 +115,38 @@ my @tests = (
     order_inner => 'foo ASC, bar DESC, hello ASC, sensors ASC',
     order_outer => 'foo DESC, bar ASC, hello DESC, sensors DESC',
   },
-  {
-    order_by => undef,
-    order_req => undef,
-    order_inner => 'foo ASC, bar ASC, hello ASC, goodbye ASC',
-    order_outer => 'foo DESC, bar DESC, hello DESC, goodbye DESC',
-  },
-  {
-    order_by => '',
-    order_req => undef,
-    order_inner => 'foo ASC, bar ASC, hello ASC, goodbye ASC',
-    order_outer => 'foo DESC, bar DESC, hello DESC, goodbye DESC',
-  },
-  {
-    order_by => {},
-    order_req => undef,
-    order_inner => 'foo ASC, bar ASC, hello ASC, goodbye ASC',
-    order_outer => 'foo DESC, bar DESC, hello DESC, goodbye DESC',
-  },
-  {
-    order_by => [],
-    order_req => undef,
-    order_inner => 'foo ASC, bar ASC, hello ASC, goodbye ASC',
-    order_outer => 'foo DESC, bar DESC, hello DESC, goodbye DESC',
-  },
 );
 
-plan (tests => scalar @tests + 1);
+my @default_tests = ( undef, '', {}, [] );
+
+plan (tests => scalar @tests + scalar @default_tests + 1);
 
 test_order ($_) for @tests;
+default_test_order ($_) for @default_tests;
+
 
 is_same_sql_bind (
-  $rs->search ({}, { group_by => 'bar', order_by => 'bar' })->as_query,
-  '(
-    SELECT * FROM
-    (
-      SELECT TOP 1 * FROM
-      (
-        SELECT TOP 4  me.foo, me.bar, me.hello, me.goodbye, me.sensors, me.read_count FROM fourkeys me GROUP BY bar ORDER BY bar ASC
-      ) AS foo
-      ORDER BY bar DESC
-    ) AS bar
-    ORDER BY bar
-  )',
-  [],
+  $rs->search ({}, { group_by => 'title', order_by => 'title' })->as_query,
+'(SELECT
+me.id, me.source, me.owner, me.title, me.price, owner.id, owner.name FROM
+   ( SELECT
+      id, source, owner, title, price FROM
+      ( SELECT
+         TOP 1 id, source, owner, title, price FROM
+         ( SELECT
+            TOP 4 me.id, me.source, me.owner, me.title, me.price FROM
+            books me  JOIN
+            owners owner ON owner.id = me.owner
+            WHERE ( source = ? )
+            GROUP BY title
+            ORDER BY title ASC
+         ) me
+         ORDER BY title DESC
+      ) me
+      ORDER BY title
+   ) me  JOIN
+   owners owner ON owner.id = me.owner WHERE
+   ( source = ? )
+   ORDER BY title)' ,
+  [ [ source => 'Library' ], [ source => 'Library' ] ],
 );
index 6ed3125..1c03591 100644 (file)
@@ -7,7 +7,7 @@ use lib qw(t/lib);
 use DBICTest;
 my $schema = DBICTest->init_schema();
 
-plan tests => 16;
+plan tests => 19;
 
 # select from a class with resultset_attributes
 my $resultset = $schema->resultset('BooksInLibrary');
@@ -72,3 +72,14 @@ eval {$collection->add_to_objects({ value => "Wheel", type => "round" })};
 if ($@) { print $@ }
 ok( !$@, 'many_to_many add_to_$rel($hash) did not throw');
 is($round_objects->count, $round_count+1, 'many_to_many add_to_$rel($hash) count correct');
+
+# test set_$rel
+$round_count = $round_objects->count();
+$pointy_count = $pointy_objects->count();
+my @all_pointy_objects = $pointy_objects->all;
+# doing a set on pointy objects with its current set should not change any counts
+eval {$collection->set_pointy_objects(\@all_pointy_objects)};
+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');
index 629d49d..d4fc083 100644 (file)
@@ -9,9 +9,9 @@ use DBIC::SqlMakerTest;
 
 my $schema = DBICTest->init_schema();
 
-plan tests => 106;
+plan tests => 103;
 
-eval { require DateTime::Format::MySQL };
+eval { require DateTime::Format::SQLite };
 my $NO_DTFM = $@ ? 1 : 0;
 
 my @art = $schema->resultset("Artist")->search({ }, { order_by => 'name DESC'});
@@ -195,7 +195,7 @@ is( $schema->resultset("Track")->find(100)->title, 'Insert or Update - updated',
 
 # get_inflated_columns w/relation and accessor alias
 SKIP: {
-    skip "This test requires DateTime::Format::MySQL", 8 if $NO_DTFM;
+    skip "This test requires DateTime::Format::SQLite", 8 if $NO_DTFM;
 
     isa_ok($new->updated_date, 'DateTime', 'have inflated object via accessor');
     my %tdata = $new->get_inflated_columns;
@@ -229,20 +229,6 @@ my $collapsed_or_rs = $or_rs->search ({}, { distinct => 1 }); # induce collapse
 is ($collapsed_or_rs->all, 4, 'Collapsed joined search with OR returned correct number of rows');
 is ($collapsed_or_rs->count, 4, 'Collapsed search count with OR ok');
 
-my $pref_or_rs = $collapsed_or_rs->search ({}, { prefetch => [qw/tags/] });
-is_same_sql_bind (
-  $pref_or_rs->as_query,
-  '(SELECT me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track, tags.tagid, tags.cd, tags.tag FROM cd me LEFT JOIN tags tags ON tags.cd = me.cdid WHERE ( ( tags.tag = ? OR tags.tag = ? ) ) GROUP BY me.cdid, me.artist, me.title, me.year, me.genreid, me.single_track, tags.tagid, tags.cd, tags.tag ORDER BY cdid, tags.cd, tags.tag)',
-  [
-    [ 'tags.tag' => 'Cheesy' ],
-    [ 'tags.tag' => 'Blue' ],
-  ],
-  'Prefetch + distinct resulted in correct group_by',
-);
-is ($pref_or_rs->all, 4, 'Prefetched grouped search with OR returned correct number of rows');
-is ($pref_or_rs->count, 4, 'Prefetched grouped count with OR ok');
-
-
 {
   my $tcount = $schema->resultset('Track')->search(
     {},
@@ -389,7 +375,7 @@ lives_ok (sub { my $newlink = $newbook->link}, "stringify to false value doesn't
 
 # test get_inflated_columns with objects
 SKIP: {
-    skip "This test requires DateTime::Format::MySQL", 5 if $NO_DTFM;
+    skip "This test requires DateTime::Format::SQLite", 5 if $NO_DTFM;
     my $event = $schema->resultset('Event')->search->first;
     my %edata = $event->get_inflated_columns;
     is($edata{'id'}, $event->id, 'got id');
index 17b05c3..6a6aabc 100644 (file)
@@ -84,3 +84,19 @@ is($p->(), 10, 'default rows is 10');
 $schema->default_resultset_attributes({ rows => 5 });
 
 is($p->(), 5, 'default rows is 5');
+
+# test page with offset
+$it = $schema->resultset('CD')->search({}, {
+    rows => 2,
+    page => 2,
+    offset => 1,
+    order_by => 'cdid'
+});
+
+my $row = $schema->resultset('CD')->search({}, {
+    order_by => 'cdid', 
+    offset => 3,
+    rows => 1
+})->single;
+
+is($row->cdid, $it->first->cdid, 'page with offset');
index 88d6ea2..031529c 100644 (file)
@@ -155,41 +155,38 @@ SKIP: {
     is_deeply($type_info, $test_type_info, 'columns_info_for - column data types');
 }
 
+my $cd = $schema->resultset ('CD')->create ({});
+my $producer = $schema->resultset ('Producer')->create ({});
+lives_ok { $cd->set_producers ([ $producer ]) } 'set_relationship doesnt die';
+
+
 ## Can we properly deal with the null search problem?
 ##
 ## Only way is to do a SET SQL_AUTO_IS_NULL = 0; on connect
 ## But I'm not sure if we should do this or not (Ash, 2008/06/03)
+#
+# There is now a built-in function to do this, test that everything works
+# with it (ribasushi, 2009/07/03)
 
 NULLINSEARCH: {
-    
-    ok my $artist1_rs = $schema->resultset('Artist')->search({artistid=>6666})
-    => 'Created an artist resultset of 6666';
-    
+    my $ansi_schema = DBICTest::Schema->connect ($dsn, $user, $pass, { on_connect_call => 'set_strict_mode' });
+
+    $ansi_schema->resultset('Artist')->create ({ name => 'last created artist' });
+
+    ok my $artist1_rs = $ansi_schema->resultset('Artist')->search({artistid=>6666})
+      => 'Created an artist resultset of 6666';
+
     is $artist1_rs->count, 0
-    => 'Got no returned rows';
-    
-    ok my $artist2_rs = $schema->resultset('Artist')->search({artistid=>undef})
-    => 'Created an artist resultset of undef';
-    
-    TODO: {
-       local $TODO = "need to fix the row count =1 when select * from table where pk IS NULL problem";
-           is $artist2_rs->count, 0
-           => 'got no rows';           
-    }
+      => 'Got no returned rows';
 
-    my $artist = $artist2_rs->single;
-    
-    is $artist => undef
-    => 'Nothing Found!';
-}
-    
-my $cd = $schema->resultset ('CD')->create ({});
+    ok my $artist2_rs = $ansi_schema->resultset('Artist')->search({artistid=>undef})
+      => 'Created an artist resultset of undef';
 
-my $producer = $schema->resultset ('Producer')->create ({});
+    is $artist2_rs->count, 0
+      => 'got no rows';
 
-lives_ok { $cd->set_producers ([ $producer ]) } 'set_relationship doesnt die';
+    my $artist = $artist2_rs->single;
 
-# clean up our mess
-END {
-    #$dbh->do("DROP TABLE artist") if $dbh;
+    is $artist => undef
+      => 'Nothing Found!';
 }
index 05470a6..a5b5ac4 100644 (file)
--- a/t/72pg.t
+++ b/t/72pg.t
@@ -144,7 +144,9 @@ like($artistid_defval,
 is_deeply($type_info, $test_type_info,
           'columns_info_for - column data types');
 
-{
+SKIP: {
+  skip "Need DBD::Pg 2.9.2 or newer for array tests", 4 if $DBD::Pg::VERSION < 2.009002;
+
   lives_ok {
     $schema->resultset('ArrayTest')->create({
       arrayfield => [1, 2],
index 2e73050..a6f3ed9 100644 (file)
@@ -40,7 +40,7 @@ plan skip_all => 'Set $ENV{DBICTEST_ORA_DSN}, _USER and _PASS to run this test.
   ' as well as following sequences: \'pkid1_seq\', \'pkid2_seq\' and \'nonpkid_seq\''
   unless ($dsn && $user && $pass);
 
-plan tests => 34;
+plan tests => 35;
 
 DBICTest::Schema->load_classes('ArtistFQN');
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
@@ -113,14 +113,18 @@ $new = $schema->resultset('ArtistFQN')->create( { name => 'bar' } );
 is( $new->artistid, 2, "Oracle Auto-PK worked with fully-qualified tablename" );
 
 # test join with row count ambiguity
+
 my $cd = $schema->resultset('CD')->create({ cdid => 1, artist => 1, title => 'EP C', year => '2003' });
-my $track = $schema->resultset('Track')->create({ trackid => 1, cd => 1, position => 1, title => 'Track1' });
+my $track = $schema->resultset('Track')->create({ trackid => 1, cd => 1,
+    position => 1, title => 'Track1' });
 my $tjoin = $schema->resultset('Track')->search({ 'me.title' => 'Track1'},
         { join => 'cd',
           rows => 2 }
 );
 
-is($tjoin->next->title, 'Track1', "ambiguous column ok");
+ok(my $row = $tjoin->next);
+
+is($row->title, 'Track1', "ambiguous column ok");
 
 # check count distinct with multiple columns
 my $other_track = $schema->resultset('Track')->create({ trackid => 2, cd => 1, position => 1, title => 'Track2' });
index f53d49f..fa8f137 100644 (file)
@@ -1,18 +1,20 @@
 use strict;
-use warnings;  
+use warnings;
 
 use Test::More;
+use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
+use DBIC::SqlMakerTest;
 
 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);
 
-plan tests => 13;
+plan tests => 33;
 
-my $schema = DBICTest::Schema->connect($dsn, $user, $pass, {AutoCommit => 1});
+my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
 
 {
   no warnings 'redefine';
@@ -47,7 +49,7 @@ SQL
 my %seen_id;
 
 # fresh $schema so we start unconnected
-$schema = DBICTest::Schema->connect($dsn, $user, $pass, {AutoCommit => 1});
+$schema = DBICTest::Schema->connect($dsn, $user, $pass);
 
 # test primary key handling
 my $new = $schema->resultset('Artist')->create({ name => 'foo' });
@@ -73,10 +75,204 @@ $it->next;
 is( $it->next->name, "Artist 2", "iterator->next ok" );
 is( $it->next, undef, "next past end of resultset ok" );
 
+# test MONEY type
+$schema->storage->dbh_do (sub {
+    my ($storage, $dbh) = @_;
+    eval { $dbh->do("DROP TABLE money_test") };
+    $dbh->do(<<'SQL');
+
+CREATE TABLE money_test (
+   id INT IDENTITY PRIMARY KEY,
+   amount MONEY NULL
+)
+
+SQL
+
+});
+
+my $rs = $schema->resultset('Money');
+
+my $row;
+lives_ok {
+  $row = $rs->create({ amount => 100 });
+} 'inserted a money value';
+
+is $rs->find($row->id)->amount, '100.00', 'money value round-trip';
+
+lives_ok {
+  $row->update({ amount => 200 });
+} 'updated a money value';
+
+is $rs->find($row->id)->amount, '200.00', 'updated money value round-trip';
+
+lives_ok {
+  $row->update({ amount => undef });
+} 'updated a money value to NULL';
+
+is $rs->find($row->id)->amount, undef,'updated money value to NULL round-trip';
+
+$schema->storage->dbh_do (sub {
+    my ($storage, $dbh) = @_;
+    eval { $dbh->do("DROP TABLE Owners") };
+    eval { $dbh->do("DROP TABLE Books") };
+    $dbh->do(<<'SQL');
+
+
+CREATE TABLE Books (
+   id INT IDENTITY (1, 1) NOT NULL,
+   source VARCHAR(100),
+   owner INT,
+   title VARCHAR(10),
+   price INT NULL
+)
+
+CREATE TABLE Owners (
+   id INT IDENTITY (1, 1) NOT NULL,
+   name VARCHAR(100),
+)
+
+SQL
+
+});
+
+lives_ok ( sub {
+  $schema->populate ('Owners', [
+    [qw/id  name  /],
+    [qw/1   wiggle/],
+    [qw/2   woggle/],
+    [qw/3   boggle/],
+    [qw/4   fREW/],
+    [qw/5   fRIOUX/],
+    [qw/6   fROOH/],
+    [qw/7   fRUE/],
+    [qw/8   fISMBoC/],
+    [qw/9   station/],
+    [qw/10   mirror/],
+    [qw/11   dimly/],
+    [qw/12   face_to_face/],
+    [qw/13   icarus/],
+    [qw/14   dream/],
+    [qw/15   dyrstyggyr/],
+  ]);
+}, 'populate with PKs supplied ok' );
+
+lives_ok ( sub {
+  $schema->populate ('BooksInLibrary', [
+    [qw/source  owner title   /],
+    [qw/Library 1     secrets0/],
+    [qw/Library 1     secrets1/],
+    [qw/Eatery  1     secrets2/],
+    [qw/Library 2     secrets3/],
+    [qw/Library 3     secrets4/],
+    [qw/Eatery  3     secrets5/],
+    [qw/Library 4     secrets6/],
+    [qw/Library 5     secrets7/],
+    [qw/Eatery  5     secrets8/],
+    [qw/Library 6     secrets9/],
+    [qw/Library 7     secrets10/],
+    [qw/Eatery  7     secrets11/],
+    [qw/Library 8     secrets12/],
+  ]);
+}, 'populate without PKs supplied ok' );
+
+#
+# try a prefetch on tables with identically named columns
+#
+
+# set quote char - make sure things work while quoted
+$schema->storage->_sql_maker->{quote_char} = [qw/[ ]/];
+$schema->storage->_sql_maker->{name_sep} = '.';
+
+{
+  # try a ->has_many direction
+  my $owners = $schema->resultset ('Owners')->search ({
+      'books.id' => { '!=', undef }
+    }, {
+      prefetch => 'books',
+      order_by => 'name',
+      rows     => 3,  # 8 results total
+    });
+
+  is ($owners->page(1)->all, 3, 'has_many prefetch returns correct number of rows');
+  is ($owners->page(1)->count, 3, 'has-many prefetch returns correct count');
+
+  TODO: {
+    local $TODO = 'limit past end of resultset problem';
+    is ($owners->page(3)->all, 2, 'has_many prefetch returns correct number of rows');
+    is ($owners->page(3)->count, 2, 'has-many prefetch returns correct count');
+    is ($owners->page(3)->count_rs->next, 2, 'has-many prefetch returns correct count_rs');
+
+    # make sure count does not become overly complex FIXME
+    is_same_sql_bind (
+      $owners->page(3)->count_rs->as_query,
+      '(
+        SELECT COUNT( * )
+          FROM (
+            SELECT TOP 3 [me].[id]
+              FROM [owners] [me]
+              LEFT JOIN [books] [books] ON [books].[owner] = [me].[id]
+            WHERE ( [books].[id] IS NOT NULL )
+            GROUP BY [me].[id]
+            ORDER BY [me].[id] DESC
+          ) [count_subq]
+      )',
+      [],
+    );
+  }
+
+  # try a ->belongs_to direction (no select collapse, group_by should work)
+  my $books = $schema->resultset ('BooksInLibrary')->search ({
+      'owner.name' => [qw/wiggle woggle/],
+    }, {
+      distinct => 1,
+      prefetch => 'owner',
+      rows     => 2,  # 3 results total
+      order_by => { -desc => 'owner' },
+      # there is no sane way to order by the right side of a grouped prefetch currently :(
+      #order_by => { -desc => 'owner.name' },
+    });
+
+
+  is ($books->page(1)->all, 2, 'Prefetched grouped search returns correct number of rows');
+  is ($books->page(1)->count, 2, 'Prefetched grouped search returns correct count');
+
+  TODO: {
+    local $TODO = 'limit past end of resultset problem';
+    is ($books->page(2)->all, 1, 'Prefetched grouped search returns correct number of rows');
+    is ($books->page(2)->count, 1, 'Prefetched grouped search returns correct count');
+    is ($books->page(2)->count_rs->next, 1, 'Prefetched grouped search returns correct count_rs');
+
+    # make sure count does not become overly complex FIXME
+    is_same_sql_bind (
+      $books->page(2)->count_rs->as_query,
+      '(
+        SELECT COUNT( * )
+          FROM (
+            SELECT TOP 2 [me].[id]
+              FROM [books] [me]
+              JOIN [owners] [owner] ON [owner].[id] = [me].[owner]
+            WHERE ( ( ( [owner].[name] = ? OR [owner].[name] = ? ) AND [source] = ? ) )
+            GROUP BY [me].[id], [me].[source], [me].[owner], [me].[title], [me].[price], [owner].[id], [owner].[name]
+            ORDER BY [me].[id] DESC
+          ) [count_subq]
+      )',
+      [
+        [ 'owner.name' => 'wiggle' ],
+        [ 'owner.name' => 'woggle' ],
+        [ 'source' => 'Library' ],
+      ],
+    );
+  }
+
+}
 
 # clean up our mess
 END {
-    my $dbh = eval { $schema->storage->_dbh };
-    $dbh->do('DROP TABLE artist') if $dbh;
+    if (my $dbh = eval { $schema->storage->_dbh }) {
+      $dbh->do('DROP TABLE artist');
+      $dbh->do('DROP TABLE money_test');
+      $dbh->do('DROP TABLE Books');
+      $dbh->do('DROP TABLE Owners');
+    }
 }
-
+# vim:sw=2 sts=2
index f09862f..9fc87f0 100644 (file)
@@ -2,6 +2,7 @@ use strict;
 use warnings;  
 
 use Test::More;
+use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
 
@@ -10,13 +11,21 @@ my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_SYBASE_${_}" } qw/DSN USER PASS/}
 plan skip_all => 'Set $ENV{DBICTEST_SYBASE_DSN}, _USER and _PASS to run this test'
   unless ($dsn && $user);
 
-plan tests => 12;
+plan tests => 13;
 
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass, {AutoCommit => 1});
 
+# start disconnected to test reconnection
 $schema->storage->ensure_connected;
+$schema->storage->_dbh->disconnect;
+
 isa_ok( $schema->storage, 'DBIx::Class::Storage::DBI::Sybase' );
 
+my $dbh;
+lives_ok (sub {
+  $dbh = $schema->storage->dbh;
+}, 'reconnect works');
+
 $schema->storage->dbh_do (sub {
     my ($storage, $dbh) = @_;
     eval { $dbh->do("DROP TABLE artist") };
index 49f7967..cbaffc0 100644 (file)
@@ -1,7 +1,15 @@
 use strict;
 use warnings;  
 
+# use this if you keep a copy of DBD::Sybase linked to FreeTDS somewhere else
+BEGIN {
+  if (my $lib_dirs = $ENV{DBICTEST_MSSQL_PERL5LIB}) {
+    unshift @INC, $_ for split /:/, $lib_dirs;
+  }
+}
+
 use Test::More;
+use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
 
@@ -10,15 +18,22 @@ 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 tests => 6;
+plan tests => 13;
 
 my $schema = DBICTest::Schema->clone;
 $schema->connection($dsn, $user, $pass);
 
-my $dbh = $schema->storage->dbh;
+# start disconnected to test reconnection
+$schema->storage->ensure_connected;
+$schema->storage->_dbh->disconnect;
 
 isa_ok($schema->storage, 'DBIx::Class::Storage::DBI::Sybase::Microsoft_SQL_Server');
 
+my $dbh;
+lives_ok (sub {
+  $dbh = $schema->storage->dbh;
+}, 'reconnect works');
+
 $dbh->do("IF OBJECT_ID('artist', 'U') IS NOT NULL
     DROP TABLE artist");
 $dbh->do("IF OBJECT_ID('cd', 'U') IS NOT NULL
@@ -59,10 +74,48 @@ $it->next;
 $it->next;
 is( $it->next, undef, "next past end of resultset ok" );
 
+# test MONEY column support
+$schema->storage->dbh_do (sub {
+    my ($storage, $dbh) = @_;
+    eval { $dbh->do("DROP TABLE money_test") };
+    $dbh->do(<<'SQL');
+
+CREATE TABLE money_test (
+   id INT IDENTITY PRIMARY KEY,
+   amount MONEY NULL
+)
+
+SQL
+
+});
+
+my $rs = $schema->resultset('Money');
+
+my $row;
+lives_ok {
+  $row = $rs->create({ amount => 100 });
+} 'inserted a money value';
+
+is $rs->find($row->id)->amount, 100, 'money value round-trip';
+
+lives_ok {
+  $row->update({ amount => 200 });
+} 'updated a money value';
+
+is $rs->find($row->id)->amount, 200, 'updated money value round-trip';
+
+lives_ok {
+  $row->update({ amount => undef });
+} 'updated a money value to NULL';
+
+is $rs->find($row->id)->amount, undef,'updated money value to NULL round-trip';
+
 # clean up our mess
 END {
     $dbh->do("IF OBJECT_ID('artist', 'U') IS NOT NULL DROP TABLE artist")
         if $dbh;
     $dbh->do("IF OBJECT_ID('cd', 'U') IS NOT NULL DROP TABLE cd")
         if $dbh;
+    $dbh->do("IF OBJECT_ID('money_test', 'U') IS NOT NULL DROP TABLE money_test")
+        if $dbh;
 }
index 45f50c1..91b226c 100644 (file)
@@ -8,10 +8,9 @@ use DBICTest;
 my $schema = DBICTest->init_schema();
 
 my $queries;
-$schema->storage->debugcb( sub{ $queries++ } );
+my $debugcb = sub{ $queries++ };
+my $sdebug = $schema->storage->debug;
 
-eval "use DBD::SQLite";
-plan skip_all => 'needs DBD::SQLite for testing' if $@;
 plan tests => 23;
 
 my $rs = $schema->resultset("Artist")->search(
@@ -46,6 +45,7 @@ $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 ) {}
@@ -53,7 +53,8 @@ $artist = $rs->first();
 
 is( $queries, 1, 'revisiting a row does not issue a query when cache => 1' );
 
-$schema->storage->debug(0);
+$schema->storage->debug($sdebug);
+$schema->storage->debugcb (undef);
 
 my @a = $schema->resultset("Artist")->search(
   { },
@@ -78,6 +79,7 @@ use Data::Dumper; $Data::Dumper::Deparse = 1;
 # start test for prefetch SELECT count
 $queries = 0;
 $schema->storage->debug(1);
+$schema->storage->debugcb ($debugcb);
 
 $artist = $rs->first;
 $rs->reset();
@@ -99,7 +101,8 @@ is( $artist->count_related('cds'), 3, 'artist->count_related returns correct val
 
 is($queries, 1, 'only one SQL statement executed');
 
-$schema->storage->debug(0);
+$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,18 +134,21 @@ 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');
 
-$schema->storage->debug(0);
+$schema->storage->debug($sdebug);
+$schema->storage->debugcb (undef);
 
 my @objs;
 #$artist = $rs->find(1);
 
 $queries = 0;
 $schema->storage->debug(1);
+$schema->storage->debugcb ($debugcb);
 
 my $cds = $artist->cds;
 my $tags = $cds->next->tags;
@@ -185,5 +191,5 @@ $artist = $rs->find(1);
 
 is( $queries, 1, 'only one select statement on find with has_many prefetch on resultset' );
 
-$schema->storage->debug(0);
-
+$schema->storage->debug($sdebug);
+$schema->storage->debugcb (undef);
index 76e18c2..99449d4 100644 (file)
@@ -2,6 +2,7 @@ use strict;
 use warnings;  
 
 use Test::More;
+use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
 use Storable qw(dclone freeze thaw);
@@ -20,10 +21,11 @@ my %stores = (
     },
 );
 
-plan tests => (7 * keys %stores);
+plan tests => (11 * keys %stores);
 
 for my $name (keys %stores) {
     my $store = $stores{$name};
+    my $copy;
 
     my $artist = $schema->resultset('Artist')->find(1);
     
@@ -39,22 +41,35 @@ for my $name (keys %stores) {
         DBICTest::CD->result_source_instance->schema(undef);
     }
 
-    my $copy = eval { $store->($artist) };
+    lives_ok { $copy = $store->($artist) } "serialize row object lives: $name";
     is_deeply($copy, $artist, "serialize row object works: $name");
 
-    # Test that an object with a related_resultset can be serialized.
-    my @cds = $artist->related_resultset("cds");
+    my $cd_rs = $artist->search_related("cds");
+
+    # test that a result source can be serialized as well
+
+    $cd_rs->_resolved_attrs;  # this builds up the {from} attr
 
+    lives_ok {
+      $copy = $store->($cd_rs);
+      is_deeply (
+        [ $copy->all ],
+        [ $cd_rs->all ],
+        "serialize resultset works: $name",
+      );
+    } "serialize resultset lives: $name";
+
+    # Test that an object with a related_resultset can be serialized.
     ok $artist->{related_resultsets}, 'has key: related_resultsets';
 
-    $copy = eval { $store->($artist) };
+    lives_ok { $copy = $store->($artist) } "serialize row object with related_resultset lives: $name";
     for my $key (keys %$artist) {
         next if $key eq 'related_resultsets';
         next if $key eq '_inflated_column';
         is_deeply($copy->{$key}, $artist->{$key},
                   qq[serialize with related_resultset "$key"]);
     }
-  
+
     ok eval { $copy->discard_changes; 1 } or diag $@;
     is($copy->id, $artist->id, "IDs still match ");
 }
index 438bd85..b9993a1 100644 (file)
@@ -16,33 +16,23 @@ if ($] <= 5.008000) {
     eval 'use utf8; 1' or plan skip_all => 'Need utf8 run this test';
 }
 
-plan tests => 5;
+plan tests => 6;
 
 DBICTest::Schema::CD->load_components('UTF8Columns');
 DBICTest::Schema::CD->utf8_columns('title');
 Class::C3->reinitialize();
 
-my $cd = $schema->resultset('CD')->create( { artist => 1, title => 'øni', year => 'foo' } );
+my $cd = $schema->resultset('CD')->create( { artist => 1, title => 'øni', year => '2048' } );
 my $utf8_char = 'uniuni';
 
-if ($] <= 5.008000) {
-
-    ok( Encode::is_utf8( $cd->title ), 'got title with utf8 flag' );
-    ok( !Encode::is_utf8( $cd->year ), 'got year without utf8 flag' );
-
-    Encode::_utf8_on($utf8_char);
-    $cd->title($utf8_char);
-    ok( !Encode::is_utf8( $cd->{_column_data}{title} ), 'store utf8-less chars' );
 
-} else {
+ok( _is_utf8( $cd->title ), 'got title with utf8 flag' );
+ok(! _is_utf8( $cd->year ), 'got year without utf8 flag' );
 
-    ok( utf8::is_utf8( $cd->title ), 'got title with utf8 flag' );
-    ok( !utf8::is_utf8( $cd->year ), 'got year without utf8 flag' );
+_force_utf8($utf8_char);
+$cd->title($utf8_char);
+ok(! _is_utf8( $cd->{_column_data}{title} ), 'store utf8-less chars' );
 
-    utf8::decode($utf8_char);
-    $cd->title($utf8_char);
-    ok( !utf8::is_utf8( $cd->{_column_data}{title} ), 'store utf8-less chars' );
-}
 
 my $v_utf8 = "\x{219}";
 
@@ -53,3 +43,28 @@ ok( !$cd->is_column_changed('title'), 'column is not dirty after setting the sam
 $cd->update ({ title => $v_utf8 });
 $cd->title('something_else');
 ok( $cd->is_column_changed('title'), 'column is dirty after setting to something completely different');
+
+TODO: {
+  local $TODO = 'There is currently no way to propagate aliases to inflate_result()';
+  $cd = $schema->resultset('CD')->find ({ title => $v_utf8 }, { select => 'title', as => 'name' });
+  ok (_is_utf8( $cd->get_column ('name') ), 'utf8 flag propagates via as');
+}
+
+
+sub _force_utf8 {
+  if ($] <= 5.008000) {
+    Encode::_utf8_on ($_[0]);
+  }
+  else {
+    utf8::decode ($_[0]);
+  }
+}
+
+sub _is_utf8 {
+  if ($] <= 5.008000) {
+    return Encode::is_utf8 (shift);
+  }
+  else {
+    return utf8::is_utf8 (shift);
+  }
+}
index 81cbf84..8ebf7b8 100644 (file)
@@ -8,14 +8,11 @@ use DBICTest;
 my $schema = DBICTest->init_schema();
 
 my $queries;
-#$schema->storage->debugfh(IO::File->new('t/var/temp.trace', 'w'));
 $schema->storage->debugcb( sub{ $queries++ } );
+my $sdebug = $schema->storage->debug;
 
-eval "use DBD::SQLite";
-plan skip_all => 'needs DBD::SQLite for testing' if $@;
 plan tests => 2;
 
-
 my $cd = $schema->resultset("CD")->find(1);
 $cd->title('test');
 
@@ -28,7 +25,7 @@ $cd->update;
 is($queries, 1, 'liner_notes (might_have) not prefetched - do not load 
 liner_notes on update');
 
-$schema->storage->debug(0);
+$schema->storage->debug($sdebug);
 
 
 my $cd2 = $schema->resultset("CD")->find(2, {prefetch => 'liner_notes'});
@@ -43,5 +40,4 @@ $cd2->update;
 is($queries, 1, 'liner_notes (might_have) prefetched - do not load 
 liner_notes on update');
 
-$schema->storage->debug(0);
-
+$schema->storage->debug($sdebug);
index 4b89019..467fed3 100644 (file)
@@ -45,7 +45,11 @@ my $translator = SQL::Translator->new(
     ok($output, "SQLT produced someoutput")
       or diag($translator->error);
 
-    like ($warn, qr/^SQLT attribute .+? was supplied for relationship/, 'Warn about dubious on_delete/on_update attributes');
+    like (
+      $warn,
+      qr/SQLT attribute .+? was supplied for relationship .+? which does not appear to be a foreign constraint/,
+      'Warn about dubious on_delete/on_update attributes',
+    );
 }
 
 # Note that the constraints listed here are the only ones that are tested -- if
@@ -155,7 +159,7 @@ my %fk_constraints = (
       'name' => 'artist_undirected_map_fk_id2', 'index_name' => 'artist_undirected_map_idx_id2',
       'selftable' => 'artist_undirected_map', 'foreigntable' => 'artist', 
       'selfcols'  => ['id2'], 'foreigncols' => ['artistid'],
-      on_delete => '', on_update => 'CASCADE', deferrable => 1,
+      on_delete => '', on_update => '', deferrable => 1,
     },
   ],
 
index 66169f3..aac98dc 100644 (file)
@@ -8,10 +8,9 @@ use DBICTest;
 
 my $schema = DBICTest->init_schema();
 
-plan tests => 18;
+plan tests => 20;
 
-my $cd;
-my $rs = $cd = $schema->resultset("CD")->search({}, { order_by => 'cdid' });
+my $rs = $schema->resultset("CD")->search({}, { order_by => 'cdid' });
 
 my $rs_title = $rs->get_column('title');
 my $rs_year = $rs->get_column('year');
@@ -76,3 +75,22 @@ is ($schema->resultset('BooksInLibrary')->get_column ('price')->sum, 125, 'Sum o
 my $owner = $schema->resultset('Owners')->find ({ name => 'Newton' });
 ok ($owner->books->count > 1, 'Owner Newton has multiple books');
 is ($owner->search_related ('books')->get_column ('price')->sum, 60, 'Correctly calculated price of all owned books');
+
+
+# make sure joined/prefetched get_column of a PK dtrt
+
+$rs->reset;
+my $j_rs = $rs->search ({}, { join => 'tracks' })->get_column ('cdid');
+is_deeply (
+  [ $j_rs->all ],
+  [ map { my $c = $rs->next; ( ($c->id) x $c->tracks->count ) } (1 .. $rs->count) ],
+  'join properly explodes amount of rows from get_column',
+);
+
+$rs->reset;
+my $p_rs = $rs->search ({}, { prefetch => 'tracks' })->get_column ('cdid');
+is_deeply (
+  [ $p_rs->all ],
+  [ $rs->get_column ('cdid')->all ],
+  'prefetch properly collapses amount of rows from get_column',
+);
index 0339bfc..6eeda5a 100644 (file)
@@ -46,9 +46,9 @@ cmp_ok(scalar @cds, '==', 1, "condition based on inherited join okay");
 
 my $rs3 = $rs2->search_related('cds');
 
-cmp_ok(scalar($rs3->all), '==', 45, "All cds for artist returned");
+cmp_ok(scalar($rs3->all), '==', 15, "All cds for artist returned");
 
-cmp_ok($rs3->count, '==', 45, "All cds for artist returned via count");
+cmp_ok($rs3->count, '==', 15, "All cds for artist returned via count");
 
 my $rs4 = $schema->resultset("CD")->search({ 'artist.artistid' => '1' }, { join => ['tracks', 'artist'], prefetch => 'artist' });
 my @rs4_results = $rs4->all;
diff --git a/t/92storage_on_connect_call.t b/t/92storage_on_connect_call.t
new file mode 100644 (file)
index 0000000..09befcd
--- /dev/null
@@ -0,0 +1,68 @@
+use strict;
+use warnings;
+no warnings qw/once redefine/;
+
+use lib qw(t/lib);
+use DBICTest;
+
+use Test::More tests => 9;
+
+my $schema = DBICTest->init_schema(
+  no_connect  => 1,
+  no_deploy   => 1,
+);
+
+local *DBIx::Class::Storage::DBI::connect_call_foo = sub {
+  isa_ok $_[0], 'DBIx::Class::Storage::DBI',
+    'got storage in connect_call method';
+  is $_[1], 'bar', 'got param in connect_call method';
+};
+
+local *DBIx::Class::Storage::DBI::disconnect_call_foo = sub {
+  isa_ok $_[0], 'DBIx::Class::Storage::DBI',
+    'got storage in disconnect_call method';
+};
+
+ok $schema->connection(
+  DBICTest->_database,
+  {
+    on_connect_call => [
+        [ do_sql => 'create table test1 (id integer)' ],
+        [ do_sql => [ 'insert into test1 values (?)', {}, 1 ] ],
+        [ do_sql => sub { ['insert into test1 values (2)'] } ],
+        [ sub { $_[0]->dbh->do($_[1]) }, 'insert into test1 values (3)' ],
+        # this invokes $storage->connect_call_foo('bar') (above)
+        [ foo => 'bar' ],
+    ],
+    on_connect_do => 'insert into test1 values (4)',
+    on_disconnect_call => 'foo',
+  },
+), 'connection()';
+
+is_deeply (
+  $schema->storage->dbh->selectall_arrayref('select * from test1'),
+  [ [ 1 ], [ 2 ], [ 3 ], [ 4 ] ],
+  'on_connect_call/do actions worked'
+);
+
+local *DBIx::Class::Storage::DBI::connect_call_foo = sub {
+  isa_ok $_[0], 'DBIx::Class::Storage::DBI',
+    'got storage in connect_call method';
+};
+
+local *DBIx::Class::Storage::DBI::connect_call_bar = sub {
+  isa_ok $_[0], 'DBIx::Class::Storage::DBI',
+    'got storage in connect_call method';
+};
+
+$schema->storage->disconnect;
+
+ok $schema->connection(
+  DBICTest->_database,
+  {
+    # method list form
+    on_connect_call => [ 'foo', sub { ok 1, "coderef in list form" }, 'bar' ],
+  },
+), 'connection()';
+
+$schema->storage->ensure_connected;
index b85da4a..65c236e 100644 (file)
@@ -6,13 +6,14 @@ use Test::Exception;
 use DBICTest;
 use List::Util 'first';
 use Scalar::Util 'reftype';
+use File::Spec;
 use IO::Handle;
 
 BEGIN {
     eval "use DBIx::Class::Storage::DBI::Replicated; use Test::Moose";
     plan $@
         ? ( skip_all => "Deps not installed: $@" )
-        : ( tests => 90 );
+        : ( tests => 126 );
 }
 
 use_ok 'DBIx::Class::Storage::DBI::Replicated::Pool';
@@ -20,6 +21,10 @@ use_ok 'DBIx::Class::Storage::DBI::Replicated::Balancer';
 use_ok 'DBIx::Class::Storage::DBI::Replicated::Replicant';
 use_ok 'DBIx::Class::Storage::DBI::Replicated';
 
+use Moose();
+use MooseX::Types();
+diag "Using Moose version $Moose::VERSION and MooseX::Types version $MooseX::Types::VERSION";
+
 =head1 HOW TO USE
 
     This is a test of the replicated storage system.  This will work in one of
@@ -142,9 +147,9 @@ TESTSCHEMACLASSES: {
     use File::Copy;    
     use base 'DBIx::Class::DBI::Replicated::TestReplication';
     
-    __PACKAGE__->mk_accessors( qw/master_path slave_paths/ );
+    __PACKAGE__->mk_accessors(qw/master_path slave_paths/);
     
-    ## Set the mastep path from DBICTest
+    ## Set the master path from DBICTest
     
        sub new {
            my $class = shift @_;
@@ -152,9 +157,9 @@ TESTSCHEMACLASSES: {
        
            $self->master_path( DBICTest->_sqlite_dbfilename );
            $self->slave_paths([
-            "t/var/DBIxClass_slave1.db",
-            "t/var/DBIxClass_slave2.db",    
-        ]);
+                       File::Spec->catfile(qw/t var DBIxClass_slave1.db/),
+                       File::Spec->catfile(qw/t var DBIxClass_slave2.db/),
+               ]);
         
            return $self;
        }    
@@ -170,7 +175,10 @@ TESTSCHEMACLASSES: {
         
         my @connect_infos = map { [$_,'','',{AutoCommit=>1}] } @dsn;
 
-    # try a hashref too
+               ## Make sure nothing is left over from a failed test
+               $self->cleanup;
+
+               ## try a hashref too
         my $c = $connect_infos[0];
         $connect_infos[0] = {
           dsn => $c->[0],
@@ -198,7 +206,9 @@ TESTSCHEMACLASSES: {
     sub cleanup {
         my $self = shift @_;
         foreach my $slave (@{$self->slave_paths}) {
-            unlink $slave;
+                       if(-e $slave) {
+                               unlink $slave;
+                       }
         }     
     }
     
@@ -275,6 +285,19 @@ ok my @replicant_connects = $replicated->generate_replicant_connect_info
 ok my @replicated_storages = $replicated->schema->storage->connect_replicants(@replicant_connects)
     => 'Created some storages suitable for replicants';
 
+our %debug;
+$replicated->schema->storage->debug(1);
+$replicated->schema->storage->debugcb(sub {
+       my ($op, $info) = @_;
+       ##warn "\n$op, $info\n";
+       %debug = (
+               op => $op,
+               info => $info,
+               dsn => ($info=~m/\[(.+)\]/)[0],
+               storage_type => $info=~m/REPLICANT/ ? 'REPLICANT' : 'MASTER',
+       );
+});
+
 ok my @all_storages = $replicated->schema->storage->all_storages
     => '->all_storages';
 
@@ -296,6 +319,8 @@ is ((grep $_->{master_option}, @all_storage_opts),
  
 my @replicant_names = keys %{ $replicated->schema->storage->replicants };
 
+ok @replicant_names, "found replicant names @replicant_names";
+
 ## Silence warning about not supporting the is_replicating method if using the
 ## sqlite dbs.
 $replicated->schema->storage->debugobj->silence(1)
@@ -332,6 +357,11 @@ $replicated
         [ qw/artistid name/ ],
         [ 4, "Ozric Tentacles"],
     ]);
+
+       is $debug{storage_type}, 'MASTER', 
+               "got last query from a master: $debug{dsn}";
+       
+       like $debug{info}, qr/INSERT/, 'Last was an insert';
                 
 ## Make sure all the slaves have the table definitions
 
@@ -353,6 +383,11 @@ $replicated->schema->storage->debugobj->silence(0);
 ok my $artist1 = $replicated->schema->resultset('Artist')->find(4)
     => 'Created Result';
 
+## We removed testing here since master read weight is on, so we can't tell in
+## advance what storage to expect.  We turn master read weight off a bit lower
+## is $debug{storage_type}, 'REPLICANT' 
+##     => "got last query from a replicant: $debug{dsn}, $debug{info}";
+
 isa_ok $artist1
     => 'DBICTest::Artist';
     
@@ -361,10 +396,7 @@ is $artist1->name, 'Ozric Tentacles'
 
 ## Check that master_read_weight is honored
 {
-    no warnings 'once';
-
-    # turn off redefined warning
-    local $SIG{__WARN__} = sub {};
+    no warnings qw/once redefine/;
 
     local
     *DBIx::Class::Storage::DBI::Replicated::Balancer::Random::_random_number =
@@ -394,6 +426,11 @@ $replicated
         [ 7, "Watergate"],
     ]);
 
+       is $debug{storage_type}, 'MASTER', 
+               "got last query from a master: $debug{dsn}";
+       
+       like $debug{info}, qr/INSERT/, 'Last was an insert';
+
 ## Make sure all the slaves have the table definitions
 $replicated->replicate;
 
@@ -401,7 +438,10 @@ $replicated->replicate;
 
 ok my $artist2 = $replicated->schema->resultset('Artist')->find(5)
     => 'Sync succeed';
-    
+
+is $debug{storage_type}, 'REPLICANT' 
+       => "got last query from a replicant: $debug{dsn}";
+           
 isa_ok $artist2
     => 'DBICTest::Artist';
     
@@ -423,7 +463,10 @@ is $replicated->schema->storage->pool->connected_replicants => 0
 
 ok my $artist3 = $replicated->schema->resultset('Artist')->find(6)
     => 'Still finding stuff.';
-    
+
+is $debug{storage_type}, 'REPLICANT' 
+       => "got last query from a replicant: $debug{dsn}";
+           
 isa_ok $artist3
     => 'DBICTest::Artist';
     
@@ -437,7 +480,10 @@ is $replicated->schema->storage->pool->connected_replicants => 1
 
 ok ! $replicated->schema->resultset('Artist')->find(666)
     => 'Correctly failed to find something.';
-    
+
+is $debug{storage_type}, 'REPLICANT' 
+       => "got last query from a replicant: $debug{dsn}";
+                   
 ## test the reliable option
 
 TESTRELIABLE: {
@@ -446,24 +492,39 @@ TESTRELIABLE: {
        
        ok $replicated->schema->resultset('Artist')->find(2)
            => 'Read from master 1';
-       
+
+       is $debug{storage_type}, 'MASTER', 
+               "got last query from a master: $debug{dsn}";
+                       
        ok $replicated->schema->resultset('Artist')->find(5)
            => 'Read from master 2';
-           
+
+       is $debug{storage_type}, 'MASTER', 
+               "got last query from a master: $debug{dsn}";
+                           
     $replicated->schema->storage->set_balanced_storage;            
            
        ok $replicated->schema->resultset('Artist')->find(3)
         => 'Read from replicant';
+
+       is $debug{storage_type}, 'REPLICANT', 
+               "got last query from a replicant: $debug{dsn}";
 }
 
 ## Make sure when reliable goes out of scope, we are using replicants again
 
 ok $replicated->schema->resultset('Artist')->find(1)
     => 'back to replicant 1.';
-    
+
+       is $debug{storage_type}, 'REPLICANT', 
+               "got last query from a replicant: $debug{dsn}";
+                   
 ok $replicated->schema->resultset('Artist')->find(2)
     => 'back to replicant 2.';
 
+       is $debug{storage_type}, 'REPLICANT', 
+               "got last query from a replicant: $debug{dsn}";
+
 ## set all the replicants to inactive, and make sure the balancer falls back to
 ## the master.
 
@@ -477,10 +538,13 @@ $replicated->schema->storage->replicants->{$replicant_names[1]}->active(0);
     $replicated->schema->storage->debugfh($debugfh);
 
     ok $replicated->schema->resultset('Artist')->find(2)
-       => 'Fallback to master';
+               => 'Fallback to master';
+
+       is $debug{storage_type}, 'MASTER', 
+               "got last query from a master: $debug{dsn}";
 
     like $fallback_warning, qr/falling back to master/
-       => 'emits falling back to master warning';
+               => 'emits falling back to master warning';
 
     $replicated->schema->storage->debugfh($oldfh);
 }
@@ -499,6 +563,9 @@ $replicated->schema->storage->debugobj->silence(0);
 
 ok $replicated->schema->resultset('Artist')->find(2)
     => 'Returned to replicates';
+
+is $debug{storage_type}, 'REPLICANT', 
+       "got last query from a replicant: $debug{dsn}";
     
 ## Getting slave status tests
 
@@ -506,7 +573,7 @@ SKIP: {
     ## We skip this tests unless you have a custom replicants, since the default
     ## sqlite based replication tests don't support these functions.
     
-    skip 'Cannot Test Replicant Status on Non Replicating Database', 9
+    skip 'Cannot Test Replicant Status on Non Replicating Database', 10 
      unless DBICTest->has_custom_dsn && $ENV{"DBICTEST_SLAVE0_DSN"};
 
     $replicated->replicate; ## Give the slaves a chance to catchup.
@@ -562,6 +629,9 @@ SKIP: {
                
        ok $replicated->schema->resultset('Artist')->find(5)
            => 'replicant reactivated';
+
+       is $debug{storage_type}, 'REPLICANT',
+               "got last query from a replicant: $debug{dsn}";
            
        is $replicated->schema->storage->pool->active_replicants => 2
            => "both replicants reactivated";        
@@ -572,7 +642,10 @@ SKIP: {
 ok my $reliably = sub {
        
     ok $replicated->schema->resultset('Artist')->find(5)
-        => 'replicant reactivated';    
+        => 'replicant reactivated';
+
+       is $debug{storage_type}, 'MASTER',
+               "got last query from a master: $debug{dsn}";
        
 } => 'created coderef properly';
 
@@ -595,6 +668,8 @@ throws_ok {$replicated->schema->storage->execute_reliably($unreliably)}
 
 ok $replicated->schema->resultset('Artist')->find(3)
     => 'replicant reactivated';
+
+is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
     
 ## make sure transactions are set to execute_reliably
 
@@ -610,11 +685,17 @@ ok my $transaction = sub {
            ]);
            
     ok my $result = $replicated->schema->resultset('Artist')->find($id)
-        => 'Found expected artist';
-        
+        => "Found expected artist for $id";
+
+       is $debug{storage_type}, 'MASTER',
+               "got last query from a master: $debug{dsn}";
+                               
     ok my $more = $replicated->schema->resultset('Artist')->find(1)
-        => 'Found expected artist again';
-        
+        => 'Found expected artist again for 1';
+
+       is $debug{storage_type}, 'MASTER',
+               "got last query from a master: $debug{dsn}";
+                               
    return ($result, $more);
    
 } => 'Created a coderef properly';
@@ -626,18 +707,28 @@ ok my $transaction = sub {
            
            is $return[0]->id, 666
                => 'first returned value is correct';
+
+               is $debug{storage_type}, 'MASTER',
+                   "got last query from a master: $debug{dsn}";
                
            is $return[1]->id, 1
                => 'second returned value is correct';
+
+               is $debug{storage_type}, 'MASTER',
+                    "got last query from a master: $debug{dsn}";
+
 }
 
 ## Test that asking for single return works
 {
-       ok my $return = $replicated->schema->txn_do($transaction, 777)
+       ok my @return = $replicated->schema->txn_do($transaction, 777)
            => 'did transaction';
            
-           is $return->id, 777
+           is $return[0]->id, 777
                => 'first returned value is correct';
+               
+           is $return[1]->id, 1
+               => 'second returned value is correct';
 }
 
 ## Test transaction returning a single value
@@ -646,6 +737,7 @@ ok my $transaction = sub {
        ok my $result = $replicated->schema->txn_do(sub {
                ok my $more = $replicated->schema->resultset('Artist')->find(1)
                => 'found inside a transaction';
+               is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
                return $more;
        }) => 'successfully processed transaction';
        
@@ -657,15 +749,22 @@ ok my $transaction = sub {
 
 ok $replicated->schema->resultset('Artist')->find(1)
     => 'replicant reactivated';
+
+is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
     
 ## Test Discard changes
 
 {
        ok my $artist = $replicated->schema->resultset('Artist')->find(2)
            => 'got an artist to test discard changes';
-           
-       ok $artist->discard_changes
+
+       is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
+
+       ok $artist->get_from_storage({force_pool=>'master'})
           => 'properly discard changes';
+
+       is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
+
 }
 
 ## Test some edge cases, like trying to do a transaction inside a transaction, etc
@@ -675,6 +774,7 @@ ok $replicated->schema->resultset('Artist')->find(1)
        return $replicated->schema->txn_do(sub {
                ok my $more = $replicated->schema->resultset('Artist')->find(1)
                => 'found inside a transaction inside a transaction';
+                       is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
                return $more;                   
        });
     }) => 'successfully processed transaction';
@@ -689,7 +789,8 @@ ok $replicated->schema->resultset('Artist')->find(1)
                return $replicated->schema->txn_do(sub {
                        return $replicated->schema->storage->execute_reliably(sub {
                                ok my $more = $replicated->schema->resultset('Artist')->find(1)
-                               => 'found inside crazy deep transactions and execute_reliably';
+                                 => 'found inside crazy deep transactions and execute_reliably';
+                                       is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
                                return $more;                           
                        });
                });     
@@ -712,8 +813,25 @@ ok $replicated->schema->resultset('Artist')->find(1)
           
     ok my $artist = $reliable_artist_rs->find(2) 
         => 'got an artist result via force_pool storage';
+
+       is $debug{storage_type}, 'MASTER', "got last query from a master: $debug{dsn}";
 }
 
+## Test the force_pool resultset attribute part two.
+
+{
+       ok my $artist_rs = $replicated->schema->resultset('Artist')
+        => 'got artist resultset';
+          
+       ## Turn on Forced Pool Storage
+       ok my $reliable_artist_rs = $artist_rs->search(undef, {force_pool=>$replicant_names[0]})
+        => 'Created a resultset using force_pool storage';
+          
+    ok my $artist = $reliable_artist_rs->find(2) 
+        => 'got an artist result via force_pool storage';
+
+       is $debug{storage_type}, 'REPLICANT', "got last query from a replicant: $debug{dsn}";
+}
 ## Delete the old database files
 $replicated->cleanup;
 
index d99d201..c4a65a2 100644 (file)
@@ -51,6 +51,7 @@ my $sql_maker = $schema->storage->sql_maker;
 
 # Make sure the carp/croak override in SQLA works (via SQLAHacks)
 my $file = __FILE__;
+$file = "\Q$file\E";
 throws_ok (sub {
   $schema->resultset ('Artist')->search ({}, { order_by => { -asc => 'stuff', -desc => 'staff' } } )->as_query;
 }, qr/$file/, 'Exception correctly croak()ed');
index a7687f8..6b27c7b 100644 (file)
@@ -35,12 +35,23 @@ my ($sql, @bind) = $sql_maker->select(
               {
                 'artist.artistid' => 'me.artist'
               }
-            ]
+            ],
+            [
+              {
+                'tracks' => 'tracks',
+                '-join_type' => 'left'
+              },
+              {
+                'tracks.cd' => 'me.cdid'
+              }
+            ],
           ],
           [
-            {
-              'count' => '*'
-            }
+            'me.cdid',
+            { count => 'tracks.cd' },
+            { -select => 'me.artist' },
+            { -select => 'me.title', -as => 'name' },
+            { -select => { min => 'me.year' }, -as => 'me.minyear' },
           ],
           {
             'artist.name' => 'Caterwauler McCrae',
@@ -53,8 +64,15 @@ my ($sql, @bind) = $sql_maker->select(
 
 is_same_sql_bind(
   $sql, \@bind,
-  q/SELECT COUNT( * ) FROM `cd` `me`  JOIN `artist` `artist` ON ( `artist`.`artistid` = `me`.`artist` ) WHERE ( `artist`.`name` = ? AND `me`.`year` = ? )/, [ ['artist.name' => 'Caterwauler McCrae'], ['me.year' => 2001] ],
-  'got correct SQL and bind parameters for count query with quoting'
+  q/
+    SELECT `me`.`cdid`, COUNT( `tracks`.`cd` ), `me`.`artist`, `me`.`title` AS `name`, MIN( `me`.`year` ) AS `me`.`minyear`
+      FROM `cd` `me`
+      JOIN `artist` `artist` ON ( `artist`.`artistid` = `me`.`artist` )
+      LEFT JOIN `tracks` `tracks` ON ( `tracks`.`cd` = `me`.`cdid` )
+    WHERE ( `artist`.`name` = ? AND `me`.`year` = ? )
+  /,
+  [ ['artist.name' => 'Caterwauler McCrae'], ['me.year' => 2001] ],
+  'got correct SQL and bind parameters for complex select query with quoting'
 );
 
 
index 9968a82..bbae42d 100644 (file)
@@ -15,9 +15,13 @@ BEGIN {
 
 my $schema = DBICTest->init_schema();
 # Dummy was yanked out by the sqlt hook test
+# CustomSql tests the horrific/deprecated ->name(\$sql) hack
 # YearXXXXCDs are views
-my @sources = grep { $_ ne 'Dummy' && $_ !~ /^Year\d{4}CDs$/ } 
-                $schema->sources;
+#
+my @sources = grep
+  { $_ !~ /^ (?: Dummy | CustomSql | Year\d{4}CDs ) $/x }
+  $schema->sources
+;
 
 plan tests => ( @sources * 3);
 
index b9946a4..1a1d0c7 100644 (file)
@@ -13,7 +13,7 @@ BEGIN {
     eval "use DBD::SQLite";
     plan $@
         ? ( skip_all => 'needs DBD::SQLite for testing' )
-        : ( tests => 9 );
+        : ( tests => 13 );
 }
 
 my $where_bind = {
@@ -45,34 +45,34 @@ TODO: {
     is ( $rs->count, 1, 'where/bind last' );
 }
 
-# More complex cases, based primarily on the Cookbook
-# "Arbitrary SQL through a custom ResultSource" technique,
-# which seems to be the only place the bind attribute is
-# documented.  Breaking this technique probably breaks existing
-# application code.
-my $source = DBICTest::Artist->result_source_instance;
-my $new_source = $source->new($source);
-$new_source->source_name('Complex');
+{
+  # More complex cases, based primarily on the Cookbook
+  # "Arbitrary SQL through a custom ResultSource" technique,
+  # which seems to be the only place the bind attribute is
+  # documented.  Breaking this technique probably breaks existing
+  # application code.
+  my $source = DBICTest::Artist->result_source_instance;
+  my $new_source = $source->new($source);
+  $new_source->source_name('Complex');
 
-$new_source->name(\<<'');
-( SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year 
-  FROM artist a
-  JOIN cd ON cd.artist = a.artistid
-  WHERE cd.year = ?)
+  $new_source->name(\<<'');
+  ( SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year 
+    FROM artist a
+    JOIN cd ON cd.artist = a.artistid
+    WHERE cd.year = ?)
 
-$schema->register_extra_source('Complex' => $new_source);
+  $schema->register_extra_source('Complex' => $new_source);
 
-$rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] });
-is ( $rs->count, 1, 'cookbook arbitrary sql example' );
+  $rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] });
+  is ( $rs->count, 1, 'cookbook arbitrary sql example' );
 
-$rs = $schema->resultset('Complex')->search({ 'artistid' => 1 }, { bind => [ 1999 ] });
-is ( $rs->count, 1, '...coobook + search condition' );
+  $rs = $schema->resultset('Complex')->search({ 'artistid' => 1 }, { bind => [ 1999 ] });
+  is ( $rs->count, 1, '...cookbook + search condition' );
 
-$rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] })
-    ->search({ 'artistid' => 1 });
-is ( $rs->count, 1, '...cookbook (bind first) + chained search' );
+  $rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] })
+      ->search({ 'artistid' => 1 });
+  is ( $rs->count, 1, '...cookbook (bind first) + chained search' );
 
-{
   $rs = $schema->resultset('Complex')->search({}, { bind => [ 1999 ] })->search({}, { where => \"title LIKE ?", bind => [ 'Spoon%' ] });
   is_same_sql_bind(
     $rs->as_query,
@@ -82,8 +82,36 @@ is ( $rs->count, 1, '...cookbook (bind first) + chained search' );
       [ '!!dummy' => 'Spoon%' ]
     ],
     'got correct SQL'
-);
+  );
+}
+
+{
+  # More complex cases, based primarily on the Cookbook
+  # "Arbitrary SQL through a custom ResultSource" technique,
+  # which seems to be the only place the bind attribute is
+  # documented.  Breaking this technique probably breaks existing
+  # application code.
+
+  $rs = $schema->resultset('CustomSql')->search({}, { bind => [ 1999 ] });
+  is ( $rs->count, 1, 'cookbook arbitrary sql example (in separate file)' );
+
+  $rs = $schema->resultset('CustomSql')->search({ 'artistid' => 1 }, { bind => [ 1999 ] });
+  is ( $rs->count, 1, '...cookbook (in separate file) + search condition' );
+
+  $rs = $schema->resultset('CustomSql')->search({}, { bind => [ 1999 ] })
+      ->search({ 'artistid' => 1 });
+  is ( $rs->count, 1, '...cookbook (bind first, in separate file) + chained search' );
 
+  $rs = $schema->resultset('CustomSql')->search({}, { bind => [ 1999 ] })->search({}, { where => \"title LIKE ?", bind => [ 'Spoon%' ] });
+  is_same_sql_bind(
+    $rs->as_query,
+    "(SELECT me.artistid, me.name, me.rank, me.charfield FROM (SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year FROM artist a JOIN cd ON cd.artist = a.artistid WHERE cd.year = ?) WHERE title LIKE ?)",
+    [
+      [ '!!dummy' => '1999' ], 
+      [ '!!dummy' => 'Spoon%' ]
+    ],
+    'got correct SQL (cookbook arbitrary SQL, in separate file)'
+  );
 }
 
 TODO: {
index 0e1b22e..5b44328 100644 (file)
@@ -8,8 +8,7 @@ BEGIN {
     plan (skip_all => 'Class::Trigger and DBIx::ContextualFetch required');
     next;
   }
-  eval "use DBD::SQLite";
-  plan $@ ? (skip_all => 'needs DBD::SQLite for testing') : (tests => 98);
+  plan tests => 98;
 }
 
 INIT {
@@ -187,17 +186,14 @@ eval {
        ok(!Film->retrieve('Ishtar'), 'Ishtar no longer there');
        {
                my $deprecated = 0;
-               #local $SIG{__WARN__} = sub { $deprecated++ if $_[0] =~ /deprecated/ };
+               local $SIG{__WARN__} = sub { $deprecated++ if $_[0] =~ /deprecated/ };
                ok(
                        Film->delete(Director => 'Elaine May'),
                        "In fact, delete all films by Elaine May"
                );
                cmp_ok(Film->search(Director => 'Elaine May'), '==',
                        0, "0 Films by Elaine May");
-                SKIP: {
-                    skip "No deprecated warnings from compat layer", 1;
-                   is $deprecated, 1, "Got a deprecated warning";
-                }
+               is $deprecated, 0, "No deprecated warnings from compat layer";
        }
 };
 is $@, '', "No problems with deletes";
old mode 100755 (executable)
new mode 100644 (file)
diff --git a/t/count/count_rs.t b/t/count/count_rs.t
new file mode 100644 (file)
index 0000000..acf696c
--- /dev/null
@@ -0,0 +1,119 @@
+use strict;
+use warnings;
+
+use lib qw(t/lib);
+
+use Test::More;
+use DBICTest;
+use DBIC::SqlMakerTest;
+use DBIC::DebugObj;
+
+plan tests => 10;
+
+my $schema = DBICTest->init_schema();
+
+# non-collapsing prefetch (no multi prefetches)
+{
+  my $rs = $schema->resultset("CD")
+            ->search_related('tracks',
+                { 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);
+
+  is ($rs->count, 2, 'Correct count via count()');
+
+  is_same_sql_bind (
+    $sql,
+    \@bind,
+    'SELECT COUNT( * )
+      FROM cd me
+      JOIN track tracks ON tracks.cd = me.cdid
+      JOIN cd disc ON disc.cdid = tracks.cd
+      LEFT JOIN lyrics lyrics ON lyrics.track_id = tracks.trackid 
+     WHERE ( ( position = ? OR position = ? ) )
+    ',
+    [ qw/'1' '2'/ ],
+    'count softlimit applied',
+  );
+
+  my $crs = $rs->count_rs;
+  is ($crs->next, 2, 'Correct count via count_rs()');
+
+  is_same_sql_bind (
+    $crs->as_query,
+    '(SELECT COUNT( * )
+       FROM (
+        SELECT tracks.trackid
+          FROM cd me
+          JOIN track tracks ON tracks.cd = me.cdid
+          JOIN cd disc ON disc.cdid = tracks.cd
+          LEFT JOIN lyrics lyrics ON lyrics.track_id = tracks.trackid 
+        WHERE ( ( position = ? OR position = ? ) )
+        LIMIT 3 OFFSET 8
+       ) count_subq
+    )',
+    [ [ position => 1 ], [ position => 2 ] ],
+    'count_rs db-side limit applied',
+  );
+}
+
+# has_many prefetch with limit
+{
+  my $rs = $schema->resultset("Artist")
+            ->search_related('cds',
+                { '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);
+
+  is ($rs->count, 1, 'Correct count via count()');
+
+  is_same_sql_bind (
+    $sql,
+    \@bind,
+    'SELECT COUNT( * )
+      FROM (
+        SELECT cds.cdid
+          FROM artist me
+          JOIN cd cds ON cds.artist = me.artistid
+          LEFT JOIN track tracks ON tracks.cd = cds.cdid
+          JOIN artist artist ON artist.artistid = cds.artist
+        WHERE tracks.position = ? OR tracks.position = ?
+        GROUP BY cds.cdid
+      ) count_subq
+    ',
+    [ qw/'1' '2'/ ],
+    'count softlimit applied',
+  );
+
+  my $crs = $rs->count_rs;
+  is ($crs->next, 1, 'Correct count via count_rs()');
+
+  is_same_sql_bind (
+    $crs->as_query,
+    '(SELECT COUNT( * )
+      FROM (
+        SELECT cds.cdid
+          FROM artist me
+          JOIN cd cds ON cds.artist = me.artistid
+          LEFT JOIN track tracks ON tracks.cd = cds.cdid
+          JOIN artist artist ON artist.artistid = cds.artist
+        WHERE tracks.position = ? OR tracks.position = ?
+        GROUP BY cds.cdid
+        LIMIT 3 OFFSET 4
+      ) count_subq
+    )',
+    [ [ 'tracks.position' => 1 ], [ 'tracks.position' => 2 ] ],
+    'count_rs db-side limit applied',
+  );
+}
index 8e956b5..6df9ed0 100644 (file)
@@ -11,9 +11,7 @@ use DBIC::SqlMakerTest;
 
 my $schema = DBICTest->init_schema();
 
-eval "use DBD::SQLite";
-plan skip_all => 'needs DBD::SQLite for testing' if $@;
-plan tests => 22;
+plan tests => 56;
 
 # The tag Blue is assigned to cds 1 2 3 and 5
 # The tag Cheesy is assigned to cds 2 4 and 5
@@ -23,77 +21,70 @@ plan tests => 22;
 my $rs;
 my $in_rs = $schema->resultset('Tag')->search({ tag => [ 'Blue', 'Cheesy' ] });
 
-$rs = $schema->resultset('Tag')->search({ tag => 'Blue' });
-is($rs->count, 4, 'Count without DISTINCT');
+for my $get_count (
+  sub { shift->count },
+  sub { my $crs = shift->count_rs; isa_ok ($crs, 'DBIx::Class::ResultSetColumn'); $crs->next }
+) {
+  $rs = $schema->resultset('Tag')->search({ tag => 'Blue' });
+  is($get_count->($rs), 4, 'Count without DISTINCT');
 
-$rs = $schema->resultset('Tag')->search({ tag => [ 'Blue', 'Cheesy' ] }, { group_by => 'tag' });
-is($rs->count, 2, 'Count with single column group_by');
+  $rs = $schema->resultset('Tag')->search({ tag => [ 'Blue', 'Cheesy' ] }, { group_by => 'tag' });
+  is($get_count->($rs), 2, 'Count with single column group_by');
 
-$rs = $schema->resultset('Tag')->search({ tag => [ 'Blue', 'Cheesy' ] }, { group_by => 'cd' });
-is($rs->count, 5, 'Count with another single column group_by');
+  $rs = $schema->resultset('Tag')->search({ tag => [ 'Blue', 'Cheesy' ] }, { group_by => 'cd' });
+  is($get_count->($rs), 5, 'Count with another single column group_by');
 
-$rs = $schema->resultset('Tag')->search({ tag => 'Blue' }, { group_by => [ qw/tag cd/ ]});
-is($rs->count, 4, 'Count with multiple column group_by');
+  $rs = $schema->resultset('Tag')->search({ tag => 'Blue' }, { group_by => [ qw/tag cd/ ]});
+  is($get_count->($rs), 4, 'Count with multiple column group_by');
 
-$rs = $schema->resultset('Tag')->search({ tag => 'Blue' }, { distinct => 1 });
-is($rs->count, 4, 'Count with single column distinct');
+  $rs = $schema->resultset('Tag')->search({ tag => 'Blue' }, { distinct => 1 });
+  is($get_count->($rs), 4, 'Count with single column distinct');
 
-$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } });
-is($rs->count, 7, 'Count with IN subquery');
+  $rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } });
+  is($get_count->($rs), 7, 'Count with IN subquery');
 
-$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { group_by => 'tag' });
-is($rs->count, 2, 'Count with IN subquery with outside group_by');
+  $rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { group_by => 'tag' });
+  is($get_count->($rs), 2, 'Count with IN subquery with outside group_by');
 
-$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { distinct => 1 });
-is($rs->count, 7, 'Count with IN subquery with outside distinct');
+  $rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { distinct => 1 });
+  is($get_count->($rs), 7, 'Count with IN subquery with outside distinct');
 
-$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { distinct => 1, select => 'tag' }), 
-is($rs->count, 2, 'Count with IN subquery with outside distinct on a single column');
+  $rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->get_column('tag')->as_query } }, { distinct => 1, select => 'tag' }), 
+  is($get_count->($rs), 2, 'Count with IN subquery with outside distinct on a single column');
 
-$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->search({}, { group_by => 'tag' })->get_column('tag')->as_query } });
-is($rs->count, 7, 'Count with IN subquery with single group_by');
+  $rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->search({}, { group_by => 'tag' })->get_column('tag')->as_query } });
+  is($get_count->($rs), 7, 'Count with IN subquery with single group_by');
 
-$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->search({}, { group_by => 'cd' })->get_column('tag')->as_query } });
-is($rs->count, 7, 'Count with IN subquery with another single group_by');
+  $rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->search({}, { group_by => 'cd' })->get_column('tag')->as_query } });
+  is($get_count->($rs), 7, 'Count with IN subquery with another single group_by');
 
-$rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->search({}, { group_by => [ qw/tag cd/ ] })->get_column('tag')->as_query } });
-is($rs->count, 7, 'Count with IN subquery with multiple group_by');
+  $rs = $schema->resultset('Tag')->search({ tag => { -in => $in_rs->search({}, { group_by => [ qw/tag cd/ ] })->get_column('tag')->as_query } });
+  is($get_count->($rs), 7, 'Count with IN subquery with multiple group_by');
 
-$rs = $schema->resultset('Tag')->search({ tag => \"= 'Blue'" });
-is($rs->count, 4, 'Count without DISTINCT, using literal SQL');
+  $rs = $schema->resultset('Tag')->search({ tag => \"= 'Blue'" });
+  is($get_count->($rs), 4, 'Count without DISTINCT, using literal SQL');
 
-$rs = $schema->resultset('Tag')->search({ tag => \" IN ('Blue', 'Cheesy')" }, { group_by => 'tag' });
-is($rs->count, 2, 'Count with literal SQL and single group_by');
+  $rs = $schema->resultset('Tag')->search({ tag => \" IN ('Blue', 'Cheesy')" }, { group_by => 'tag' });
+  is($get_count->($rs), 2, 'Count with literal SQL and single group_by');
 
-$rs = $schema->resultset('Tag')->search({ tag => \" IN ('Blue', 'Cheesy')" }, { group_by => 'cd' });
-is($rs->count, 5, 'Count with literal SQL and another single group_by');
+  $rs = $schema->resultset('Tag')->search({ tag => \" IN ('Blue', 'Cheesy')" }, { group_by => 'cd' });
+  is($get_count->($rs), 5, 'Count with literal SQL and another single group_by');
 
-$rs = $schema->resultset('Tag')->search({ tag => \" IN ('Blue', 'Cheesy')" }, { group_by => [ qw/tag cd/ ] });
-is($rs->count, 7, 'Count with literal SQL and multiple group_by');
+  $rs = $schema->resultset('Tag')->search({ tag => \" IN ('Blue', 'Cheesy')" }, { group_by => [ qw/tag cd/ ] });
+  is($get_count->($rs), 7, 'Count with literal SQL and multiple group_by');
 
-$rs = $schema->resultset('Tag')->search({ tag => 'Blue' }, { '+select' => { max => 'tagid' }, distinct => 1 });
-is($rs->count, 4, 'Count with +select aggreggate');
+  $rs = $schema->resultset('Tag')->search({ tag => 'Blue' }, { '+select' => { max => 'tagid' }, distinct => 1 });
+  is($get_count->($rs), 4, 'Count with +select aggreggate');
 
-$rs = $schema->resultset('Tag')->search({}, { select => 'length(me.tag)', distinct => 1 });
-is($rs->count, 3, 'Count by distinct function result as select literal');
-
-eval {
-  my @warnings;
-  local $SIG{__WARN__} = sub { $_[0] =~ /The select => { distinct => ... } syntax will be deprecated/ 
-    ? push @warnings, @_
-    : warn @_
-  };
-  my $row = $schema->resultset('Tag')->search({}, { select => { distinct => 'tag' } })->first;
-  is (@warnings, 1, 'Warned about deprecated distinct') if $DBIx::Class::VERSION < 0.09;
-};
-ok ($@, 'Exception on deprecated distinct usage thrown') if $DBIx::Class::VERSION >= 0.09;
+  $rs = $schema->resultset('Tag')->search({}, { select => 'length(me.tag)', distinct => 1 });
+  is($get_count->($rs), 3, 'Count by distinct function result as select literal');
+}
 
 throws_ok(
   sub { my $row = $schema->resultset('Tag')->search({}, { select => { distinct => [qw/tag cd/] } })->first },
-  qr/select => { distinct => ... } syntax is not supported for multiple columns/,
+  qr/select => { distinct => \.\.\. } syntax is not supported for multiple columns/,
   'throw on unsupported syntax'
 );
 
 # These two rely on the database to throw an exception. This might not be the case one day. Please revise.
 dies_ok(sub { my $count = $schema->resultset('Tag')->search({}, { '+select' => \'tagid AS tag_id', distinct => 1 })->count }, 'expecting to die');
-dies_ok(sub { my $count = $schema->resultset('Tag')->search({}, { select => { length => 'tag' }, distinct => 1 })->count }, 'expecting to die');
index 5ba7deb..352eef9 100644 (file)
@@ -7,7 +7,7 @@ use lib qw(t/lib);
 
 use DBICTest;
 
-plan tests => 3;
+plan tests => 7;
 
 my $schema = DBICTest->init_schema();
 
@@ -26,6 +26,12 @@ is (
   "Count correct with requested distinct collapse of main table"
 );
 
+# 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.
+my $artist=$schema->resultset('Artist')->create({name => 'xxx'});
+is($artist->related_resultset('cds')->count(), 0, "No CDs found for a shiny new artist");
+is(scalar($artist->related_resultset('cds')->all()), 0, "No CDs fetched for a shiny new artist");
 
-
-
+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");
index 54f6c05..2032a4b 100644 (file)
@@ -32,7 +32,7 @@ my $schema = DBICTest->init_schema();
   is_same_sql_bind (
     $sql,
     \@bind,
-    'SELECT COUNT( * ) FROM (SELECT cds.cdid FROM artist me LEFT JOIN cd cds ON cds.artist = me.artistid LEFT JOIN track tracks ON tracks.cd = cds.cdid JOIN artist artist ON artist.artistid = cds.artist WHERE tracks.position = ? OR tracks.position = ? GROUP BY cds.cdid) count_subq',
+    'SELECT COUNT( * ) FROM (SELECT cds.cdid FROM artist me JOIN cd cds ON cds.artist = me.artistid LEFT JOIN track tracks ON tracks.cd = cds.cdid JOIN artist artist ON artist.artistid = cds.artist WHERE tracks.position = ? OR tracks.position = ? GROUP BY cds.cdid) count_subq',
     [ qw/'1' '2'/ ],
   );
 }
@@ -57,7 +57,7 @@ my $schema = DBICTest->init_schema();
   is_same_sql_bind (
     $sql,
     \@bind,
-    'SELECT COUNT( * ) FROM cd me LEFT JOIN track tracks ON tracks.cd = me.cdid JOIN cd disc ON disc.cdid = tracks.cd LEFT JOIN lyrics lyrics ON lyrics.track_id = tracks.trackid WHERE ( ( position = ? OR position = ? ) )',
+    'SELECT COUNT( * ) FROM cd me JOIN track tracks ON tracks.cd = me.cdid JOIN cd disc ON disc.cdid = tracks.cd LEFT JOIN lyrics lyrics ON lyrics.track_id = tracks.trackid WHERE ( ( position = ? OR position = ? ) )',
     [ qw/'1' '2'/ ],
   );
 }
index 8c777ea..9b340c5 100644 (file)
@@ -3,17 +3,12 @@ use warnings FATAL => 'all';
 
 use Test::More;
 
-BEGIN {
-    eval "use SQL::Abstract 1.49";
-    plan $@
-        ? ( skip_all => "Needs SQLA 1.49+" )
-        : ( tests => 8 );
-}
-
 use lib qw(t/lib);
 use DBICTest;
 use DBIC::SqlMakerTest;
 
+plan tests => 8;
+
 my $schema = DBICTest->init_schema();
 my $art_rs = $schema->resultset('Artist');
 my $cdrs = $schema->resultset('CD');
similarity index 75%
rename from t/73oracle_inflate.t
rename to t/inflate/datetime_oracle.t
index 0f2fc23..22fabce 100644 (file)
@@ -17,7 +17,7 @@ else {
         plan skip_all => 'needs DateTime and DateTime::Format::Oracle for testing';
     }
     else {
-        plan tests => 7;
+        plan tests => 10;
     }
 }
 
@@ -67,9 +67,36 @@ $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' );
+
 # clean up our mess
 END {
-    if($dbh) {
+    if($schema && ($dbh = $schema->storage->dbh)) {
         $dbh->do("DROP TABLE track");
     }
 }
index 054edf1..2b19df4 100644 (file)
@@ -13,7 +13,7 @@ use DBICTest;
 eval { require DateTime::Format::Pg };
 plan $@
   ? ( skip_all =>  'Need DateTime::Format::Pg for timestamp inflation tests')
-  : ( tests => 3 )
+  : ( tests => 6 )
 ;
 
 
@@ -27,4 +27,14 @@ my $schema = DBICTest->init_schema();
   is($event->created_on->time_zone->name, "America/Chicago", "Timezone changed");
   # Time zone difference -> -6hours
   is($event->created_on->iso8601, "2009-01-15T11:00:00", "Time with TZ correct");
+
+# test 'timestamp without time zone'
+  my $dt = DateTime->from_epoch(epoch => time);
+  $dt->set_nanosecond(int 500_000_000);
+  $event->update({ts_without_tz => $dt});
+  $event->discard_changes;
+  isa_ok($event->ts_without_tz, "DateTime") or diag $event->created_on;
+  is($event->ts_without_tz, $dt, 'timestamp without time zone inflation');
+  is($event->ts_without_tz->microsecond, $dt->microsecond,
+    'timestamp without time zone microseconds survived');
 }
index 3532900..1d32dd6 100644 (file)
@@ -6,7 +6,6 @@ use lib qw(t/lib);
 use DBICTest;
 my $schema = DBICTest->init_schema();
 
-
 # Under some versions of SQLite if the $rs is left hanging around it will lock
 # So we create a scope here cos I'm lazy
 {
@@ -124,3 +123,15 @@ $rs_hashrefinf = $schema->resultset ('Artist')->search ({ 'me.artistid' => 1}, {
 });
 $rs_hashrefinf->result_class('DBIx::Class::ResultClass::HashRefInflator');
 is_deeply [$rs_hashrefinf->all], \@hashrefinf, 'Check query using extended columns syntax';
+
+# check nested prefetching of has_many relationships which return nothing
+my $artist = $schema->resultset ('Artist')->create ({ name => 'unsuccessful artist without CDs'});
+$artist->discard_changes;
+my $rs_artists = $schema->resultset ('Artist')->search ({ 'me.artistid' => $artist->id}, {
+    prefetch => { cds => 'tracks' }, result_class => 'DBIx::Class::ResultClass::HashRefInflator',
+});
+is_deeply(
+  [$rs_artists->all],
+  [{ $artist->get_columns, cds => [] }],
+  'nested has_many prefetch without entries'
+);
index 59c0997..c2be971 100644 (file)
@@ -32,7 +32,7 @@ foreach my $serializer (@serializers) {
 
 plan (skip_all => "No suitable serializer found") unless $selected;
 
-plan (tests => 8);
+plan (tests => 11);
 DBICTest::Schema::Serialized->inflate_column( 'serialized',
     { inflate => $selected->{inflater},
       deflate => $selected->{deflater},
@@ -84,3 +84,17 @@ is_deeply($object->serialized, $struct_hash, 'inflated hash matches original');
 ok($object->update( { serialized => $struct_array } ), 'arrayref deflation');
 ok($inflated = $object->serialized, 'arrayref inflation');
 is_deeply($inflated, $struct_array, 'inflated array matches original');
+
+
+#===== make sure make_column_dirty ineracts reasonably with inflation
+$object = $rs->first;
+$object->update ({serialized => { x => 'y'}});
+
+$object->serialized->{x} = 'z'; # change state without notifying $object
+ok (!$object->get_dirty_columns, 'no dirty columns yet');
+is_deeply ($object->serialized, { x => 'z' }, 'object data correct');
+
+$object->make_column_dirty('serialized');
+$object->update;
+
+is_deeply ($rs->first->serialized, { x => 'z' }, 'changes made it to the db' );
index b03d090..a3e4484 100644 (file)
@@ -20,6 +20,8 @@ __PACKAGE__->load_classes(qw/
   Tag
   Year2000CDs
   Year1999CDs
+  CustomSql
+  Money
   /,
   { 'DBICTest::Schema' => [qw/
     LinerNotes
index 1626787..2f4d85f 100644 (file)
@@ -11,7 +11,7 @@ __PACKAGE__->add_columns(
 __PACKAGE__->set_primary_key(qw/id1 id2/);
 
 __PACKAGE__->belongs_to( 'artist1', 'DBICTest::Schema::Artist', 'id1', { on_delete => 'RESTRICT', on_update => 'CASCADE'} );
-__PACKAGE__->belongs_to( 'artist2', 'DBICTest::Schema::Artist', 'id2', { on_delete => undef, on_update => 'CASCADE'} );
+__PACKAGE__->belongs_to( 'artist2', 'DBICTest::Schema::Artist', 'id2', { on_delete => undef, on_update => undef} );
 __PACKAGE__->has_many(
   'mapped_artists', 'DBICTest::Schema::Artist',
   [ {'foreign.artistid' => 'self.id1'}, {'foreign.artistid' => 'self.id2'} ],
index 849096b..4eecef5 100644 (file)
@@ -7,6 +7,7 @@ __PACKAGE__->table('cd_artwork');
 __PACKAGE__->add_columns(
   'cd_id' => {
     data_type => 'integer',
+    is_nullable => 0,
   },
 );
 __PACKAGE__->set_primary_key('cd_id');
index c468936..8c2c3b1 100644 (file)
@@ -15,6 +15,7 @@ __PACKAGE__->add_columns(
     },
     'link' => {
         data_type => 'integer',
+        is_nullable => 1,
     },
 );
 
index ec6ab24..80af1df 100644 (file)
@@ -56,6 +56,7 @@ __PACKAGE__->might_have(
     { proxy => [ qw/notes/ ] },
 );
 __PACKAGE__->might_have(artwork => 'DBICTest::Schema::Artwork', 'cd_id');
+__PACKAGE__->has_one(mandatory_artwork => 'DBICTest::Schema::Artwork', 'cd_id');
 
 __PACKAGE__->many_to_many( producers => cd_to_producer => 'producer' );
 __PACKAGE__->many_to_many(
diff --git a/t/lib/DBICTest/Schema/CustomSql.pm b/t/lib/DBICTest/Schema/CustomSql.pm
new file mode 100644 (file)
index 0000000..d169d72
--- /dev/null
@@ -0,0 +1,15 @@
+package # hide from PAUSE 
+    DBICTest::Schema::CustomSql;
+
+use base qw/DBICTest::Schema::Artist/;
+
+__PACKAGE__->table('dummy');
+
+__PACKAGE__->result_source_instance->name(\<<SQL);
+  ( SELECT a.*, cd.cdid AS cdid, cd.title AS title, cd.year AS year 
+  FROM artist a
+  JOIN cd ON cd.artist = a.artistid
+  WHERE cd.year = ?)
+SQL
+
+1;
index c56c1bd..22b655e 100644 (file)
@@ -15,6 +15,7 @@ __PACKAGE__->add_columns(
   varchar_date => { data_type => 'varchar', inflate_date => 1, size => 20, is_nullable => 1 },
   varchar_datetime => { data_type => 'varchar', inflate_datetime => 1, size => 20, is_nullable => 1 },
   skip_inflation => { data_type => 'datetime', inflate_datetime => 0, is_nullable => 1 },
+  ts_without_tz => { data_type => 'datetime', is_nullable => 1 }, # used in EventTZPg
 );
 
 __PACKAGE__->set_primary_key('id');
index e2b512e..444fe69 100644 (file)
@@ -12,6 +12,7 @@ __PACKAGE__->add_columns(
   id => { data_type => 'integer', is_auto_increment => 1 },
   starts_at => { data_type => 'datetime', timezone => "America/Chicago", locale => 'de_DE' },
   created_on => { data_type => 'timestamp with time zone', timezone => "America/Chicago" },
+  ts_without_tz => { data_type => 'timestamp without time zone' },
 );
 
 __PACKAGE__->set_primary_key('id');
index 3b3675a..dceabc9 100644 (file)
@@ -20,4 +20,6 @@ __PACKAGE__->add_unique_constraint ( genre_name => [qw/name/] );
 
 __PACKAGE__->has_many (cds => 'DBICTest::Schema::CD', 'genreid');
 
+__PACKAGE__->has_one (model_cd => 'DBICTest::Schema::CD', 'genreid');
+
 1;
diff --git a/t/lib/DBICTest/Schema/Money.pm b/t/lib/DBICTest/Schema/Money.pm
new file mode 100644 (file)
index 0000000..f4586eb
--- /dev/null
@@ -0,0 +1,21 @@
+package # hide from PAUSE 
+    DBICTest::Schema::Money;
+
+use base qw/DBICTest::BaseResult/;
+
+__PACKAGE__->table('money_test');
+
+__PACKAGE__->add_columns(
+  'id' => {
+    data_type => 'integer',
+    is_auto_increment => 1,
+  },
+  'amount' => {
+    data_type => 'money',
+    is_nullable => 1,
+  },
+);
+
+__PACKAGE__->set_primary_key('id');
+
+1;
old mode 100755 (executable)
new mode 100644 (file)
index 441c811..463c2c6 100644 (file)
@@ -1,6 +1,6 @@
 -- 
 -- Created by SQL::Translator::Producer::SQLite
--- Created on Thu May 28 10:10:00 2009
+-- Created on Thu Jul 30 08:44:22 2009
 -- 
 
 
@@ -17,41 +17,6 @@ CREATE TABLE artist (
 );
 
 --
--- Table: artist_undirected_map
---
-CREATE TABLE artist_undirected_map (
-  id1 integer NOT NULL,
-  id2 integer NOT NULL,
-  PRIMARY KEY (id1, id2)
-);
-
-CREATE INDEX artist_undirected_map_idx_id1_ ON artist_undirected_map (id1);
-
-CREATE INDEX artist_undirected_map_idx_id2_ ON artist_undirected_map (id2);
-
---
--- Table: cd_artwork
---
-CREATE TABLE cd_artwork (
-  cd_id INTEGER PRIMARY KEY NOT NULL
-);
-
-CREATE INDEX cd_artwork_idx_cd_id_cd_artwor ON cd_artwork (cd_id);
-
---
--- Table: artwork_to_artist
---
-CREATE TABLE artwork_to_artist (
-  artwork_cd_id integer NOT NULL,
-  artist_id integer NOT NULL,
-  PRIMARY KEY (artwork_cd_id, artist_id)
-);
-
-CREATE INDEX artwork_to_artist_idx_artist_id_artwork_to_arti ON artwork_to_artist (artist_id);
-
-CREATE INDEX artwork_to_artist_idx_artwork_cd_id_artwork_to_ ON artwork_to_artist (artwork_cd_id);
-
---
 -- Table: bindtype_test
 --
 CREATE TABLE bindtype_test (
@@ -62,63 +27,6 @@ CREATE TABLE bindtype_test (
 );
 
 --
--- Table: bookmark
---
-CREATE TABLE bookmark (
-  id INTEGER PRIMARY KEY NOT NULL,
-  link integer NOT NULL
-);
-
-CREATE INDEX bookmark_idx_link_bookmark ON bookmark (link);
-
---
--- Table: books
---
-CREATE TABLE books (
-  id INTEGER PRIMARY KEY NOT NULL,
-  source varchar(100) NOT NULL,
-  owner integer NOT NULL,
-  title varchar(100) NOT NULL,
-  price integer
-);
-
-CREATE INDEX books_idx_owner_books ON books (owner);
-
---
--- Table: cd
---
-CREATE TABLE cd (
-  cdid INTEGER PRIMARY KEY NOT NULL,
-  artist integer NOT NULL,
-  title varchar(100) NOT NULL,
-  year varchar(100) NOT NULL,
-  genreid integer,
-  single_track integer
-);
-
-CREATE INDEX cd_idx_artist_cd ON cd (artist);
-
-CREATE INDEX cd_idx_genreid_cd ON cd (genreid);
-
-CREATE INDEX cd_idx_single_track_cd ON cd (single_track);
-
-CREATE UNIQUE INDEX cd_artist_title_cd ON cd (artist, title);
-
---
--- Table: cd_to_producer
---
-CREATE TABLE cd_to_producer (
-  cd integer NOT NULL,
-  producer integer NOT NULL,
-  attribute integer,
-  PRIMARY KEY (cd, producer)
-);
-
-CREATE INDEX cd_to_producer_idx_cd_cd_to_pr ON cd_to_producer (cd);
-
-CREATE INDEX cd_to_producer_idx_producer_cd ON cd_to_producer (producer);
-
---
 -- Table: collection
 --
 CREATE TABLE collection (
@@ -127,19 +35,6 @@ CREATE TABLE collection (
 );
 
 --
--- Table: collection_object
---
-CREATE TABLE collection_object (
-  collection integer NOT NULL,
-  object integer NOT NULL,
-  PRIMARY KEY (collection, object)
-);
-
-CREATE INDEX collection_object_idx_collection_collection_obj ON collection_object (collection);
-
-CREATE INDEX collection_object_idx_object_c ON collection_object (object);
-
---
 -- Table: employee
 --
 CREATE TABLE employee (
@@ -168,7 +63,8 @@ CREATE TABLE event (
   created_on timestamp NOT NULL,
   varchar_date varchar(20),
   varchar_datetime varchar(20),
-  skip_inflation datetime
+  skip_inflation datetime,
+  ts_without_tz datetime
 );
 
 --
@@ -180,16 +76,6 @@ CREATE TABLE file_columns (
 );
 
 --
--- Table: forceforeign
---
-CREATE TABLE forceforeign (
-  artist INTEGER PRIMARY KEY NOT NULL,
-  cd integer NOT NULL
-);
-
-CREATE INDEX forceforeign_idx_artist_forcef ON forceforeign (artist);
-
---
 -- Table: fourkeys
 --
 CREATE TABLE fourkeys (
@@ -203,25 +89,6 @@ CREATE TABLE fourkeys (
 );
 
 --
--- Table: fourkeys_to_twokeys
---
-CREATE TABLE fourkeys_to_twokeys (
-  f_foo integer NOT NULL,
-  f_bar integer NOT NULL,
-  f_hello integer NOT NULL,
-  f_goodbye integer NOT NULL,
-  t_artist integer NOT NULL,
-  t_cd integer NOT NULL,
-  autopilot character NOT NULL,
-  pilot_sequence integer,
-  PRIMARY KEY (f_foo, f_bar, f_hello, f_goodbye, t_artist, t_cd)
-);
-
-CREATE INDEX fourkeys_to_twokeys_idx_f_foo_f_bar_f_hello_f_goodbye_ ON fourkeys_to_twokeys (f_foo, f_bar, f_hello, f_goodbye);
-
-CREATE INDEX fourkeys_to_twokeys_idx_t_artist_t_cd_fourkeys_to ON fourkeys_to_twokeys (t_artist, t_cd);
-
---
 -- Table: genre
 --
 CREATE TABLE genre (
@@ -229,29 +96,7 @@ CREATE TABLE genre (
   name varchar(100) NOT NULL
 );
 
-CREATE UNIQUE INDEX genre_name_genre ON genre (name);
-
---
--- Table: images
---
-CREATE TABLE images (
-  id INTEGER PRIMARY KEY NOT NULL,
-  artwork_id integer NOT NULL,
-  name varchar(100) NOT NULL,
-  data blob
-);
-
-CREATE INDEX images_idx_artwork_id_images ON images (artwork_id);
-
---
--- Table: liner_notes
---
-CREATE TABLE liner_notes (
-  liner_id INTEGER PRIMARY KEY NOT NULL,
-  notes varchar(100) NOT NULL
-);
-
-CREATE INDEX liner_notes_idx_liner_id_liner ON liner_notes (liner_id);
+CREATE UNIQUE INDEX genre_name ON genre (name);
 
 --
 -- Table: link
@@ -263,26 +108,13 @@ CREATE TABLE link (
 );
 
 --
--- Table: lyric_versions
+-- Table: money_test
 --
-CREATE TABLE lyric_versions (
+CREATE TABLE money_test (
   id INTEGER PRIMARY KEY NOT NULL,
-  lyric_id integer NOT NULL,
-  text varchar(100) NOT NULL
+  amount money
 );
 
-CREATE INDEX lyric_versions_idx_lyric_id_ly ON lyric_versions (lyric_id);
-
---
--- Table: lyrics
---
-CREATE TABLE lyrics (
-  lyric_id INTEGER PRIMARY KEY NOT NULL,
-  track_id integer NOT NULL
-);
-
-CREATE INDEX lyrics_idx_track_id_lyrics ON lyrics (track_id);
-
 --
 -- Table: noprimarykey
 --
@@ -292,7 +124,7 @@ CREATE TABLE noprimarykey (
   baz integer NOT NULL
 );
 
-CREATE UNIQUE INDEX foo_bar_noprimarykey ON noprimarykey (foo, bar);
+CREATE UNIQUE INDEX foo_bar ON noprimarykey (foo, bar);
 
 --
 -- Table: onekey
@@ -319,7 +151,7 @@ CREATE TABLE producer (
   name varchar(100) NOT NULL
 );
 
-CREATE UNIQUE INDEX prod_name_producer ON producer (name);
+CREATE UNIQUE INDEX prod_name ON producer (name);
 
 --
 -- Table: self_ref
@@ -330,19 +162,6 @@ CREATE TABLE self_ref (
 );
 
 --
--- Table: self_ref_alias
---
-CREATE TABLE self_ref_alias (
-  self_ref integer NOT NULL,
-  alias integer NOT NULL,
-  PRIMARY KEY (self_ref, alias)
-);
-
-CREATE INDEX self_ref_alias_idx_alias_self_ ON self_ref_alias (alias);
-
-CREATE INDEX self_ref_alias_idx_self_ref_se ON self_ref_alias (self_ref);
-
---
 -- Table: sequence_test
 --
 CREATE TABLE sequence_test (
@@ -362,15 +181,99 @@ CREATE TABLE serialized (
 );
 
 --
--- Table: tags
+-- Table: treelike
 --
-CREATE TABLE tags (
-  tagid INTEGER PRIMARY KEY NOT NULL,
-  cd integer NOT NULL,
-  tag varchar(100) NOT NULL
+CREATE TABLE treelike (
+  id INTEGER PRIMARY KEY NOT NULL,
+  parent integer,
+  name varchar(100) NOT NULL
 );
 
-CREATE INDEX tags_idx_cd_tags ON tags (cd);
+CREATE INDEX treelike_idx_parent ON treelike (parent);
+
+--
+-- Table: twokeytreelike
+--
+CREATE TABLE twokeytreelike (
+  id1 integer NOT NULL,
+  id2 integer NOT NULL,
+  parent1 integer NOT NULL,
+  parent2 integer NOT NULL,
+  name varchar(100) NOT NULL,
+  PRIMARY KEY (id1, id2)
+);
+
+CREATE INDEX twokeytreelike_idx_parent1_parent2 ON twokeytreelike (parent1, parent2);
+
+CREATE UNIQUE INDEX tktlnameunique ON twokeytreelike (name);
+
+--
+-- Table: typed_object
+--
+CREATE TABLE typed_object (
+  objectid INTEGER PRIMARY KEY NOT NULL,
+  type varchar(100) NOT NULL,
+  value varchar(100) NOT NULL
+);
+
+--
+-- Table: artist_undirected_map
+--
+CREATE TABLE artist_undirected_map (
+  id1 integer NOT NULL,
+  id2 integer NOT NULL,
+  PRIMARY KEY (id1, id2)
+);
+
+CREATE INDEX artist_undirected_map_idx_id1 ON artist_undirected_map (id1);
+
+CREATE INDEX artist_undirected_map_idx_id2 ON artist_undirected_map (id2);
+
+--
+-- Table: bookmark
+--
+CREATE TABLE bookmark (
+  id INTEGER PRIMARY KEY NOT NULL,
+  link integer
+);
+
+CREATE INDEX bookmark_idx_link ON bookmark (link);
+
+--
+-- Table: books
+--
+CREATE TABLE books (
+  id INTEGER PRIMARY KEY NOT NULL,
+  source varchar(100) NOT NULL,
+  owner integer NOT NULL,
+  title varchar(100) NOT NULL,
+  price integer
+);
+
+CREATE INDEX books_idx_owner ON books (owner);
+
+--
+-- Table: forceforeign
+--
+CREATE TABLE forceforeign (
+  artist INTEGER PRIMARY KEY NOT NULL,
+  cd integer NOT NULL
+);
+
+CREATE INDEX forceforeign_idx_artist ON forceforeign (artist);
+
+--
+-- Table: self_ref_alias
+--
+CREATE TABLE self_ref_alias (
+  self_ref integer NOT NULL,
+  alias integer NOT NULL,
+  PRIMARY KEY (self_ref, alias)
+);
+
+CREATE INDEX self_ref_alias_idx_alias ON self_ref_alias (alias);
+
+CREATE INDEX self_ref_alias_idx_self_ref ON self_ref_alias (self_ref);
 
 --
 -- Table: track
@@ -384,38 +287,121 @@ CREATE TABLE track (
   last_updated_at datetime
 );
 
-CREATE INDEX track_idx_cd_track ON track (cd);
+CREATE INDEX track_idx_cd ON track (cd);
 
-CREATE UNIQUE INDEX track_cd_position_track ON track (cd, position);
+CREATE UNIQUE INDEX track_cd_position ON track (cd, position);
 
-CREATE UNIQUE INDEX track_cd_title_track ON track (cd, title);
+CREATE UNIQUE INDEX track_cd_title ON track (cd, title);
 
 --
--- Table: treelike
+-- Table: cd
 --
-CREATE TABLE treelike (
+CREATE TABLE cd (
+  cdid INTEGER PRIMARY KEY NOT NULL,
+  artist integer NOT NULL,
+  title varchar(100) NOT NULL,
+  year varchar(100) NOT NULL,
+  genreid integer,
+  single_track integer
+);
+
+CREATE INDEX cd_idx_artist ON cd (artist);
+
+CREATE INDEX cd_idx_genreid ON cd (genreid);
+
+CREATE INDEX cd_idx_single_track ON cd (single_track);
+
+CREATE UNIQUE INDEX cd_artist_title ON cd (artist, title);
+
+--
+-- Table: collection_object
+--
+CREATE TABLE collection_object (
+  collection integer NOT NULL,
+  object integer NOT NULL,
+  PRIMARY KEY (collection, object)
+);
+
+CREATE INDEX collection_object_idx_collection ON collection_object (collection);
+
+CREATE INDEX collection_object_idx_object ON collection_object (object);
+
+--
+-- Table: lyrics
+--
+CREATE TABLE lyrics (
+  lyric_id INTEGER PRIMARY KEY NOT NULL,
+  track_id integer NOT NULL
+);
+
+CREATE INDEX lyrics_idx_track_id ON lyrics (track_id);
+
+--
+-- Table: cd_artwork
+--
+CREATE TABLE cd_artwork (
+  cd_id INTEGER PRIMARY KEY NOT NULL
+);
+
+CREATE INDEX cd_artwork_idx_cd_id ON cd_artwork (cd_id);
+
+--
+-- Table: liner_notes
+--
+CREATE TABLE liner_notes (
+  liner_id INTEGER PRIMARY KEY NOT NULL,
+  notes varchar(100) NOT NULL
+);
+
+CREATE INDEX liner_notes_idx_liner_id ON liner_notes (liner_id);
+
+--
+-- Table: lyric_versions
+--
+CREATE TABLE lyric_versions (
   id INTEGER PRIMARY KEY NOT NULL,
-  parent integer,
-  name varchar(100) NOT NULL
+  lyric_id integer NOT NULL,
+  text varchar(100) NOT NULL
 );
 
-CREATE INDEX treelike_idx_parent_treelike ON treelike (parent);
+CREATE INDEX lyric_versions_idx_lyric_id ON lyric_versions (lyric_id);
 
 --
--- Table: twokeytreelike
+-- Table: tags
 --
-CREATE TABLE twokeytreelike (
-  id1 integer NOT NULL,
-  id2 integer NOT NULL,
-  parent1 integer NOT NULL,
-  parent2 integer NOT NULL,
-  name varchar(100) NOT NULL,
-  PRIMARY KEY (id1, id2)
+CREATE TABLE tags (
+  tagid INTEGER PRIMARY KEY NOT NULL,
+  cd integer NOT NULL,
+  tag varchar(100) NOT NULL
+);
+
+CREATE INDEX tags_idx_cd ON tags (cd);
+
+--
+-- Table: cd_to_producer
+--
+CREATE TABLE cd_to_producer (
+  cd integer NOT NULL,
+  producer integer NOT NULL,
+  attribute integer,
+  PRIMARY KEY (cd, producer)
 );
 
-CREATE INDEX twokeytreelike_idx_parent1_parent2_twokeytre ON twokeytreelike (parent1, parent2);
+CREATE INDEX cd_to_producer_idx_cd ON cd_to_producer (cd);
 
-CREATE UNIQUE INDEX tktlnameunique_twokeytreelike ON twokeytreelike (name);
+CREATE INDEX cd_to_producer_idx_producer ON cd_to_producer (producer);
+
+--
+-- Table: images
+--
+CREATE TABLE images (
+  id INTEGER PRIMARY KEY NOT NULL,
+  artwork_id integer NOT NULL,
+  name varchar(100) NOT NULL,
+  data blob
+);
+
+CREATE INDEX images_idx_artwork_id ON images (artwork_id);
 
 --
 -- Table: twokeys
@@ -426,17 +412,40 @@ CREATE TABLE twokeys (
   PRIMARY KEY (artist, cd)
 );
 
-CREATE INDEX twokeys_idx_artist_twokeys ON twokeys (artist);
+CREATE INDEX twokeys_idx_artist ON twokeys (artist);
 
 --
--- Table: typed_object
+-- Table: artwork_to_artist
 --
-CREATE TABLE typed_object (
-  objectid INTEGER PRIMARY KEY NOT NULL,
-  type varchar(100) NOT NULL,
-  value varchar(100) NOT NULL
+CREATE TABLE artwork_to_artist (
+  artwork_cd_id integer NOT NULL,
+  artist_id integer NOT NULL,
+  PRIMARY KEY (artwork_cd_id, artist_id)
 );
 
+CREATE INDEX artwork_to_artist_idx_artist_id ON artwork_to_artist (artist_id);
+
+CREATE INDEX artwork_to_artist_idx_artwork_cd_id ON artwork_to_artist (artwork_cd_id);
+
+--
+-- Table: fourkeys_to_twokeys
+--
+CREATE TABLE fourkeys_to_twokeys (
+  f_foo integer NOT NULL,
+  f_bar integer NOT NULL,
+  f_hello integer NOT NULL,
+  f_goodbye integer NOT NULL,
+  t_artist integer NOT NULL,
+  t_cd integer NOT NULL,
+  autopilot character NOT NULL,
+  pilot_sequence integer,
+  PRIMARY KEY (f_foo, f_bar, f_hello, f_goodbye, t_artist, t_cd)
+);
+
+CREATE INDEX fourkeys_to_twokeys_idx_f_foo_f_bar_f_hello_f_goodbye ON fourkeys_to_twokeys (f_foo, f_bar, f_hello, f_goodbye);
+
+CREATE INDEX fourkeys_to_twokeys_idx_t_artist_t_cd ON fourkeys_to_twokeys (t_artist, t_cd);
+
 --
 -- View: year2000cds
 --
index 4275f58..26934c9 100644 (file)
@@ -23,7 +23,7 @@ lives_ok ( sub {
 
   my $cd2 = $schema->resultset('CD')->search ( { cdid => { '!=', $cd->cdid } }, {rows => 1} )->single;  # retrieve a cd different from the first
   $cd2->add_to_producers ({name => 'new m2m producer'});                                                # attach to an existing producer
-  ok ($cd2->producers->find ({name => 'new m2m producer'}), 'Exsiting producer attached to existing cd');
+  ok ($cd2->producers->find ({name => 'new m2m producer'}), 'Existing producer attached to existing cd');
 
 }, 'Test far-end find_or_create over many_to_many');
 
diff --git a/t/multi_create/multilev_single_PKeqFK.t b/t/multi_create/multilev_single_PKeqFK.t
new file mode 100644 (file)
index 0000000..07101a2
--- /dev/null
@@ -0,0 +1,105 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBICTest;
+
+sub mc_diag { diag (@_) if $ENV{DBIC_MULTICREATE_DEBUG} };
+
+plan tests => 26;
+
+my $schema = DBICTest->init_schema();
+
+mc_diag (<<'DG');
+* Test a multilevel might-have/has_one with a PK == FK in the mid-table
+
+CD -> might have -> Artwork
+    \- has_one -/     \
+                       \
+                        \-> has_many \
+                                      --> Artwork_to_Artist
+                        /-> has_many /
+                       /
+                     Artist
+DG
+
+my $rels = {
+  has_one => 'mandatory_artwork',
+  might_have => 'artwork',
+};
+
+for my $type (qw/has_one might_have/) {
+
+  lives_ok (sub {
+
+    my $rel = $rels->{$type};
+    my $cd_title = "Simple test $type cd";
+
+    my $cd = $schema->resultset('CD')->create ({
+      artist => 1,
+      title => $cd_title,
+      year => 2008,
+      $rel => {},
+    });
+
+    isa_ok ($cd, 'DBICTest::CD', 'Main CD object created');
+    is ($cd->title, $cd_title, 'Correct CD title');
+
+    isa_ok ($cd->$rel, 'DBICTest::Artwork', 'Related artwork present');
+    ok ($cd->$rel->in_storage, 'And in storage');
+
+  }, "Simple $type creation");
+}
+
+my $artist_rs = $schema->resultset('Artist');
+for my $type (qw/has_one might_have/) {
+
+  my $rel = $rels->{$type};
+
+  my $cd_title = "Test $type cd";
+  my $artist_names = [ map { "Artist via $type $_" } (1, 2) ];
+
+  my $someartist = $artist_rs->next;
+
+  lives_ok (sub {
+    my $cd = $schema->resultset('CD')->create ({
+      artist => $someartist,
+      title => $cd_title,
+      year => 2008,
+      $rel => {
+      artwork_to_artist => [ map {
+            { artist => { name => $_ } }
+          } (@$artist_names)
+        ]
+      },
+    });
+
+
+    isa_ok ($cd, 'DBICTest::CD', 'Main CD object created');
+    is ($cd->title, $cd_title, 'Correct CD title');
+
+    my $art_obj = $cd->$rel;
+    ok ($art_obj->has_column_loaded ('cd_id'), 'PK/FK present on artwork object');
+    is ($art_obj->artists->count, 2, 'Correct artwork creator count via the new object');
+    is_deeply (
+      [ sort $art_obj->artists->get_column ('name')->all ],
+      $artist_names,
+      'Artists named correctly when queried via object',
+    );
+
+    my $artwork = $schema->resultset('Artwork')->search (
+      { 'cd.title' => $cd_title },
+      { join => 'cd' },
+    )->single;
+    is ($artwork->artists->count, 2, 'Correct artwork creator count via a new search');
+    is_deeply (
+        [ sort $artwork->artists->get_column ('name')->all ],
+      $artist_names,
+      'Artists named correctly queried via a new search',
+    );
+  }, "multilevel $type with a PK == FK in the $type/has_many table ok");
+}
+
+1;
similarity index 98%
rename from t/96multi_create.t
rename to t/multi_create/standard.t
index 856a158..9cd754f 100644 (file)
@@ -6,7 +6,7 @@ use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
 
-plan tests => 92;
+plan tests => 93;
 
 my $schema = DBICTest->init_schema();
 
@@ -328,7 +328,7 @@ lives_ok ( sub {
   is($newartist2->name, 'Fred 3', 'Created new artist with cds via find_or_create');
 }, 'Nested find_or_create');
 
-throws_ok ( sub {
+lives_ok ( sub {
   my $artist2 = $schema->resultset('Artist')->create({
     name => 'Fred 4',
     cds => [
@@ -344,7 +344,9 @@ throws_ok ( sub {
       },
     ]
   });
-}, qr/title are not unique/, 'Multiple same level has_many create fails with duplicate error');
+
+  is($artist2->in_storage, 1, 'artist with duplicate rels inserted okay');
+}, 'Multiple same level has_many create');
 
 lives_ok ( sub {
        my $artist = $schema->resultset('Artist')->first;
similarity index 98%
rename from t/96multi_create_torture.t
rename to t/multi_create/torture.t
index e3a552d..28a4e1d 100644 (file)
@@ -143,7 +143,11 @@ eval {
   is (
     $cds_2012->search(
       { 'tags.tag' => { -in => [qw/A B/] } },
-      { join => 'tags', group_by => 'me.cdid' }
+      {
+        join => 'tags',
+        group_by => 'me.cdid',
+        having => 'count(me.cdid) = 2',
+      }
     ),
     5,
     'All 10 tags were pairwise distributed between 5 year-2012 CDs'
index 53894b8..41ad883 100644 (file)
@@ -8,16 +8,7 @@ use Data::Dumper;
 
 my $schema = DBICTest->init_schema();
 
-my $orig_debug = $schema->storage->debug;
-
-use IO::File;
-
-BEGIN {
-    eval "use DBD::SQLite";
-    plan $@
-        ? ( skip_all => 'needs DBD::SQLite for testing' )
-        : ( tests => 3 );
-}
+plan tests => 3;
 
 # bug in 0.07000 caused attr (join/prefetch) to be modifed by search
 # so we check the search & attr arrays are not modified
diff --git a/t/prefetch/count.t b/t/prefetch/count.t
new file mode 100644 (file)
index 0000000..49370a4
--- /dev/null
@@ -0,0 +1,101 @@
+use strict;
+use warnings;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest;
+use DBIC::SqlMakerTest;
+
+plan tests => 23;
+
+my $schema = DBICTest->init_schema();
+
+my $cd_rs = $schema->resultset('CD')->search (
+  { 'tracks.cd' => { '!=', undef } },
+  { 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)');
+
+is($cd_rs->all, 5, 'Amount of CD objects with tracks');
+is($cd_rs->search_related('tracks')->count, 15, 'Tracks associated with CDs count (after SELECT()ing)');
+
+is($cd_rs->search_related ('tracks')->all, 15, 'Track objects associated with CDs (after SELECT()ing)');
+
+my $artist = $schema->resultset('Artist')->create({name => 'xxx'});
+
+my $artist_rs = $schema->resultset('Artist')->search(
+  {artistid => $artist->id},
+  {prefetch=>'cds', join => 'twokeys' }
+);
+
+is($artist_rs->count, 1, "New artist found with prefetch turned on");
+is(scalar($artist_rs->all), 1, "New artist fetched with prefetch turned on");
+is($artist_rs->related_resultset('cds')->count, 0, "No CDs counted on a brand new artist");
+is(scalar($artist_rs->related_resultset('cds')->all), 0, "No CDs fetched on a brand new artist (count == fetch)");
+
+# create a cd, and make sure the non-existing join does not skew the count
+$artist->create_related ('cds', { title => 'yyy', year => '1999' });
+is($artist_rs->related_resultset('cds')->count, 1, "1 CDs counted on a brand new artist");
+is(scalar($artist_rs->related_resultset('cds')->all), 1, "1 CDs prefetched on a brand new artist (count == fetch)");
+
+# Really fuck shit up with one more cd and some insanity
+# this doesn't quite work as there are the prefetch gets lost
+# on search_related. This however is too esoteric to fix right
+# now
+
+my $cd2 = $artist->create_related ('cds', {
+    title => 'zzz',
+    year => '1999',
+    tracks => [{ title => 'ping' }, { title => 'pong' }],
+});
+
+my $cds = $cd2->search_related ('artist', {}, { 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 Track objects on cd via artist via one of the cds");
+
+is($cds->count, 2, "2 CDs counted on artist via one of the cds");
+is(scalar($cds->all), 2, "2 CD objectson artist via one of the cds");
+
+# make sure the join collapses all the way
+is_same_sql_bind (
+  $tracks->count_rs->as_query,
+  '(
+    SELECT COUNT( * )
+      FROM artist me
+      LEFT JOIN twokeys twokeys ON twokeys.artist = me.artistid
+      JOIN cd cds ON cds.artist = me.artistid
+      JOIN track tracks ON tracks.cd = cds.cdid
+    WHERE ( me.artistid = ? )
+  )',
+  [ [ 'me.artistid' => 4 ] ],
+);
+
+
+TODO: {
+  local $TODO = "Chaining with prefetch is fundamentally broken";
+
+  my $queries;
+  $schema->storage->debugcb ( sub { $queries++ } );
+  $schema->storage->debug (1);
+
+  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");
+
+  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 ($queries, 3, '2 counts + 1 prefetch?');
+}
diff --git a/t/prefetch/double_prefetch.t b/t/prefetch/double_prefetch.t
new file mode 100644 (file)
index 0000000..b666220
--- /dev/null
@@ -0,0 +1,35 @@
+use warnings;  
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBIC::SqlMakerTest;
+use DBICTest;
+
+my $schema = DBICTest->init_schema();
+
+plan tests => 1;
+
+# While this is a rather GIGO case, make sure it behaves as pre-103,
+# as it may result in hard-to-track bugs
+my $cds = $schema->resultset('Artist')
+            ->search_related ('cds')
+              ->search ({}, {
+                  prefetch => [ 'single_track', { single_track => 'cd' } ],
+                });
+
+is_same_sql(
+  ${$cds->as_query}->[0],
+  '(
+    SELECT
+      cds.cdid, cds.artist, cds.title, cds.year, cds.genreid, cds.single_track,
+      single_track.trackid, single_track.cd, single_track.position, single_track.title, single_track.last_updated_on, single_track.last_updated_at,
+      single_track_2.trackid, single_track_2.cd, single_track_2.position, single_track_2.title, single_track_2.last_updated_on, single_track_2.last_updated_at,
+      cd.cdid, cd.artist, cd.title, cd.year, cd.genreid, cd.single_track
+    FROM artist me
+      LEFT JOIN cd cds ON cds.artist = me.artistid
+      LEFT JOIN track single_track ON single_track.trackid = cds.single_track
+      LEFT JOIN track single_track_2 ON single_track_2.trackid = cds.single_track
+      LEFT JOIN cd cd ON cd.cdid = single_track_2.cd
+  )',
+);
diff --git a/t/prefetch/grouped.t b/t/prefetch/grouped.t
new file mode 100644 (file)
index 0000000..19fa923
--- /dev/null
@@ -0,0 +1,205 @@
+use strict;
+use warnings;
+use Test::More;
+
+use lib qw(t/lib);
+use DBICTest;
+use DBIC::SqlMakerTest;
+
+#plan tests => 6;
+plan 'no_plan';
+
+my $schema = DBICTest->init_schema();
+my $sdebug = $schema->storage->debug;
+
+my $cd_rs = $schema->resultset('CD')->search (
+  { 'tracks.cd' => { '!=', undef } },
+  { prefetch => 'tracks' },
+);
+
+# Database sanity check
+is($cd_rs->count, 5, 'CDs with tracks count');
+for ($cd_rs->all) {
+  is ($_->tracks->count, 3, '3 tracks for CD' . $_->id );
+}
+
+# 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 ] } },
+    {
+      select => [
+        'me.cd',
+        { count => 'me.trackid' },
+      ],
+      as => [qw/
+        cd
+        track_count
+      /],
+      group_by => [qw/me.cd/],
+      prefetch => 'cd',
+    },
+  );
+
+  # this used to fuck up ->all, do not remove!
+  ok ($track_rs->first, 'There is stuff in the rs');
+
+  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);
+
+    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);
+  }
+
+  # Test sql by hand, as the sqlite db will simply paper over
+  # improper group/select combinations
+  #
+  is_same_sql_bind (
+    $track_rs->count_rs->as_query,
+    '(
+      SELECT COUNT( * )
+        FROM (
+          SELECT me.cd
+            FROM track me
+            JOIN cd cd ON cd.cdid = me.cd
+          WHERE ( me.cd IN ( ?, ?, ?, ?, ? ) )
+          GROUP BY me.cd
+        )
+      count_subq
+    )',
+    [ map { [ 'me.cd' => $_] } ($cd_rs->get_column ('cdid')->all) ],
+    'count() query generated expected SQL',
+  );
+
+  is_same_sql_bind (
+    $track_rs->as_query,
+    '(
+      SELECT me.cd, me.track_count, cd.cdid, cd.artist, cd.title, cd.year, cd.genreid, cd.single_track
+        FROM (
+          SELECT me.cd, COUNT (me.trackid) AS track_count,
+            FROM track me
+            JOIN cd cd ON cd.cdid = me.cd
+          WHERE ( me.cd IN ( ?, ?, ?, ?, ? ) )
+          GROUP BY me.cd
+          ) as me
+        JOIN cd cd ON cd.cdid = me.cd
+      WHERE ( me.cd IN ( ?, ?, ?, ?, ? ) )
+    )',
+    [ map { [ 'me.cd' => $_] } ( ($cd_rs->get_column ('cdid')->all) x 2 ) ],
+    'next() query generated expected SQL',
+  );
+
+
+  # add an extra track to one of the cds, and then make sure we can get it on top
+  # (check if limit works)
+  my $top_cd = $cd_rs->slice (1,1)->next;
+  $top_cd->create_related ('tracks', {
+    title => 'over the top',
+  });
+
+  my $top_cd_collapsed_track = $track_rs->search ({}, {
+    rows => 2,
+    order_by => [
+      { -desc => 'track_count' },
+    ],
+  });
+
+  is ($top_cd_collapsed_track->count, 2);
+
+  is (
+    $top_cd->title,
+    $top_cd_collapsed_track->first->cd->title,
+    'Correct collapsed track with prefetched CD returned on top'
+  );
+}
+
+# test a has_many/might_have prefetch at the same level
+# Note that one of the CDs now has 4 tracks instead of 3
+{
+  my $most_tracks_rs = $schema->resultset ('CD')->search (
+    {
+      'me.cdid' => { '!=' => undef },  # duh - this is just to test WHERE
+    },
+    {
+      prefetch => [qw/tracks liner_notes/],
+      select => ['me.cdid', { count => 'tracks.trackid' } ],
+      as => [qw/cdid track_count/],
+      group_by => 'me.cdid',
+      order_by => { -desc => 'track_count' },
+      rows => 2,
+    }
+  );
+
+  is_same_sql_bind (
+    $most_tracks_rs->count_rs->as_query,
+    '(
+      SELECT COUNT( * )
+        FROM (
+          SELECT me.cdid
+            FROM cd me
+            LEFT JOIN track tracks ON tracks.cd = me.cdid
+            LEFT JOIN liner_notes liner_notes ON liner_notes.liner_id = me.cdid
+          WHERE ( me.cdid IS NOT NULL )
+          GROUP BY me.cdid
+          LIMIT 2
+        ) count_subq
+    )',
+    [],
+    'count() query generated expected SQL',
+  );
+
+  is_same_sql_bind (
+    $most_tracks_rs->as_query,
+    '(
+      SELECT me.cdid, me.track_count, tracks.trackid, tracks.cd, tracks.position, tracks.title, tracks.last_updated_on, tracks.last_updated_at, liner_notes.liner_id, liner_notes.notes
+        FROM (
+          SELECT me.cdid, COUNT( tracks.trackid ) AS track_count
+            FROM cd me
+            LEFT JOIN track tracks ON tracks.cd = me.cdid
+          WHERE ( me.cdid IS NOT NULL )
+          GROUP BY me.cdid
+          ORDER BY track_count DESC
+          LIMIT 2
+        ) me
+        LEFT JOIN track tracks ON tracks.cd = me.cdid
+        LEFT JOIN liner_notes liner_notes ON liner_notes.liner_id = me.cdid
+      WHERE ( me.cdid IS NOT NULL )
+      ORDER BY track_count DESC, tracks.cd
+    )',
+    [],
+    'next() query generated expected SQL',
+  );
+
+  is ($most_tracks_rs->count, 2, 'Limit works');
+  my $top_cd = $most_tracks_rs->first;
+  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);
+}
diff --git a/t/prefetch/incomplete.t b/t/prefetch/incomplete.t
new file mode 100644 (file)
index 0000000..a93e693
--- /dev/null
@@ -0,0 +1,53 @@
+use strict;
+use warnings;  
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBICTest;
+
+plan tests => 9;
+
+my $schema = DBICTest->init_schema();
+
+lives_ok(sub {
+  # while cds.* will be selected anyway (prefetch currently forces the result of _resolve_prefetch)
+  # only the requested me.name column will be fetched.
+
+  # reference sql with select => [...]
+  #   SELECT me.name, cds.title, cds.cdid, cds.artist, cds.title, cds.year, cds.genreid, cds.single_track FROM ...
+
+  my $rs = $schema->resultset('Artist')->search(
+    { 'cds.title' => { '!=', 'Generic Manufactured Singles' } },
+    {
+      prefetch => [ qw/ cds / ],
+      order_by => [ { -desc => 'me.name' }, 'cds.title' ],
+      select => [qw/ me.name  cds.title / ],
+    }
+  );
+
+  is ($rs->count, 2, 'Correct number of collapsed artists');
+  my $we_are_goth = $rs->first;
+  is ($we_are_goth->name, 'We Are Goth', 'Correct first artist');
+  is ($we_are_goth->cds->count, 1, 'Correct number of CDs for first artist');
+  is ($we_are_goth->cds->first->title, 'Come Be Depressed With Us', 'Correct cd for artist');
+}, 'explicit prefetch on a keyless object works');
+
+
+lives_ok(sub {
+  # test implicit prefetch as well
+
+  my $rs = $schema->resultset('CD')->search(
+    { title => 'Generic Manufactured Singles' },
+    {
+      join=> 'artist',
+      select => [qw/ me.title artist.name / ],
+    }
+  );
+
+  my $cd = $rs->next;
+  is ($cd->title, 'Generic Manufactured Singles', 'CD title prefetched correctly');
+  isa_ok ($cd->artist, 'DBICTest::Artist');
+  is ($cd->artist->name, 'Random Boy Band', 'Artist object has correct name');
+
+}, 'implicit keyless prefetch works');
index 96d86c5..ca89d55 100644 (file)
@@ -5,13 +5,13 @@ use Test::More;
 use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
-use Data::Dumper;
+use IO::File;
 
 plan tests => 10;
 
 my $schema = DBICTest->init_schema();
+my $sdebug = $schema->storage->debug;
 
-use IO::File;
 
 # once the following TODO is complete, remove the 2 warning tests immediately
 # after the TODO block
@@ -44,6 +44,9 @@ TODO: {
     ok(! $o_mm_warn, 'no warning on attempt to prefetch several same level has_many\'s (1 -> M + M)');
 
     is($queries, 1, 'prefetch one->(has_many,has_many) ran exactly 1 query');
+    $schema->storage->debugcb (undef);
+    $schema->storage->debug ($sdebug);
+
     is($pr_tracks_count, $tracks_count, 'equal count of prefetched relations over several same level has_many\'s (1 -> M + M)');
 
     for ($pr_tracks_rs, $tracks_rs) {
@@ -79,6 +82,8 @@ TODO: {
     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)');
 
diff --git a/t/prefetch/rows_bug.t b/t/prefetch/rows_bug.t
deleted file mode 100644 (file)
index 21bb6a2..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-# Test to ensure we get a consistent result set wether or not we use the
-# prefetch option in combination rows (LIMIT).
-use strict;
-use warnings;
-
-use Test::More;
-use lib qw(t/lib);
-use DBICTest;
-
-plan skip_all => 'fix pending';
-#plan tests => 4;
-
-my $schema = DBICTest->init_schema();
-my $no_prefetch = $schema->resultset('Artist')->search(
-  undef,
-  { rows => 3 }
-);
-
-my $use_prefetch = $schema->resultset('Artist')->search(
-  undef,
-  {
-    prefetch => 'cds',
-    rows     => 3
-  }
-);
-
-is($no_prefetch->count, $use_prefetch->count, '$no_prefetch->count == $use_prefetch->count');
-is(
-  scalar ($no_prefetch->all),
-  scalar ($use_prefetch->all),
-  "Amount of returned rows is right"
-);
-
-
-
-my $artist_many_cds = $schema->resultset('Artist')->search ( {}, {
-  join => 'cds',
-  group_by => 'me.artistid',
-  having => \ 'count(cds.cdid) > 1',
-})->first;
-
-
-$no_prefetch = $schema->resultset('Artist')->search(
-  { artistid => $artist_many_cds->id },
-  { rows => 1 }
-);
-
-$use_prefetch = $schema->resultset('Artist')->search(
-  { artistid => $artist_many_cds->id },
-  {
-    prefetch => 'cds',
-    rows     => 1
-  }
-);
-
-my $prefetch_artist = $use_prefetch->first;
-my $normal_artist = $no_prefetch->first;
-
-is(
-  $prefetch_artist->cds->count,
-  $normal_artist->cds->count,
-  "Count of child rel with prefetch + rows => 1 is right"
-);
-is (
-  scalar ($prefetch_artist->cds->all),
-  scalar ($normal_artist->cds->all),
-  "Amount of child rel rows with prefetch + rows => 1 is right"
-);
index 56cf77d..72426ee 100644 (file)
@@ -1,24 +1,17 @@
 use strict;
-use warnings;  
+use warnings;
 
 use Test::More;
 use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
 use Data::Dumper;
+use IO::File;
 
 my $schema = DBICTest->init_schema();
-
 my $orig_debug = $schema->storage->debug;
 
-use IO::File;
-
-BEGIN {
-    eval "use DBD::SQLite";
-    plan $@
-        ? ( skip_all => 'needs DBD::SQLite for testing' )
-        : ( tests => 45 );
-}
+plan tests => 44;
 
 my $queries = 0;
 $schema->storage->debugcb(sub { $queries++; });
@@ -227,29 +220,11 @@ is(eval { $tree_like->children->first->children->first->name }, 'quux',
 
 $tree_like = eval { $schema->resultset('TreeLike')->search(
     { 'children.id' => 3, 'children_2.id' => 6 }, 
-    { join => [qw/children children/] }
+    { 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');
 
-# test that collapsed joins don't get a _2 appended to the alias
-
-my $sql = '';
-$schema->storage->debugcb(sub { $sql = $_[1] });
-$schema->storage->debug(1);
-
-eval {
-  my $row = $schema->resultset('Artist')->search_related('cds', undef, {
-    join => 'tracks',
-    prefetch => 'tracks',
-  })->search_related('tracks')->first;
-};
-
-like( $sql, qr/^SELECT tracks_2\.trackid/, "join not collapsed for search_related" );
-
-$schema->storage->debug($orig_debug);
-$schema->storage->debugobj->callback(undef);
-
 $rs = $schema->resultset('Artist');
 $rs->create({ artistid => 4, name => 'Unknown singer-songwriter' });
 $rs->create({ artistid => 5, name => 'Emo 4ever' });
@@ -314,3 +289,5 @@ is($art_rs_pr->search_related('cds')->search_related('tracks')->first->title,
 
 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);
diff --git a/t/prefetch/with_limit.t b/t/prefetch/with_limit.t
new file mode 100644 (file)
index 0000000..1dd0829
--- /dev/null
@@ -0,0 +1,92 @@
+# Test to ensure we get a consistent result set wether or not we use the
+# prefetch option in combination rows (LIMIT).
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBICTest;
+
+plan tests => 9;
+
+my $schema = DBICTest->init_schema();
+
+
+my $no_prefetch = $schema->resultset('Artist')->search(
+  [   # search deliberately contrived
+    { 'artwork.cd_id' => undef },
+    { 'tracks.title' => { '!=' => 'blah-blah-1234568' }}
+  ],
+  { rows => 3, join => { cds => [qw/artwork tracks/] },
+ }
+);
+
+my $use_prefetch = $no_prefetch->search(
+  {},
+  {
+    prefetch => 'cds',
+    order_by => { -desc => 'name' },
+  }
+);
+
+is($no_prefetch->count, $use_prefetch->count, '$no_prefetch->count == $use_prefetch->count');
+is(
+  scalar ($no_prefetch->all),
+  scalar ($use_prefetch->all),
+  "Amount of returned rows is right"
+);
+
+my $artist_many_cds = $schema->resultset('Artist')->search ( {}, {
+  join => 'cds',
+  group_by => 'me.artistid',
+  having => \ 'count(cds.cdid) > 1',
+})->first;
+
+
+$no_prefetch = $schema->resultset('Artist')->search(
+  { artistid => $artist_many_cds->id },
+  { rows => 1 }
+);
+
+$use_prefetch = $no_prefetch->search ({}, { prefetch => 'cds' });
+
+my $normal_artist = $no_prefetch->single;
+my $prefetch_artist = $use_prefetch->find({ name => $artist_many_cds->name });
+my $prefetch2_artist = $use_prefetch->first;
+
+is(
+  $prefetch_artist->cds->count,
+  $normal_artist->cds->count,
+  "Count of child rel with prefetch + rows => 1 is right (find)"
+);
+is(
+  $prefetch2_artist->cds->count,
+  $normal_artist->cds->count,
+  "Count of child rel with prefetch + rows => 1 is right (first)"
+);
+
+is (
+  scalar ($prefetch_artist->cds->all),
+  scalar ($normal_artist->cds->all),
+  "Amount of child rel rows with prefetch + rows => 1 is right (find)"
+);
+is (
+  scalar ($prefetch2_artist->cds->all),
+  scalar ($normal_artist->cds->all),
+  "Amount of child rel rows with prefetch + rows => 1 is right (first)"
+);
+
+throws_ok (
+  sub { $use_prefetch->single },
+  qr/resultsets prefetching has_many/,
+  'single() with multiprefetch is illegal',
+);
+
+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");
+
+# try with double limit
+my $artist2 = $use_prefetch->search({'cds.title' => { '!=' => $artist_many_cds->cds->first->title } })->slice (0,0)->next;
+is($artist2->cds->count, 2, "count on search limiting prefetched has_many");
+
index 65093c4..566e5f3 100644 (file)
@@ -7,8 +7,9 @@ use lib qw(t/lib);
 use DBICTest;
 
 my $schema = DBICTest->init_schema();
+my $sdebug = $schema->storage->debug;
 
-plan tests => 78;
+plan tests => 79;
 
 # has_a test
 my $cd = $schema->resultset("CD")->find(4);
@@ -40,8 +41,8 @@ if ($INC{'DBICTest/HelperRels.pm'}) {
       year => 2005,
   } );
 
- SKIP:{
-    skip "Can't fix right now", 1 if $DBIx::Class::VERSION < 0.09;
+ TODO: {
+    local $TODO = "Can't fix right now" if $DBIx::Class::VERSION < 0.09;
     lives_ok { $big_flop->genre} "Don't throw exception when col is not loaded after insert";
   };
 }
@@ -57,7 +58,7 @@ is( $big_flop_cd->title, 'Big Flop', 'create_related ok' );
   is($queries, 0, 'No SELECT made for belongs_to if key IS NULL');
   $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(0);
+  $schema->storage->debug($sdebug);
   $schema->storage->debugcb(undef);
 }
 
@@ -275,11 +276,11 @@ my $searched = $mapped_rs->search({'mapped_artists.artistid' => {'!=', undef}});
 
 cmp_ok($searched->count, '==', 2, "Both artist returned from map after adding another condition");
 
-# check join through cascaded has_many relationships
+# check join through cascaded has_many relationships (also empty has_many rels)
 $artist = $schema->resultset("Artist")->find(1);
 my $trackset = $artist->cds->search_related('tracks');
-# LEFT join means we also see the trackless additional album...
-cmp_ok($trackset->count, '==', 11, "Correct number of tracks for artist");
+is($trackset->count, 10, "Correct number of tracks for artist");
+is($trackset->all, 10, "Correct number of track objects for artist");
 
 # now see about updating eveything that belongs to artist 2 to artist 3
 $artist = $schema->resultset("Artist")->find(2);
diff --git a/t/relationship/update_or_create_multi.t b/t/relationship/update_or_create_multi.t
new file mode 100644 (file)
index 0000000..885b15c
--- /dev/null
@@ -0,0 +1,89 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+use lib qw(t/lib);
+use DBICTest;
+use DBIC::SqlMakerTest;
+
+my $schema = DBICTest->init_schema();
+my $sdebug = $schema->storage->debug;
+
+plan tests => 6;
+
+my $artist = $schema->resultset ('Artist')->first;
+
+my $genre = $schema->resultset ('Genre')
+            ->create ({ name => 'par excellence' });
+
+is ($genre->search_related( 'cds' )->count, 0, 'No cds yet');
+
+# expect a create
+$genre->update_or_create_related ('cds', {
+  artist => $artist,
+  year => 2009,
+  title => 'the best thing since sliced bread',
+});
+
+# verify cd was inserted ok
+is ($genre->search_related( 'cds' )->count, 1, 'One cd');
+my $cd = $genre->find_related ('cds', {});
+is_deeply (
+  { map { $_, $cd->get_column ($_) } qw/artist year title/ },
+  {
+    artist => $artist->id,
+    year => 2009,
+    title => 'the best thing since sliced bread',
+  },
+  'CD created correctly',
+);
+
+# expect a year update on the only related row
+# (non-qunique column + unique column as disambiguator)
+$genre->update_or_create_related ('cds', {
+  year => 2010,
+  title => 'the best thing since sliced bread',
+});
+
+# re-fetch the cd, verify update
+is ($genre->search_related( 'cds' )->count, 1, 'Still one cd');
+$cd = $genre->find_related ('cds', {});
+is_deeply (
+  { map { $_, $cd->get_column ($_) } qw/artist year title/ },
+  {
+    artist => $artist->id,
+    year => 2010,
+    title => 'the best thing since sliced bread',
+  },
+  'CD year column updated correctly',
+);
+
+
+# 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);
+
+is_same_sql (
+  $sql[0],
+  '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',
+);
+
+# a has_many search without a unique constraint makes no sense
+# but I am not sure what to test for - leaving open
diff --git a/t/relationship/update_or_create_single.t b/t/relationship/update_or_create_single.t
new file mode 100644 (file)
index 0000000..1675525
--- /dev/null
@@ -0,0 +1,99 @@
+use strict;
+use warnings;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest;
+
+my $schema = DBICTest->init_schema();
+
+plan tests => 9;
+
+my $artist = $schema->resultset ('Artist')->first;
+
+my $genre = $schema->resultset ('Genre')
+            ->create ({ name => 'par excellence' });
+
+is ($genre->search_related( 'model_cd' )->count, 0, 'No cds yet');
+
+# expect a create
+$genre->update_or_create_related ('model_cd', {
+  artist => $artist,
+  year => 2009,
+  title => 'the best thing since sliced bread',
+});
+
+# verify cd was inserted ok
+is ($genre->search_related( 'model_cd' )->count, 1, 'One cd');
+my $cd = $genre->find_related ('model_cd', {});
+is_deeply (
+  { map { $_, $cd->get_column ($_) } qw/artist year title/ },
+  {
+    artist => $artist->id,
+    year => 2009,
+    title => 'the best thing since sliced bread',
+  },
+  'CD created correctly',
+);
+
+# expect a year update on the only related row
+# (non-qunique column + unique column as disambiguator)
+$genre->update_or_create_related ('model_cd', {
+  year => 2010,
+  title => 'the best thing since sliced bread',
+});
+
+# re-fetch the cd, verify update
+is ($genre->search_related( 'model_cd' )->count, 1, 'Still one cd');
+$cd = $genre->find_related ('model_cd', {});
+is_deeply (
+  { map { $_, $cd->get_column ($_) } qw/artist year title/ },
+  {
+    artist => $artist->id,
+    year => 2010,
+    title => 'the best thing since sliced bread',
+  },
+  'CD year column updated correctly',
+);
+
+
+# expect an update of the only related row
+# (update a unique column)
+$genre->update_or_create_related ('model_cd', {
+  title => 'the best thing since vertical toasters',
+});
+
+# re-fetch the cd, verify update
+is ($genre->search_related( 'model_cd' )->count, 1, 'Still one cd');
+$cd = $genre->find_related ('model_cd', {});
+is_deeply (
+  { map { $_, $cd->get_column ($_) } qw/artist year title/ },
+  {
+    artist => $artist->id,
+    year => 2010,
+    title => 'the best thing since vertical toasters',
+  },
+  'CD title column updated correctly',
+);
+
+
+# expect a year update on the only related row
+# (non-qunique column only)
+$genre->update_or_create_related ('model_cd', {
+  year => 2011,
+});
+
+# re-fetch the cd, verify update
+is ($genre->search_related( 'model_cd' )->count, 1, 'Still one cd');
+$cd = $genre->find_related ('model_cd', {});
+is_deeply (
+  { map { $_, $cd->get_column ($_) } qw/artist year title/ },
+  {
+    artist => $artist->id,
+    year => 2011,
+    title => 'the best thing since vertical toasters',
+  },
+  'CD year column updated correctly without a disambiguator',
+);
+
+
index 3ccd4a7..fd86646 100644 (file)
@@ -68,7 +68,7 @@ ok( ( $ratio < 2 ), 'Overload/bless performance acceptable' )
     "in the Troubleshooting POD documentation entitled\n",
     "'Perl Performance Issues on Red Hat Systems'\n",
     "As this is an extremely serious condition, the only way to skip\n",
-    "over this test is to --force the installation, or to edit the test\n",
+    "over this test is to --force the installation, or to look in the test\n",
     "file " . __FILE__ . "\n",
   );
 
@@ -115,7 +115,7 @@ SKIP: {
         "Please read the section in the Troubleshooting POD documentation\n",
         "entitled 'Perl Performance Issues on Red Hat Systems'\n",
         "As this is an extremely serious condition, the only way to skip\n",
-        "over this test is to --force the installation, or to edit the test\n",
+        "over this test is to --force the installation, or to look in the test\n",
         "file " . __FILE__ . "\n",
       );
 }
diff --git a/t/zzzzzzz_sqlite_deadlock.t b/t/zzzzzzz_sqlite_deadlock.t
new file mode 100644 (file)
index 0000000..e48baa2
--- /dev/null
@@ -0,0 +1,41 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Test::Exception;
+use lib 't/lib';
+
+use File::Temp ();
+use DBICTest;
+use DBICTest::Schema;
+
+plan tests => 2;
+my $wait_for = 10;  # how many seconds to wait
+
+for my $close (0,1) {
+
+  my $tmp = File::Temp->new(
+    UNLINK => 1,
+    TMPDIR => 1,
+    SUFFIX => '.sqlite',
+    EXLOCK => 0,  # important for BSD and derivatives
+  );
+
+  my $tmp_fn = $tmp->filename;
+  close $tmp if $close;
+
+  local $SIG{ALRM} = sub { die sprintf (
+    "Timeout of %d seconds reached (tempfile still open: %s)",
+    $wait_for, $close ? 'No' : 'Yes'
+  )};
+
+  alarm $wait_for;
+
+  lives_ok (sub {
+    my $schema = DBICTest::Schema->connect ("DBI:SQLite:$tmp_fn");
+    DBICTest->deploy_schema ($schema);
+    #DBICTest->populate_schema ($schema);
+  });
+
+  alarm 0;
+}