[Jifty-commit] r4662 - in Jifty-DBI/branches/index-relationship: . debian lib/Jifty lib/Jifty/DBI/Class lib/Jifty/DBI/Filter lib/Jifty/DBI/Handle lib/Jifty/DBI/Record t

jifty-commit at lists.jifty.org jifty-commit at lists.jifty.org
Mon Dec 10 17:40:43 EST 2007


Author: sterling
Date: Mon Dec 10 17:40:42 2007
New Revision: 4662

Added:
   Jifty-DBI/branches/index-relationship/t/18triggers.t
   Jifty-DBI/branches/index-relationship/t/19reference.t
   Jifty-DBI/branches/index-relationship/t/case_sensitivity.t
   Jifty-DBI/branches/index-relationship/t/metadata.t
Removed:
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Class/
Modified:
   Jifty-DBI/branches/index-relationship/   (props changed)
   Jifty-DBI/branches/index-relationship/Changes
   Jifty-DBI/branches/index-relationship/MANIFEST
   Jifty-DBI/branches/index-relationship/MANIFEST.SKIP
   Jifty-DBI/branches/index-relationship/META.yml
   Jifty-DBI/branches/index-relationship/Makefile.PL
   Jifty-DBI/branches/index-relationship/README
   Jifty-DBI/branches/index-relationship/SIGNATURE
   Jifty-DBI/branches/index-relationship/debian/changelog
   Jifty-DBI/branches/index-relationship/debian/control
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Collection.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Column.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter/DateTime.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter/YAML.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Informix.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/ODBC.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Oracle.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Pg.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Sybase.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Record.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Record/Plugin.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Schema.pm
   Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/SchemaGenerator.pm
   Jifty-DBI/branches/index-relationship/t/01records.t
   Jifty-DBI/branches/index-relationship/t/01searches.t
   Jifty-DBI/branches/index-relationship/t/02records_cachable.t
   Jifty-DBI/branches/index-relationship/t/02records_object.t
   Jifty-DBI/branches/index-relationship/t/02searches_joins.t
   Jifty-DBI/branches/index-relationship/t/04memcached.t
   Jifty-DBI/branches/index-relationship/t/10schema.t
   Jifty-DBI/branches/index-relationship/t/11schema_records.t
   Jifty-DBI/branches/index-relationship/t/12prefetch.t
   Jifty-DBI/branches/index-relationship/t/14handle-pg.t
   Jifty-DBI/branches/index-relationship/t/17virtualtypes.t
   Jifty-DBI/branches/index-relationship/t/testmodels.pl
   Jifty-DBI/branches/index-relationship/t/utils.pl

Log:
 r14468 at dynpc145:  andrew | 2007-12-10 16:39:39 -0600
 Merge down from JDBI trunk.
  r8694 at dynpc145:  andrew | 2007-08-16 19:30:47 -0500
   r8693 at dynpc145 (orig r3911):  trs | 2007-08-16 14:59:16 -0500
    r26169 at zot:  tom | 2007-08-16 15:58:56 -0400
    Backup r3908 for now since it breaks existing code
   
  
  r8874 at dynpc145:  andrew | 2007-08-22 12:03:07 -0500
  Updated documentation to discuss the register_triggers_late() method.
  r8912 at dynpc145:  andrew | 2007-08-24 07:29:45 -0500
  Removed register_triggers_late() in favor of register_triggers_for_column(), which is nearly the same thing but with clearer semantics.
  r8928 at dynpc145:  andrew | 2007-08-24 07:30:16 -0500
   r8889 at dynpc145 (orig r3923):  efunneko | 2007-08-20 15:57:43 -0500
   Added support for the IN operator within limits.  This should be accompanied by an array ref of values to be compared against.  It also allows an array ref to be passed with the '=' operator to accomplish the same thing
   r8892 at dynpc145 (orig r3926):  yves | 2007-08-21 09:49:01 -0500
   debian packaging
   
   r8893 at dynpc145 (orig r3927):  falcone | 2007-08-21 18:08:59 -0500
    r23978 at ketch:  falcone | 2007-08-21 18:00:27 -0400
    * we don't need to TODO this MySQL test because the case matches exactly
      and returns values
   
   r8894 at dynpc145 (orig r3928):  falcone | 2007-08-21 18:09:22 -0500
    r23979 at ketch:  falcone | 2007-08-21 18:00:57 -0400
    * tell Pg to be case sensitive on IN clauses and handle the arrayref
      structure used by IN
   
   r8905 at dynpc145 (orig r3939):  audreyt | 2007-08-22 11:38:23 -0500
   * Jifty::DBI::Filter::DateTime: The _time_zone setting was not
     honoured by ->decode, because the code had a no-op $dt->time_zone($tz)
     where it should've been $dt->set_time_zone($tz).
   r8920 at dynpc145 (orig r3952):  jesse | 2007-08-23 23:17:15 -0500
    r66551 at pinglin:  jesse | 2007-08-24 00:16:06 -0400
     * 0.43
   
   r8921 at dynpc145 (orig r3953):  jesse | 2007-08-23 23:17:32 -0500
    r66552 at pinglin:  jesse | 2007-08-24 00:16:30 -0400
     * signature file
   
  
  r8931 at dynpc145:  andrew | 2007-08-24 07:31:12 -0500
  Fixed a significant typo that made it into the code of the previous commit.
  r8933 at dynpc145:  andrew | 2007-08-24 10:45:16 -0500
  Fix the documentation of the after_create trigger to match the actual behavior.
  r8934 at dynpc145:  andrew | 2007-08-24 10:47:06 -0500
  Reverting previous mistaken commit.
  r8935 at dynpc145:  andrew | 2007-08-24 10:48:06 -0500
  Fix the documentation of the after_create trigger to match the actual behavior (and only that, this time).
  r8936 at dynpc145:  andrew | 2007-08-24 11:41:29 -0500
  Improvements to the trigger documentation for after_create and before_create.
  r8937 at dynpc145:  andrew | 2007-08-24 11:42:00 -0500
  Added tests and debugged register_triggers_for_column on record plugins.
  r8943 at dynpc145:  andrew | 2007-08-24 11:50:51 -0500
  Moved Jifty::DBI to use Class::Trigger instead of its own fork of Class::Trigger.
  r8945 at dynpc145:  andrew | 2007-08-24 12:01:51 -0500
  Adding before_set and after_set triggers and tests for them.
  r8947 at dynpc145:  andrew | 2007-08-24 12:04:58 -0500
  Adding basic documentation for before_set and after_set triggers.
  r8949 at dynpc145:  andrew | 2007-08-24 13:08:08 -0500
  Added a new accessor, RECORD_MIXINS, for tracking which mixins have been attached to a model.
  r8960 at dynpc145:  andrew | 2007-08-24 21:17:17 -0500
  Fix the call to the after_set trigger.
  r11131 at dynpc145:  andrew | 2007-09-02 21:32:04 -0500
  Added documentation for using @EXPORT with mixins.
  r11141 at dynpc145:  andrew | 2007-09-03 09:53:50 -0500
   r11140 at dynpc145 (orig r4036):  clkao | 2007-09-03 06:29:09 -0500
   * Allow is_distinct when joining collections, hinting the
     resulting collection is still distinct.
   
   * If table2 is given as a Jifty::DBI::Collection object,
     detect if the join is still distinct.
   
  
  r11142 at dynpc145:  andrew | 2007-09-03 09:55:54 -0500
  Make sure limit() does not modify arrays passed in to the "value" parameter.
  r12801 at dynpc145:  andrew | 2007-10-02 12:59:40 -0500
   r11816 at dynpc145 (orig r4046):  jesse | 2007-09-06 13:30:44 -0500
    r67015 at pinglin:  jesse | 2007-09-06 14:29:24 -0400
    * Fixing case errors in tests
    
   
   r11817 at dynpc145 (orig r4047):  jesse | 2007-09-06 13:30:59 -0500
    r67016 at pinglin:  jesse | 2007-09-06 14:30:00 -0400
    * Add support for non-lowercase column names
   
   r12169 at dynpc145 (orig r4121):  jesse | 2007-09-17 11:32:34 -0500
    r67326 at pinglin:  jesse | 2007-09-17 12:28:47 -0400
    *Test updates for case issues 
   
   r12257 at dynpc145 (orig r4142):  falcone | 2007-09-20 09:29:11 -0500
    r24849 at ketch:  falcone | 2007-09-20 10:27:58 -0400
    * remove debugging from test
   
   r12570 at dynpc145 (orig r4145):  clkao | 2007-09-24 16:57:41 -0500
   Refactor Jifty::DBI::Column to split attributes into two groups.
   Actual column-related (in jifty::dbi layer) ones, such as type,
   remains as accesssors.  The rest goes into the attributes hash.
   This allows us to declare higher level meta data without having to
   change Jifty::DBI::Column in the future.
   
   Note that if you are accessing the column object's ->{label} directly
   this will now fail.  Use ->label instead.
   
   r12571 at dynpc145 (orig r4146):  clkao | 2007-09-24 17:35:17 -0500
   unreadable and immutable are now in attributes.
   
   r12572 at dynpc145 (orig r4147):  clkao | 2007-09-24 17:46:34 -0500
   * New column attribute: is case_sensitive
   * Make load_by_cols respect column case sensitivity.
   
     NOTE this is a behaviour change as columns are by default
     case-insensitive, and the call was doing a case sensitive
     search, which is different from Collection searching.
   
     To make Jifty::DBI load_by_cols work in the old behaviour:
   
     Use $rec->load_by_cols( name => { value => 'foobar',
                                       case_sensitive => 0,
                                       operator => '=' });
   
   r12573 at dynpc145 (orig r4148):  clkao | 2007-09-24 18:11:37 -0500
   Bump version as we have new features.
   r12681 at dynpc145 (orig r4163):  alexmv | 2007-10-01 13:00:21 -0500
    r22182 at zoq-fot-pik:  chmrr | 2007-10-01 13:59:23 -0400
     * We can have 0-valued columns!
   
   r12682 at dynpc145 (orig r4164):  alexmv | 2007-10-01 13:09:43 -0500
    r22184 at zoq-fot-pik:  chmrr | 2007-10-01 14:09:15 -0400
     * We can have 0-valued columns, again
   
   r12684 at dynpc145 (orig r4166):  falcone | 2007-10-01 15:47:28 -0500
    r25093 at ketch:  falcone | 2007-10-01 16:46:18 -0400
    * typo fix in docs
   
   r12685 at dynpc145 (orig r4167):  falcone | 2007-10-01 15:47:35 -0500
    r25094 at ketch:  falcone | 2007-10-01 16:46:57 -0400
    * mysql doesn't handle bare varchar's
    * TODO a test that needs us to force mysql to be case sensitive
   
   r12687 at dynpc145 (orig r4169):  jesse | 2007-10-01 22:57:13 -0500
    r27290 at hualien:  jesse | 2007-10-01 23:55:46 -0400
    * When we fail to load a related model class for a good reason, actually die, rather than burying the error
   
  
  r13790 at dynpc145:  andrew | 2007-10-31 11:46:41 -0500
   r13043 at riddle (orig r4213):  jesse | 2007-10-07 17:28:20 -0500
    r67965 at 000-166-898:  jesse | 2007-10-07 18:06:53 -0400
    Allow the specification of functions as 'column' on sql search criteria.
   
   r13229 at riddle (orig r4223):  sunnavy | 2007-10-10 02:45:02 -0500
   clean warnings
   r13236 at riddle (orig r4230):  jesse | 2007-10-10 21:56:30 -0500
    r68046 at pinglin:  jesse | 2007-10-10 21:48:07 -0500
     defined is not what you check to see if there is a hash entry. this will stop bogus extra sql calls that slowed down your app
   
   r13335 at riddle (orig r4244):  sartak | 2007-10-16 16:15:59 -0500
    r43744 at onn:  sartak | 2007-10-16 17:15:17 -0400
    Two fixes for timezones:
      * Coerce input and output to be UTC (or Floating, if JDBI::Filter::Date)
            I think we were depending on DT:F:Strptime being saner than it is
      * Use set_time_zone, not time_zone which is only a getter
   
   r13443 at riddle (orig r4265):  sartak | 2007-10-19 16:11:35 -0500
    r43892 at onn:  sartak | 2007-10-19 17:11:28 -0400
    Better error message when trying to use load_by_cols on a column that doesn't exist
   
   r13448 at riddle (orig r4270):  sartak | 2007-10-19 22:58:42 -0500
    r43911 at onn:  sartak | 2007-10-19 23:58:36 -0400
    Comment out a test diag
   
   r13449 at riddle (orig r4271):  sartak | 2007-10-19 23:03:00 -0500
    r43913 at onn:  sartak | 2007-10-20 00:02:49 -0400
    More needless warnings gone
   
   r13614 at riddle (orig r4282):  sartak | 2007-10-20 21:36:33 -0500
    r43944 at onn:  sartak | 2007-10-20 22:36:24 -0400
    Improve the decode doc, which was mostly just a copy of encode
   
   r13648 at riddle (orig r4305):  sartak | 2007-10-24 13:06:55 -0500
    r44123 at onn:  sartak | 2007-10-24 14:06:44 -0400
    Bump to 0.45 because of the JDBI::Filter::DateTime functionality fixes
   
   r13652 at riddle (orig r4309):  sartak | 2007-10-25 11:22:59 -0500
    r44174 at onn:  sartak | 2007-10-25 12:22:28 -0400
    Add failing tests for mandatory columns that aren't active, they shouldn't have the not null constraint
   
   r13653 at riddle (orig r4310):  sartak | 2007-10-25 11:48:38 -0500
    r44176 at onn:  sartak | 2007-10-25 12:48:22 -0400
    We don't need to check that a column is mandatory if it's not active
   
   r13655 at riddle (orig r4312):  sartak | 2007-10-25 13:30:11 -0500
    r44180 at onn:  sartak | 2007-10-25 14:29:57 -0400
    Fix the mandatory/inactive tests
   
   r13656 at riddle (orig r4313):  sartak | 2007-10-25 13:37:25 -0500
    r44182 at onn:  sartak | 2007-10-25 14:37:13 -0400
    Add a warning for mandatory+till until we fix it
   
   r13660 at riddle (orig r4314):  jesse | 2007-10-26 11:50:38 -0500
    r68509 at pinglin:  jesse | 2007-10-26 12:39:46 -0400
     removing a duplicate var name
   
   r13661 at riddle (orig r4315):  jesse | 2007-10-26 11:50:48 -0500
    r68510 at pinglin:  jesse | 2007-10-26 12:47:33 -0400
    * 0.45
    
   
  
  r14131 at dynpc145:  andrew | 2007-11-19 16:26:45 -0600
   r13987 at riddle (orig r4375):  alexmv | 2007-11-05 17:46:47 -0600
    r24385 at zoq-fot-pik:  chmrr | 2007-11-05 18:46:17 -0500
     * If we left join'd on the preload, skip the NULLs
   
   r13999 at riddle (orig r4387):  jesse | 2007-11-07 11:25:37 -0600
    r71476 at pinglin:  jesse | 2007-11-07 12:23:53 -0500
    * better pod coverage of deprecated methods
   
   r14000 at riddle (orig r4388):  jesse | 2007-11-07 11:25:44 -0600
    r71477 at pinglin:  jesse | 2007-11-07 12:24:25 -0500
     * stray tempfile leakage fixed. reported by andk
   
   r14001 at riddle (orig r4389):  jesse | 2007-11-07 11:25:52 -0600
    r71478 at pinglin:  jesse | 2007-11-07 12:24:52 -0500
    0.46
   
   r14002 at riddle (orig r4390):  jesse | 2007-11-07 11:25:58 -0600
    r71479 at pinglin:  jesse | 2007-11-07 12:25:13 -0500
    0.46 releng
   
   r14037 at riddle (orig r4425):  alexmv | 2007-11-13 12:32:26 -0600
    r24692 at zoq-fot-pik:  chmrr | 2007-11-13 13:31:57 -0500
     * perltidy
     * '$sb' -> '$collection' because we aren't SearchBuilder anymore
     * Remove Jifty::DBI::Collection->preload_columns, which was old and
       unused
     * add derived => 1 for prefetched collections
     * derived collections can't be relimited
     * Don't grovel theough columns to find possible joins, just look at
       the joins
     * Fix preload for records
     * Standardize on "prefetch" not "preload"
     * $collection->{leftjoins} becomes ->{joins}, to not lie
     * $collection->{aliases} rolled into ->{joins}, to also not lie
     * $collection->_preload_columns becomes ->query_columns, to have a
       better name
     * $collection->prefetch is now smarter, stealing code from future
       tisql branch
     * Remove broken ->_normal_join
     * (re-)wrapped POD
     * Factor out ->_new_record_args and ->_new_collection_args on both
       Record and Collection; makes for easier subclassing in Jifty
   
   r14038 at riddle (orig r4426):  alexmv | 2007-11-13 12:33:39 -0600
    r24694 at zoq-fot-pik:  chmrr | 2007-11-13 13:33:18 -0500
     * Version bump, for the previous vaguely non-backcompat changes
   
   r14093 at riddle (orig r4434):  clkao | 2007-11-15 00:06:19 -0600
   some tests for referencing.
   r14095 at riddle (orig r4436):  alexmv | 2007-11-15 11:44:22 -0600
    r24778 at zoq-fot-pik:  chmrr | 2007-11-15 12:43:12 -0500
     * Alias columns now count as virtual
     * Force chained auto-joins to be marked distinct
     * Can now call ->record_class as a class method
     * When looking up the column object in a limit, try to find the right
       class in the aliases.  This only currently works if you specified
       the model class as the 'table2' in the join, instead of its table
       name.
     * Only create the parser and formatter for DateTime objects once, as
       they are slow to create.
     * r4425 actually changed the way that undef foreign objects were
       treated; revert to the old behavior, but make it easier to achieve
       the new (aka Jifty's) behavior.
   
   r14097 at riddle (orig r4438):  alexmv | 2007-11-15 14:17:45 -0600
    r24783 at zoq-fot-pik:  chmrr | 2007-11-15 15:16:29 -0500
     * The formatter can't be decided at compile time, because subclasses
       may override the format.  Hence, make it once and cache it.
     * Note for posterity why we can't apply acl checks to refers_to columns
   
   r14105 at riddle (orig r4446):  sartak | 2007-11-15 14:49:51 -0600
    r45200 at onn:  sartak | 2007-11-15 15:49:13 -0500
    Get rid of a spurious warning
   
   r14106 at riddle (orig r4447):  sartak | 2007-11-15 14:50:28 -0600
    r45202 at onn:  sartak | 2007-11-15 15:49:56 -0500
    Whoops, turned off the wrong warning
   
   r14107 at riddle (orig r4448):  sartak | 2007-11-15 14:55:10 -0600
    r45204 at onn:  sartak | 2007-11-15 15:54:39 -0500
    POD coverage
   
   r14115 at riddle (orig r4456):  sartak | 2007-11-16 14:28:33 -0600
    r45270 at onn:  sartak | 2007-11-16 15:27:56 -0500
    Update Changes
   
   r14116 at riddle (orig r4457):  sartak | 2007-11-16 15:02:10 -0600
    r45272 at onn:  sartak | 2007-11-16 16:01:37 -0500
    manifest skip pm_to_blib, add new test file to manifest
   
  
  r14248 at dynpc145:  andrew | 2007-11-27 20:17:01 -0600
   r14137 at dynpc145 (orig r4471):  ishigaki | 2007-11-20 01:41:56 -0600
   JDBI: added 'escape' option (undef by default) to Collection->limit, to allow "where column like 'something including wildcard(\\%)' ESCAPE '\\'" kind of stuff, mainly for SQLite which doesn't seem to be able to escape wildcards without ESCAPE
   r14179 at dynpc145 (orig r4507):  ishigaki | 2007-11-20 14:22:27 -0600
   JDBI: changed escape character in the t/01searches.t from backslashes to at mark, which is cleaner and kind to postgres; added pod
   r14193 at dynpc145 (orig r4516):  falcone | 2007-11-21 11:09:15 -0600
    r25093 at ketch:  falcone | 2007-10-01 16:46:18 -0400
    * typo fix in docs
   
   r14194 at dynpc145 (orig r4517):  falcone | 2007-11-21 11:09:22 -0600
    r25094 at ketch:  falcone | 2007-10-01 16:46:57 -0400
    * mysql doesn't handle bare varchar's
    * TODO a test that needs us to force mysql to be case sensitive
   
   r14195 at dynpc145 (orig r4518):  falcone | 2007-11-21 11:10:12 -0600
    r26805 at ketch:  falcone | 2007-11-21 12:08:36 -0500
    * expand the escape documentation
   
   r14215 at dynpc145 (orig r4533):  ishigaki | 2007-11-24 15:25:51 -0600
   JDBI: use YAML () -> require YAML to suppress error when YAML is not installed (but ::Syck is installed)
   r14228 at dynpc145 (orig r4542):  alexmv | 2007-11-26 13:12:54 -0600
    r25123 at zoq-fot-pik:  chmrr | 2007-11-26 14:10:54 -0500
     * Fix reverse joins.  Broken in multiple ways.
   
  
  r14282 at dynpc145:  andrew | 2007-11-29 14:40:41 -0600
  Setting a default for RECORD_MIXINS and altering the mk_classdata() decls to avoid any confusion with the mk_accessors() interface.
  r14283 at dynpc145:  andrew | 2007-11-29 14:41:08 -0600
   r14271 at dynpc145 (orig r4561):  jasonmay | 2007-11-28 18:42:49 -0600
   Exclude user and passowrd from the DSN so it doesn't show up in the terminal
   
   r14276 at dynpc145 (orig r4566):  sartak | 2007-11-29 11:00:27 -0600
    r45752 at onn:  sartak | 2007-11-29 11:59:58 -0500
    Let users run arbitrary code during SQL statement logging
   
   r14279 at dynpc145 (orig r4569):  jasonmay | 2007-11-29 12:55:16 -0600
   User and password values are excluded from the DSN in a more proper fashion
   
  
  r14284 at dynpc145:  andrew | 2007-11-29 14:42:31 -0600
  Eliminated the ?: at the end of import() since RECORD_MIXINS is now [] by default.
  r14467 at dynpc145:  andrew | 2007-12-10 16:30:14 -0600
   r14289 at dynpc145 (orig r4575):  sartak | 2007-11-29 15:35:39 -0600
    r48428 at onn:  sartak | 2007-11-29 16:35:30 -0500
    Release 0.48
   
   r14313 at dynpc145 (orig r4596):  sartak | 2007-11-30 18:09:16 -0600
    r48466 at onn:  sartak | 2007-11-30 19:09:10 -0500
    Improve the JDBI::Record->column doc
   
   r14446 at dynpc145 (orig r4641):  jesse | 2007-12-07 15:17:58 -0600
    r72497 at pinglin:  jesse | 2007-11-30 00:16:53 -0500
    * Be a bit cleverer about not always doing a count before doing a select
      (The previous behaviour was pathalogical)
   
   r14447 at dynpc145 (orig r4642):  jesse | 2007-12-07 15:18:10 -0600
   
   r14449 at dynpc145 (orig r4644):  jesse | 2007-12-07 15:24:42 -0600
    r72795 at pinglin:  jesse | 2007-12-07 16:17:22 -0500
    * By not reaching inside the pager object, we save on some useless forced SQL queries
   
   r14450 at dynpc145 (orig r4645):  jesse | 2007-12-07 15:35:35 -0600
    r72803 at pinglin:  jesse | 2007-12-07 16:33:44 -0500
     * Fix a mismerge in 4642
   
   r14451 at dynpc145 (orig r4646):  jesse | 2007-12-07 15:35:40 -0600
    r72804 at pinglin:  jesse | 2007-12-07 16:34:13 -0500
    * Be more careful about initializing values we're using
   
  
 


Modified: Jifty-DBI/branches/index-relationship/Changes
==============================================================================
--- Jifty-DBI/branches/index-relationship/Changes	(original)
+++ Jifty-DBI/branches/index-relationship/Changes	Mon Dec 10 17:40:42 2007
@@ -1,6 +1,277 @@
 Revision history for Perl extension Jifty::DBI.
 
- * Updated for deprecated API in DBIx::DBSchema 0.33
+0.48 Thu Nov 29 16:28:11 EST 2007
+
+   * User and password values are excluded from the DSN in a more proper fashion
+   * Setting a default for RECORD_MIXINS and altering the mk_classdata() decls
+     to avoid any confusion with the mk_accessors() interface.
+   * Eliminated the ?: at the end of import() since RECORD_MIXINS is now [] by
+     default.
+   * Let users run arbitrary code during SQL statement logging
+   * Exclude user and passowrd from the DSN so it doesn't show up in the
+     terminal
+   * use YAML () -> require YAML to suppress error when YAML is not installed
+     (but ::Syck is installed)
+   * Fix reverse joins.  Broken in multiple ways.
+   * added 'escape' option (undef by default) to Collection->limit, to allow "
+   * where column like 'something including wildcard(\\%)' ESCAPE '\\'" kind of
+     stuff, mainly for SQLite which doesn't seem to be able to escape wildcards
+     without ESCAPE
+   * changed escape character in the t/01searches.t from backslashes to at mar
+   * k, which is cleaner and kind to postgres; added pod
+   * typo fix in docs
+   * mysql doesn't handle bare varchar's
+   * TODO a test that needs us to force mysql to be case sensitive
+   * expand the escape documentation
+   * manifest skip pm_to_blib, add new test file to manifest
+   * Update Changes
+
+0.47 Fri Nov 16 15:23:28 EST 2007
+
+   or, "The chmrr fixfest"
+
+   * Note for posterity why we can't apply acl checks to refers_to columns
+   * The formatter can't be decided at compile time, because subclasses
+     may override the format.  Hence, make it once and cache it.
+   * r4425 actually changed the way that undef foreign objects were
+     treated; revert to the old behavior, but make it easier to achieve
+     the new (aka Jifty's) behavior.
+   * Only create the parser and formatter for DateTime objects once, as
+     they are slow to create.
+   * When looking up the column object in a limit, try to find the right
+     class in the aliases.  This only currently works if you specified
+     the model class as the 'table2' in the join, instead of its table
+     name.
+   * Can now call ->record_class as a class method
+   * Force chained auto-joins to be marked distinct
+   * Alias columns now count as virtual
+   * some tests for referencing.
+   * Factor out ->_new_record_args and ->_new_collection_args on both
+     Record and Collection; makes for easier subclassing in Jifty
+   * (re-)wrapped POD
+   * Remove broken ->_normal_join
+   * $collection->prefetch is now smarter, stealing code from future
+     tisql branch
+   * $collection->_preload_columns becomes ->query_columns, to have a
+     better name
+   * $collection->{aliases} rolled into ->{joins}, to also not lie
+   * $collection->{leftjoins} becomes ->{joins}, to not lie
+   * Standardize on "prefetch" not "preload"
+   * Fix preload for records
+   * Don't grovel theough columns to find possible joins, just look at
+     the joins
+   * derived collections can't be relimited
+   * add derived => 1 for prefetched collections
+   * Remove Jifty::DBI::Collection->preload_columns, which was old and
+     unused 
+   * '$sb' -> '$collection' because we aren't SearchBuilder anymore
+   * perltidy
+   * Get rid of a spurious warning
+   * Whoops, turned off the wrong warning
+   * POD coverage
+
+0.46 Wed Nov  7 12:24:08 EST 2007
+
+ - POD Coverage nit
+ - Fix for a stray tempfiles bug reported by ANDK
+
+0.45
+
+Add a warning for mandatory+till until we fix it
+
+Fix the mandatory/inactive tests
+
+We don't need to check that a column is mandatory if it's not active
+
+Add failing tests for mandatory columns that aren't active, they shouldn't have the not null constraint
+
+Bump to 0.45 because of the JDBI::Filter::DateTime functionality fixes
+
+Improve the decode doc, which was mostly just a copy of encode
+More needless warnings gone
+
+Better error message when trying to use load_by_cols on a column that doesn't exist
+Two fixes for timezones:
+
+    Coerce input and output to be UTC (or Floating, if JDBI::Filter::Date)
+    I think we were depending on DT:F:Strptime being saner than it is
+
+    Use set_time_zone, not time_zone which is only a getter
+    defined is not what you check to see if there is a hash entry. this will stop bogus extra sql calls that slowed down your app
+
+Allow the specification of functions as 'column' on sql search criteria.
+
+When we fail to load a related model class for a good reason, actually die, rather than burying the error
+
+mysql doesn't handle bare varchar's
+
+TODO a test that needs us to force mysql to be case sensitive
+
+typo fix in docs
+
+We can have 0-valued columns, again
+
+New column attribute: is case_sensitive
+
+Make load_by_cols respect column case sensitivity.
+
+NOTE this is a behaviour change as columns are by default
+case-insensitive, and the call was doing a case sensitive
+search, which is different from Collection searching.
+
+To make Jifty::DBI load_by_cols work in the old behaviour:
+
+Use $rec->load_by_cols( name => { value => 'foobar',
+case_sensitive => 0,
+operator => '=' });
+
+unreadable and immutable are now in attributes.
+
+Refactor Jifty::DBI::Column to split attributes into two groups.
+Actual column-related (in jifty::dbi layer) ones, such as type,
+remains as accesssors.  The rest goes into the attributes hash.
+This allows us to declare higher level meta data without having to
+change Jifty::DBI::Column in the future.
+
+Note that if you are accessing the column object's ->{label} directly
+this will now fail.  Use ->label instead.
+
+*Test updates for case issues 
+
+* Add support for non-lowercase column names
+
+Make sure limit() does not modify arrays passed in to the "value" parameter.
+
+* Allow is_distinct when joining collections, hinting the
+resulting collection is still distinct.
+
+* If table2 is given as a Jifty::DBI::Collection object,
+detect if the join is still distinct.
+
+Added documentation for using @EXPORT with mixins.
+
+Fix the call to the after_set trigger.
+
+Added a new accessor, RECORD_MIXINS, for tracking which mixins have been attached to a model.
+
+Adding basic documentation for before_set and after_set triggers.
+
+Adding before_set and after_set triggers and tests for them.
+
+Moved Jifty::DBI to use Class::Trigger instead of its own fork of Class::Trigger.
+
+Added tests and debugged register_triggers_for_column on record plugins.
+
+Improvements to the trigger documentation for after_create and before_create.
+
+Fix the documentation of the after_create trigger to match the actual behavior (and only that, this time).
+
+Removed register_triggers_late() in favor of register_triggers_for_column(), which is nearly the same thing but with clearer semantics.
+
+Updated documentation to discuss the register_triggers_late() method.
+0.43 Fri Aug 24 00:13:20 EDT 2007
+
+
+----------------------------------------------------------------------
+r66529 (orig r3939):  audreyt | 2007-08-22 12:38:23 -0400
+
+* Jifty::DBI::Filter::DateTime: The _time_zone setting was not
+  honoured by ->decode, because the code had a no-op $dt->time_zone($tz)
+  where it should've been $dt->set_time_zone($tz).
+----------------------------------------------------------------------
+r66518 (orig r3928):  falcone | 2007-08-21 19:09:22 -0400
+
+ r23979 at ketch:  falcone | 2007-08-21 18:00:57 -0400
+ * tell Pg to be case sensitive on IN clauses and handle the arrayref
+   structure used by IN
+
+----------------------------------------------------------------------
+r66517 (orig r3927):  falcone | 2007-08-21 19:08:59 -0400
+
+ r23978 at ketch:  falcone | 2007-08-21 18:00:27 -0400
+ * we don't need to TODO this MySQL test because the case matches exactly
+   and returns values
+
+----------------------------------------------------------------------
+r66197 (orig r3926):  yves | 2007-08-21 10:49:01 -0400
+
+debian packaging
+
+----------------------------------------------------------------------
+r66169 (orig r3923):  efunneko | 2007-08-20 16:57:43 -0400
+
+Added support for the IN operator within limits.  This should be accompanied by an array ref of values to be compared against.  It also allows an array ref to be passed with the '=' operator to accomplish the same thing
+----------------------------------------------------------------------
+r66164 (orig r3918):  sterling | 2007-08-20 15:56:18 -0400
+
+Fixing the name of the branch.
+----------------------------------------------------------------------
+r66163 (orig r3917):  sterling | 2007-08-20 15:54:42 -0400
+
+Creating a new branch to develop improvements on indexes and relationships.
+----------------------------------------------------------------------
+r66157 (orig r3911):  trs | 2007-08-16 15:59:16 -0400
+
+ r26169 at zot:  tom | 2007-08-16 15:58:56 -0400
+ Backup r3908 for now since it breaks existing code
+
+----------------------------------------------------------------------
+r66154 (orig r3908):  sterling | 2007-08-16 10:02:48 -0400
+
+ r8684 at dynpc145:  andrew | 2007-08-16 08:55:32 -0500
+ Added support for virtual record columns.
+
+----------------------------------------------------------------------
+r66153 (orig r3907):  sterling | 2007-08-16 10:02:26 -0400
+
+ r8682 at dynpc145:  andrew | 2007-08-15 11:17:32 -0500
+ Added more code comments, minor perl tidy, and removed a redundant call to set the column type.
+
+----------------------------------------------------------------------
+r65006 (orig r3743):  falcone | 2007-07-31 16:26:13 -0400
+
+ r23202 at ketch:  falcone | 2007-07-31 16:25:03 -0400
+ * we have code that uses Jifty::Collection::implicit_clauses
+   and expects that where clause to still show up on an unlimit.
+   This is wrong, but we need to sort out the "right" thing to do.
+
+----------------------------------------------------------------------
+r64931 (orig r3741):  jesse | 2007-07-30 20:27:30 -0400
+
+ r64930 at pinglin:  jesse | 2007-07-30 20:27:14 -0400
+ * Removed this. not time yet
+
+----------------------------------------------------------------------
+r64929 (orig r3740):  jesse | 2007-07-30 19:55:14 -0400
+
+ r64923 at pinglin:  jesse | 2007-07-30 19:54:31 -0400
+  * first stab at a UUID filter
+
+----------------------------------------------------------------------
+r64902 (orig r3729):  jesse | 2007-07-28 20:27:31 -0400
+
+ r64901 at pinglin:  jesse | 2007-07-28 18:29:15 -0500
+ * Added a find_all_rows method which works like 'unlimit' without the side effect of calling _clean_slate and zapping other metadata. 
+ * Clarified unlimit's somewhat brutal nature.
+ 
+     - Thanks to Mikko Lipasti
+
+----------------------------------------------------------------------
+r60774 (orig r3694):  sartak | 2007-07-16 12:49:13 -0400
+
+Fix 'uninitialized value' warnings
+----------------------------------------------------------------------
+r60655 (orig r3692):  jesse | 2007-07-14 00:06:17 -0400
+
+ r60634 at pinglin:  jesse | 2007-07-13 20:14:44 -0400
+ * Additional  bulletproofing to stop jifty from trying to insert a bogus date into the database.
+
+----------------------------------------------------------------------
+r60350 (orig r3644):  trs | 2007-07-10 01:32:48 -0400
+
+ r25085 at zot:  tom | 2007-07-10 01:32:08 -0400
+ Fix type for Pg
+
 
 0.41 Mon Apr 16 16:16:12 EDT 2007
  * Fixed a broken dependency. Thanks to SAPER

Modified: Jifty-DBI/branches/index-relationship/MANIFEST
==============================================================================
--- Jifty-DBI/branches/index-relationship/MANIFEST	(original)
+++ Jifty-DBI/branches/index-relationship/MANIFEST	Mon Dec 10 17:40:42 2007
@@ -20,7 +20,6 @@
 inc/Module/Install/Win32.pm
 inc/Module/Install/WriteAll.pm
 lib/Jifty/DBI.pm
-lib/Jifty/DBI/Class/Trigger.pm
 lib/Jifty/DBI/Collection.pm
 lib/Jifty/DBI/Collection/Union.pm
 lib/Jifty/DBI/Collection/Unique.pm
@@ -82,6 +81,10 @@
 t/15types.t
 t/16inheritance.t
 t/17virtualtypes.t
+t/18triggers.t
+t/19reference.t
+t/case_sensitivity.t
+t/metadata.t
 t/pod-coverage.t
 t/pod.t
 t/testmodels.pl

Modified: Jifty-DBI/branches/index-relationship/MANIFEST.SKIP
==============================================================================
--- Jifty-DBI/branches/index-relationship/MANIFEST.SKIP	(original)
+++ Jifty-DBI/branches/index-relationship/MANIFEST.SKIP	Mon Dec 10 17:40:42 2007
@@ -5,6 +5,7 @@
 ^MANIFEST\.
 ^Makefile$
 ^blib/
+^pm_to_blib$
 ^MakeMaker-\d
 ~$
 \.old$

Modified: Jifty-DBI/branches/index-relationship/META.yml
==============================================================================
--- Jifty-DBI/branches/index-relationship/META.yml	(original)
+++ Jifty-DBI/branches/index-relationship/META.yml	Mon Dec 10 17:40:42 2007
@@ -3,7 +3,7 @@
   DBD::SQLite: 0
   Test::More: 0.52
 distribution_type: module
-generated_by: Module::Install version 0.670
+generated_by: Module::Install version 0.67
 license: perl
 meta-spec: 
   url: http://module-build.sourceforge.net/META-spec-v1.3.html
@@ -19,9 +19,10 @@
   Class::Accessor::Fast: 0
   Class::Data::Inheritable: 0
   Class::ReturnValue: 0.4
+  Class::Trigger: 0.12
   Clone: 0
   DBI: 0
-  DBIx::DBSchema: 0.33
+  DBIx::DBSchema: 0.34
   Data::Page: 0
   DateTime: 0.34
   DateTime::Format::ISO8601: 0
@@ -36,4 +37,4 @@
   YAML::Syck: 0
   perl: 5.8.3
   version: 0
-version: 0.42
+version: 0.46

Modified: Jifty-DBI/branches/index-relationship/Makefile.PL
==============================================================================
--- Jifty-DBI/branches/index-relationship/Makefile.PL	(original)
+++ Jifty-DBI/branches/index-relationship/Makefile.PL	Mon Dec 10 17:40:42 2007
@@ -7,9 +7,10 @@
 requires('Class::Accessor::Fast' => 0);
 requires('Class::Data::Inheritable');
 requires('Class::ReturnValue', 0.40);
+requires('Class::Trigger', 0.12);
 requires('Clone');
 requires('DBI');
-requires('DBIx::DBSchema' => '0.33');
+requires('DBIx::DBSchema' => '0.34');
 requires('Data::Page');
 requires('DateTime' => 0.34);
 requires('DateTime::Format::ISO8601');

Modified: Jifty-DBI/branches/index-relationship/README
==============================================================================
--- Jifty-DBI/branches/index-relationship/README	(original)
+++ Jifty-DBI/branches/index-relationship/README	Mon Dec 10 17:40:42 2007
@@ -1,45 +1,172 @@
 NAME
-    Jifty::DBI - An object-relational mapper
+    Jifty::DBI - An object-relational persistence framework
 
 DESCRIPTION
+    Jifty::DBI deals with databases, so that you don't have to.
 
     This module provides an object-oriented mechanism for retrieving and
-    updating data in a DBI-accesible database. 
-    
-    This module is the direct descendent of DBIx::SearchBuilder. If you're familiar
-    with SearchBuilder, Jifty::DBI should be quite familiar to you.
-
-    Jifty::DBI was forked from SearchBuilder to give us the opportunity to seriously
-    clean up the API and internals without breaking existing applications.
-
-INSTALLATION
-
-    $ perl Makefile.PL
-    $ make
-    $ make test   # but see below for how to actually test against a test database
-    # make install
-
-TESTING
-
-    In order to test most of the features of Jifty::DBI, you need
-    to provide "make test" with a test database. For each DBI driver that
-    you would like to test, set the environment variables "JDBI_TEST_FOO",
-    "JDBI_TEST_FOO_USER", and "JDBI_TEST_FOO_PASS" to a database name, database
-    username, and database password, where "FOO" is the driver name in all
-    uppercase. You can test as many drivers as you like. (The appropriate
-    "DBD::" module needs to be installed in order for the test to work.)
-    Note that the "SQLite" driver will automatically be tested if
-    "DBD::Sqlite" is installed, using a temporary file as the database. For
-    example:
-
-      JDBI_TEST_MYSQL=test JDBI_TEST_MYSQL_USER=root JDBI_TEST_MYSQL_PASS=foo \
-        JDBI_TEST_PG=test JDBI_TEST_PG_USER=postgres  make test
+    updating data in a DBI-accessible database.
 
-AUTHOR
-    Copyright (c) 2001-2005 Jesse Vincent, jesse at fsck.com.
+    This module is the direct descendent of DBIx::SearchBuilder. If you're
+    familiar with SearchBuilder, Jifty::DBI should be quite familiar to you.
 
-    All rights reserved.
+  What is it trying to do.
+    Jifty::DBI::Record abstracts the agony of writing the common and
+    generally simple SQL statements needed to serialize and de-serialize an
+    object to the database. In a traditional system, you would define
+    various methods on your object 'create', 'read', 'update', and 'delete'
+    being the most common. In each method you would have a SQL statement
+    like:
 
-    This library is free software; you can redistribute it and/or modify it
-    under the same terms as Perl itself.
+      select * from table where value='blah';
+
+    If you wanted to control what data a user could modify, you would have
+    to do some special magic to make accessors do the right thing. Etc. The
+    problem with this approach is that in a majority of the cases, the SQL
+    is incredibly simple and the code from one method/object to the next was
+    basically the same.
+
+    <trumpets>
+
+    Enter, Jifty::DBI::Record.
+
+    With ::Record, you can in the simple case, remove all of that code and
+    replace it by defining two methods and inheriting some code. It's pretty
+    simple and incredibly powerful. For more complex cases, you can do more
+    complicated things by overriding certain methods. Let's stick with the
+    simple case for now.
+
+  An Annotated Example
+    The example code below makes the following assumptions:
+
+    *   The database is 'postgres',
+
+    *   The host is 'reason',
+
+    *   The login name is 'mhat',
+
+    *   The database is called 'example',
+
+    *   The table is called 'simple',
+
+    *   The table looks like so:
+
+              id     integer     not NULL,   primary_key(id),
+              foo    varchar(10),
+              bar    varchar(10)
+
+    First, let's define our record class in a new module named "Simple.pm".
+
+      use warnings;
+      use strict;
+
+      package Simple;
+      use Jifty::DBI::Schema;
+      use Jifty::DBI::Record schema {
+        column foo => type is 'text';
+        column bar => type is 'text';
+      };
+
+      # your custom code goes here.
+
+      1;
+
+    Like all perl modules, this needs to end with a true value.
+
+    Now, on to the code that will actually *do* something with this object.
+    This code would be placed in your Perl script.
+
+      use Jifty::DBI::Handle;
+      use Simple;
+
+    Use two packages, the first is where I get the DB handle from, the
+    latter is the object I just created.
+
+      my $handle = Jifty::DBI::Handle->new();
+      $handle->connect(
+          driver   => 'Pg',
+          database => 'test',
+          host     => 'reason',
+          user     => 'mhat',
+          password => ''
+      );
+
+    Creates a new Jifty::DBI::Handle, and then connects to the database
+    using that handle. Pretty straight forward, the password '' is what I
+    use when there is no password. I could probably leave it blank, but I
+    find it to be more clear to define it.
+
+     my $s = Simple->new( handle => $handle );
+
+     $s->load_by_cols(id=>1); 
+
+    load_by_cols
+        Takes a hash of column => value pairs and returns the *first* to
+        match. First is probably lossy across databases vendors.
+
+    load_from_hash
+        Populates this record with data from a Jifty::DBI::Collection. I'm
+        currently assuming that Jifty::DBI is what we use in cases where we
+        expect > 1 record. More on this later.
+
+    Now that we have a populated object, we should do something with it!
+    ::Record automagically generates accessors and mutators for us, so all
+    we need to do is call the methods. accessors are named "column"(), and
+    Mutators are named "set_column"($). On to the example, just appending
+    this to the code from the last example.
+
+     print "ID  : ", $s->id(),  "\n";
+     print "Foo : ", $s->foo(), "\n";
+     print "Bar : ", $s->bar(), "\n";
+
+    Thats all you have to to get the data, now to change the data!
+
+     $s->set_bar('NewBar');
+
+    Pretty simple! Thats really all there is to it. Set<Field>($) returns a
+    boolean and a string describing the problem. Lets look at an example of
+    what will happen if we try to set a 'Id' which we previously defined as
+    read only.
+
+     my ($res, $str) = $s->set_id('2');
+     if (! $res) {
+       ## Print the error!
+       print "$str\n";
+     } 
+
+    The output will be:
+
+      >> Immutable column
+
+    Currently Set<Field> updates the data in the database as soon as you
+    call it. In the future I hope to extend ::Record to better support
+    transactional operations, such that updates will only happen when "you"
+    say so.
+
+    Finally, adding and removing records from the database. ::Record
+    provides a Create method which simply takes a hash of key => value
+    pairs. The keys exactly map to database columns.
+
+     ## Get a new record object.
+     $s1 = Simple->new( handle => $handle );
+     my ($id, $status_msg) = $s1->create(id  => 4,
+                       foo => 'Foooooo', 
+                       bar => 'Barrrrr');
+
+    Poof! A new row in the database has been created! Now lets delete the
+    object!
+
+     my $s2 = Simple->new( handle => $handle );
+     $s2->load_by_cols(id=>4);
+     $s2->delete();
+
+    And it's gone.
+
+    For simple use, thats more or less all there is to it. In the future, I
+    hope to exapand this HowTo to discuss using container classes,
+    overloading, and what ever else I think of.
+
+LICENSE
+    Jifty::DBI is Copyright 2005-2007 Best Practical Solutions, LLC.
+    Jifty::DBI is distributed under the same terms as Perl itself.
 

Modified: Jifty-DBI/branches/index-relationship/SIGNATURE
==============================================================================
--- Jifty-DBI/branches/index-relationship/SIGNATURE	(original)
+++ Jifty-DBI/branches/index-relationship/SIGNATURE	Mon Dec 10 17:40:42 2007
@@ -14,95 +14,100 @@
 -----BEGIN PGP SIGNED MESSAGE-----
 Hash: SHA1
 
-SHA1 340b2aa0d4522603ae1fc8477e92693b06d76026 Changes
-SHA1 59f221e6d6cbc2ba7d2a2f5c83115478d52ee18d MANIFEST
-SHA1 c958110f917af76f91efe84aa272c701065f5d41 META.yml
-SHA1 f395ee7217d1e0243a1353d3289dbb71a8c2e082 Makefile.PL
-SHA1 d0943ab047f543c92405564ab77ba008052544e6 README
+SHA1 3334928687400b465f3a892a9658e48084c6c646 Changes
+SHA1 f559ca6ef0bd30360aad7c77fb6d4251158f87ed MANIFEST
+SHA1 f489b63d4c48d37e3f60388dbac1e2ef9a571999 META.yml
+SHA1 1e748530883277156c87f2ec864746ee8780d745 Makefile.PL
+SHA1 ae8407c841f230c353f683bd5c257815aed9b9f0 README
 SHA1 82d6ac3f6def48558d09f8b6e3b53ed4194d8c81 ROADMAP
 SHA1 9d304f35438f847863969f6a069598379f5a9db2 debian/README
-SHA1 9fc3eb1a0e3cf5b81a689fd78c2cffb8316b945d debian/changelog
+SHA1 603ce50bde12c6a3a91f28742ffd8a9d3917ec79 debian/changelog
 SHA1 5d9474c0309b7ca09a182d888f73b37a8fe1362c debian/compat
-SHA1 c77ea854fe4acc7f8d63cf249b331f67cf41a030 debian/control
+SHA1 9cfaec0ba99948cbb812aa1d00005e90f55861b3 debian/control
 SHA1 c1085db4f95bd6e7e7470ccab55f8adba10d5024 debian/rules
 SHA1 c28087e498978a1a314dfcaa584844703f31ac8c doc/notes/on_intuitive_schema_definitions
 SHA1 584c0f6cdebcbf760dfca8413c94783586120214 ex/Example/Model/Address.pm
 SHA1 7cea1a5289f79c2a87837924a83feb583f6e8890 ex/Example/Model/Employee.pm
 SHA1 a9d62e4f5b43b2f78066172a4771238ee7df6339 ex/create_tables.pl
 SHA1 603bb9de29fb8cba7f13409c546750972eff645d inc/Module/AutoInstall.pm
-SHA1 9b2f9d83bcf77860f53a0c07c90a4a59ad9f5df1 inc/Module/Install.pm
-SHA1 ad955f51ad2c40d4ba35395c27f5ed899a80bf7a inc/Module/Install/AutoInstall.pm
-SHA1 abe32855d75ab13747cf65765af9947b7a8c3057 inc/Module/Install/Base.pm
-SHA1 95b81d1e91bd634467bf633571eff4420e9c04eb inc/Module/Install/Can.pm
-SHA1 1fe98c63cf9d7271c8cb4183ba230f152df69e26 inc/Module/Install/Fetch.pm
-SHA1 0606a8b02a420600bc3e2b65ab82f70266784926 inc/Module/Install/Include.pm
-SHA1 2249171a2b72cd73ff2c0a06597d29f86e5df456 inc/Module/Install/Makefile.pm
-SHA1 381bb98ea3877bba49ae85e7a7ea130645fd3dbf inc/Module/Install/Metadata.pm
-SHA1 0c2118868ef82ac517eb6d9c3bd93e6eb9bbf83e inc/Module/Install/Win32.pm
-SHA1 e827d6d43771032fa3df35c0ad5e5698d0e54cda inc/Module/Install/WriteAll.pm
-SHA1 7f3e3f4b65b7c2252b3b4143526c26b4fb3e607f lib/Jifty/DBI.pm
-SHA1 46d3dafdc13341ffe1230c654a66eeb329f96093 lib/Jifty/DBI/Class/Trigger.pm
-SHA1 fcbea0817e692158efa1090c85feaf6e6cbea7d3 lib/Jifty/DBI/Collection.pm
-SHA1 da7059734dc429040250589d704a8ad5d786c916 lib/Jifty/DBI/Collection/Union.pm
+SHA1 78edb89a439463e44c33a72bbee84c54d0dc8aaf inc/Module/Install.pm
+SHA1 ae32c02b539901de91f06366ce9bdbb7a7bd040b inc/Module/Install/AutoInstall.pm
+SHA1 8ea4e37df83fd0c1c050be5c8da75545c3828d9b inc/Module/Install/Base.pm
+SHA1 1da6031583c32f0d1ec073b8376102fc51427dcc inc/Module/Install/Can.pm
+SHA1 b779375b90c16af2f31f38a1dd2b5df223c7f2fb inc/Module/Install/Fetch.pm
+SHA1 6bf0d0d100b94d1a2ce64d010c8813dec26ac480 inc/Module/Install/Include.pm
+SHA1 2054450e1e9c1dd8056362bf4a64ae70d5d71476 inc/Module/Install/Makefile.pm
+SHA1 5d6189b2cad15cf9932a28faafd55130c8247e83 inc/Module/Install/Metadata.pm
+SHA1 02af973fae2ac3531fa6b704574b2b8cb2a08148 inc/Module/Install/Win32.pm
+SHA1 3a2eab96e91cca8d99938cda7791759ae9d97b3a inc/Module/Install/WriteAll.pm
+SHA1 22e3f05e9262cc24a497f98efc3a92e0420bbffa lib/Jifty/DBI.pm
+SHA1 23294906d6424ae747d284ba03d6b7b71113405e lib/Jifty/DBI/Collection.pm
+SHA1 639ef9c81f03fb084b312a5f9a6f6a3ff63b36b7 lib/Jifty/DBI/Collection/Union.pm
 SHA1 bcba77fd2bacf0475aea1de97f57365c8de92ca6 lib/Jifty/DBI/Collection/Unique.pm
-SHA1 ba2b05a10ca41fa8491421009350ab227a19cc76 lib/Jifty/DBI/Column.pm
-SHA1 a2c16702f3467a220e9ba96ac5e086cc2e7779d1 lib/Jifty/DBI/Filter.pm
+SHA1 049ce7a6d91bf5ae0dcfa5c3f50dfd6a682f8e70 lib/Jifty/DBI/Column.pm
+SHA1 faf43ec59f8ebd094a1bdfc92eda92053d511e97 lib/Jifty/DBI/Filter.pm
 SHA1 87192bf64a224cbea78770f4209ecae9981f3f5c lib/Jifty/DBI/Filter/Date.pm
-SHA1 e7d1ddfa3a55f69680d8637071b53d516ad0fc7d lib/Jifty/DBI/Filter/DateTime.pm
+SHA1 70ffc9da2f13fc1aabd652169ba5a08b292ba678 lib/Jifty/DBI/Filter/DateTime.pm
 SHA1 79649ca3fb9f8aa9d2fdda00d6d7c7c99fe4092f lib/Jifty/DBI/Filter/SaltHash.pm
 SHA1 45ff3c7d2c03136acf98b74c659e2fe8c734d929 lib/Jifty/DBI/Filter/Storable.pm
 SHA1 13837e1f389b4e2e60e8b2395b327604ec7e25b6 lib/Jifty/DBI/Filter/Time.pm
-SHA1 faf3c393d9980a33aa87991ad235a385cdd05a53 lib/Jifty/DBI/Filter/Truncate.pm
-SHA1 d05dc7bc82040704770386705fdbdfd2fc326a57 lib/Jifty/DBI/Filter/YAML.pm
+SHA1 83b92752da73eb8a88546509b4affaf57754ea66 lib/Jifty/DBI/Filter/Truncate.pm
+SHA1 67ffe7188a1f529d7594f4fa3803bcbe15ba6485 lib/Jifty/DBI/Filter/YAML.pm
 SHA1 9a6fd17e677321904436fefec4d434e17a4685b1 lib/Jifty/DBI/Filter/base64.pm
 SHA1 deb33fa7b35f3542aac3e2d7fb4b5d3070dc3917 lib/Jifty/DBI/Filter/utf8.pm
-SHA1 5ec6fa1efcdfac7398b84939826084d950a85040 lib/Jifty/DBI/Handle.pm
-SHA1 9d07c7e4f629bd75d41a1ba88a7890fedaf8a9d3 lib/Jifty/DBI/Handle/Informix.pm
-SHA1 b924dfc77946ec22c292a405d4a26b46b457f775 lib/Jifty/DBI/Handle/ODBC.pm
-SHA1 65ea774794a6d7f5bd3f14cb790d6a915903ee80 lib/Jifty/DBI/Handle/Oracle.pm
-SHA1 ec835b0bc23f74991012bbd6bbad08535d38c326 lib/Jifty/DBI/Handle/Pg.pm
-SHA1 28ce52fe0d1f765d37591710be696deff9a1705d lib/Jifty/DBI/Handle/SQLite.pm
-SHA1 483de5eefde5ef2d194d18ad81a44b411122ad27 lib/Jifty/DBI/Handle/Sybase.pm
+SHA1 759b75ddc720d400f610b2a460019eca902b8ae8 lib/Jifty/DBI/Handle.pm
+SHA1 bcc7c456e1c4d0dddd5564f03c8bb03a6c7e261f lib/Jifty/DBI/Handle/Informix.pm
+SHA1 338116a45f8eb6bfca5e76e8d3be78fb61fffe81 lib/Jifty/DBI/Handle/ODBC.pm
+SHA1 960fd0b63f3de11924c5d47a3c0c6d1db105ed5b lib/Jifty/DBI/Handle/Oracle.pm
+SHA1 2b64b747d072c67a510e79685cc08bebaf0a1402 lib/Jifty/DBI/Handle/Pg.pm
+SHA1 860b373ddb0d0a1d001f20c721d9dffe003e2517 lib/Jifty/DBI/Handle/SQLite.pm
+SHA1 bba2314c20fcc3ef71cc69090f1cd6bd515cd9b4 lib/Jifty/DBI/Handle/Sybase.pm
 SHA1 e6041a34c3044ed8b9691a5629ecf146fed95257 lib/Jifty/DBI/Handle/mysql.pm
 SHA1 f2cc4fcce79c9a88a023d4e6bd96c2089eef1ced lib/Jifty/DBI/Handle/mysqlPP.pm
 SHA1 0e975f9ec5480ca09025c592c06d484058e637df lib/Jifty/DBI/HasFilters.pm
-SHA1 8be6ef39a3145d46f0b236d038908d8f86fb8427 lib/Jifty/DBI/Record.pm
-SHA1 968bc51f282bfe9f9b069a0db6830b49fef99941 lib/Jifty/DBI/Record/Cachable.pm
+SHA1 61579d4fb19bab8d89471ec1dc6afea0e86394e2 lib/Jifty/DBI/Record.pm
+SHA1 1c8b4adcd312024a3408a52d3b2ef288c2084603 lib/Jifty/DBI/Record/Cachable.pm
 SHA1 f4ec61cd857cb1cead8c9c5551047dc78734b73a lib/Jifty/DBI/Record/Memcached.pm
-SHA1 85342548af141aa7121e261faaf294c8485df718 lib/Jifty/DBI/Record/Plugin.pm
-SHA1 7efba3db34611c4df677eb5acfed569cea4eedaf lib/Jifty/DBI/Schema.pm
-SHA1 c4f05cbe77cb45dcfa1229e4cfeaa1cab8f15e8d lib/Jifty/DBI/SchemaGenerator.pm
+SHA1 6a70dc2a6fd6a5d77502d391ac2630d91a681c1a lib/Jifty/DBI/Record/Plugin.pm
+SHA1 08c285edcd05698b982c082f50da82fde02e4d31 lib/Jifty/DBI/Schema.pm
+SHA1 5594fb70d330523a9e6c7b5a1d2ea2f968be2615 lib/Jifty/DBI/SchemaGenerator.pm
 SHA1 32834b7c4cf5a8d131382fccc8db341be8768291 t/00.load.t
 SHA1 9aa7fed2b2409faa4c71d2a45db210721f47403e t/01-version_checks.t
 SHA1 13c9fe3eeec0d000a7c86ea2474e30186cbc37e2 t/01basics.t
-SHA1 63c991bec4864e02a239bf6577a8ffc9be1b4eee t/01records.t
-SHA1 b1d9bb663d106e6874c0c454d64819e6b67d56d2 t/01searches.t
+SHA1 018309dfc89440dc670cccf6138e3d4679465b47 t/01records.t
+SHA1 7574130aa1dc5338b6efcd0f04eca3f6dc4b2696 t/01searches.t
 SHA1 933ebc7f0cfcaf03a2092a7c8271f98b2385f785 t/02-column_constraints.t
-SHA1 392422249cf3b3f0e26d94ab7a890efe8abc877e t/02records_cachable.t
-SHA1 a95a9329bb1b8dbcf4ba1cefea85c974a912e91f t/02records_object.t
+SHA1 90a5b9c663002e3c43a00b7f9a04d6ec2787500e t/02records_cachable.t
+SHA1 33642a61fd4b5a88436a82c6dd0fef359ba74a2b t/02records_object.t
+SHA1 369d71ca9ec0f00116ac6be24e017f592ac9cc45 t/02searches_joins.t
 SHA1 f1f330dd8b4144e3437aba1455053903306bd0bc t/03rebless.t
-SHA1 472ff16f7c3dc34238d9abd625cbb6e0108956fd t/04memcached.t
+SHA1 62c42d8458d73898f47f1b72d757239747321ef5 t/04memcached.t
 SHA1 a2d00943d47d52d3ad92efe67a52a9b8e1522903 t/06filter.t
 SHA1 8d464426f2c5b0ab5ecc5a0a0331e5f77669c2dc t/06filter_datetime.t
 SHA1 1c0727c29fb58462710e4578a237d557b8453a07 t/06filter_storable.t
 SHA1 f0f6ce9d48f419de6ac6154684f9065f32e30ddd t/06filter_truncate.t
 SHA1 2e9777a47e3a920d063bfbf9d56375c67c5b89c5 t/06filter_utf8.t
 SHA1 bb91f506a251d7b27d2fcd29c482a345318ef04f t/06filter_yaml.t
-SHA1 820a59b4e94a6a92275d4d0029827b36b8f0e69b t/10schema.t
-SHA1 09029344467af5bd30e6cd4dedbe0da8f0894e04 t/11schema_records.t
-SHA1 c3bbf4e58ae6653c55d1e302bcf5843f7d7239b7 t/12prefetch.t
+SHA1 64c3722f5b34feafc87113257079721c174f3f96 t/10schema.t
+SHA1 0f4655f0a4e558ac31df7b7fdf17c9b110f934da t/11schema_records.t
+SHA1 22a083137927a8635b02931e40f80c60220ec3a7 t/12prefetch.t
 SHA1 a93e0ee622b2291f797887f663f33c30fc7339f6 t/13collection.t
-SHA1 f057b643275b0370ae18d47b3a1b394791c850d6 t/14handle-pg.t
+SHA1 41b7fbaf031d103a4f2066f177cc3bee84ab0458 t/14handle-pg.t
 SHA1 4f41229caa246bf6ebb369010deb0c1eb8809666 t/15types.t
 SHA1 5958e59e29d29fbf3862b5d3471472cbd82d191e t/16inheritance.t
+SHA1 c7004285662f16abca274918f86d17ea43fe8c90 t/17virtualtypes.t
+SHA1 cc7d6dd9889837143074729d30030ddabcfa6b9e t/18triggers.t
+SHA1 54b7727b49111162703581d13dd47dfe276fbe9a t/19reference.t
+SHA1 5b3f8373687f89ccf3faf2dfbda9663137a8c078 t/case_sensitivity.t
+SHA1 1dd9675b0a9a59fdcd300f5d92297f0ecf4f03e4 t/metadata.t
 SHA1 59c44900b1cb957d262f96363ceff21b46e0d598 t/pod-coverage.t
 SHA1 e9c6a5881fc60173fbc8d479c1afd2ce3b43bef1 t/pod.t
-SHA1 17f75f50d9cb40ad8477ec8d77c75cca844a17b5 t/testmodels.pl
-SHA1 9b6cf7d135201f3f5ac4e29eaf180c85ba2e2bbf t/utils.pl
+SHA1 62742c946808f35bcc8b2777e975c1ce068a0a71 t/testmodels.pl
+SHA1 2cf6ba23eb00dfed6f10533830da066c774c030c t/utils.pl
 -----BEGIN PGP SIGNATURE-----
-Version: GnuPG v1.4.3 (Darwin)
+Version: GnuPG v1.4.7 (Darwin)
 
-iD8DBQFGI9peEi9d9xCOQEYRAmCEAKCME/cKRb9dxCNR/pw9Qlmahv2M1wCfTLz5
-mctzPoTKU8ZwPWChyZQ74h8=
-=HdZT
+iD8DBQFHTzB0sxfQtHhyRPoRAtM9AJ9Cy4J75ED7ppZVxRT/Ar2wPvuLwwCgj8h3
+XrFbmqQ0xgMosHiFK7ieL6U=
+=S/Zy
 -----END PGP SIGNATURE-----

Modified: Jifty-DBI/branches/index-relationship/debian/changelog
==============================================================================
--- Jifty-DBI/branches/index-relationship/debian/changelog	(original)
+++ Jifty-DBI/branches/index-relationship/debian/changelog	Mon Dec 10 17:40:42 2007
@@ -1,3 +1,9 @@
+libjifty-dbi-perl (0.42-1) unstable; urgency=low
+
+  * 0.42 
+
+ -- AGOSTINI Yves <agostini at univ-metz.fr>  Tue, 21 Aug 2007 10:34:15 +0200
+
 libjifty-dbi-perl (0.41-1) unstable; urgency=low
 
   * 0.41

Modified: Jifty-DBI/branches/index-relationship/debian/control
==============================================================================
--- Jifty-DBI/branches/index-relationship/debian/control	(original)
+++ Jifty-DBI/branches/index-relationship/debian/control	Mon Dec 10 17:40:42 2007
@@ -6,7 +6,7 @@
 Package: libjifty-dbi-perl
 Section: perl
 Architecture: all
-Depends: ${perl:Depends}, libcache-simple-timedexpiry-perl (>> 0.21), libclass-accessor-perl, libclass-data-inheritable-perl, libclass-returnvalue-perl (>> 0.40), libclone-perl, libdbi-perl, libdbix-dbschema-perl (>> 0.29), libdata-page-perl, libdatetime-perl (>> 0.34), libdatetime-format-iso8601-perl, libdatetime-format-strptime-perl, libexporter-lite-perl, libhash-merge-perl, liblingua-en-inflect-perl, libobject-declare-perl (>> 0.22), libuniversal-require-perl, libscalar-defer-perl (>> 0.10), libversion-perl, perl-modules (>> 0.52), libdbd-sqlite3-perl, libyaml-syck-perl
+Depends: ${perl:Depends}, libcache-simple-timedexpiry-perl (>> 0.21), libclass-accessor-perl, libclass-data-inheritable-perl, libclass-returnvalue-perl (>> 0.40), libclone-perl, libdbi-perl, libdbix-dbschema-perl (>> 0.33), libdata-page-perl, libdatetime-perl (>> 0.34), libdatetime-format-iso8601-perl, libdatetime-format-strptime-perl, libexporter-lite-perl, libhash-merge-perl, liblingua-en-inflect-perl, libobject-declare-perl (>> 0.22), libuniversal-require-perl, libscalar-defer-perl (>> 0.10), libversion-perl, perl-modules (>> 0.52), libdbd-sqlite3-perl, libyaml-syck-perl
 Recommends: libcache-memcached-perl
 Description: Jifty DBI
  Jifty DBI

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI.pm	Mon Dec 10 17:40:42 2007
@@ -2,7 +2,7 @@
 use warnings;
 use strict;
 
-$Jifty::DBI::VERSION = '0.42';
+$Jifty::DBI::VERSION = '0.48';
 
 =head1 NAME
 

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Collection.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Collection.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Collection.pm	Mon Dec 10 17:40:42 2007
@@ -2,6 +2,7 @@
 
 use warnings;
 use strict;
+use Scalar::Defer qw/lazy/;
 
 =head1 NAME
 
@@ -11,7 +12,7 @@
 =head1 SYNOPSIS
 
   use Jifty::DBI::Collection;
-  
+
   package My::ThingCollection;
   use base qw/Jifty::DBI::Collection/;
 
@@ -20,18 +21,18 @@
   use Jifty::DBI::Record schema {
     column column_1 => type is 'text';
   };
-  
+
   package main;
 
   use Jifty::DBI::Handle;
   my $handle = Jifty::DBI::Handle->new();
   $handle->connect( driver => 'SQLite', database => "my_test_db" );
 
-  my $sb = My::ThingCollection->new( handle => $handle );
+  my $collection = My::ThingCollection->new( handle => $handle );
 
-  $sb->limit( column => "column_1", value => "matchstring" );
+  $collection->limit( column => "column_1", value => "matchstring" );
 
-  while ( my $record = $sb->next ) {
+  while ( my $record = $collection->next ) {
       print $record->id;
   }
 
@@ -60,7 +61,7 @@
 use Clone;
 use Carp qw/croak/;
 use base qw/Class::Accessor::Fast/;
-__PACKAGE__->mk_accessors(qw/pager preload_columns preload_related/);
+__PACKAGE__->mk_accessors(qw/pager prefetch_related derived/);
 
 =head1 METHODS
 
@@ -72,7 +73,7 @@
 should pass in a L<Jifty::DBI::Handle> (or one of its subclasses) like
 this:
 
-   my $sb = My::Jifty::DBI::Subclass->new( handle => $handle );
+   my $collection = My::Jifty::DBI::Subclass->new( handle => $handle );
 
 However, if your subclass overrides L</_init> you do not need to take
 a handle argument, as long as your subclass takes care of calling the
@@ -103,10 +104,12 @@
 sub _init {
     my $self = shift;
     my %args = (
-        handle => undef,
+        handle  => undef,
+        derived => undef,
         @_
     );
-    $self->_handle( $args{'handle'} ) if ( $args{'handle'} );
+    $self->_handle( $args{'handle'} )  if ( $args{'handle'} );
+    $self->derived( $args{'derived'} ) if ( $args{'derived'} );
     $self->table( $self->new_item->table() );
     $self->clean_slate(%args);
 }
@@ -143,11 +146,10 @@
     $self->{'alias_count'}      = 0;
     $self->{'first_row'}        = 0;
     $self->{'show_rows'}        = 0;
-    @{ $self->{'aliases'} } = ();
 
     delete $self->{$_} for qw(
         items
-        leftjoins
+        joins
         raw_rows
         count_all
         subclauses
@@ -203,76 +205,68 @@
     my $records = $self->_handle->simple_query($query_string);
     return 0 unless $records;
     my @names = @{ $records->{NAME_lc} };
-    my $data = {};
-    my $column_map = {};
-    foreach my $column (@names) {
-        if ($column =~ /^((\w+)_?(?:\d*))_(.*?)$/) {
-            $column_map->{$1}->{$2} =$column;
-        }
-    }
-    my @tables = keys %$column_map;
+    my $data  = {};
 
+    my @tables = (
+        "main", map { $_->{alias} } values %{ $self->prefetch_related || {} }
+    );
 
     my @order;
     while ( my $base_row = $records->fetchrow_hashref() ) {
-        my $main_pkey = $base_row->{$names[0]};
-        push @order, $main_pkey unless ( $order[0] && $order[-1] eq $main_pkey);
+        my $main_pkey = $base_row->{ $names[0] };
+        push @order, $main_pkey
+            unless ( $order[0] && $order[-1] eq $main_pkey );
 
-            # let's chop the row into subrows;
+        # let's chop the row into subrows;
         foreach my $table (@tables) {
             for ( keys %$base_row ) {
-                if ( $_ =~ /$table\_(.*)$/ ) {
-                    $data->{$main_pkey}->{$table} ->{ ($base_row->{ $table . '_id' } ||$main_pkey )}->{$1} = $base_row->{$_};
+                if (/^$table\_(.*)$/) {
+                    $data->{$main_pkey}->{$table}
+                        ->{ ( $base_row->{ $table . '_id' } || $main_pkey ) }
+                        ->{$1} = $base_row->{$_};
                 }
             }
         }
-
     }
 
-    # For related "record" values, we can simply prepopulate the
-    # Jifty::DBI::Record cache and life will be good. (I suspect we want
-    # to do this _before_ doing the initial primary record load on the
-    # off chance that the primary record will try to do the relevant
-    # prefetch manually For related "collection" values, our job is a bit
-    # harder. we need to create a new empty collection object, set it's
-    # "must search" to 0 and manually add the records to it for each of
-    # the items we find. Then we need to ram it into place.
-
-    foreach my $row_id ( @order) {
+    foreach my $row_id (@order) {
         my $item;
         foreach my $row ( values %{ $data->{$row_id}->{'main'} } ) {
             $item = $self->new_item();
             $item->load_from_hash($row);
         }
-        foreach my $alias ( grep { $_ ne 'main' } keys %{ $data->{$row_id} } ) {
+        foreach my $alias ( grep { $_ ne 'main' } keys %{ $data->{$row_id} } )
+        {
 
             my $related_rows = $data->{$row_id}->{$alias};
-            my ( $class, $col_name ) = $self->class_and_column_for_alias($alias);
-            if ($class) {
+            my ( $class, $col_name )
+                = $self->class_and_column_for_alias($alias);
+            next unless $class;
+
+            my @rows = sort { $a->{id} <=> $b->{id} }
+                grep { $_->{id} } values %$related_rows;
 
             if ( $class->isa('Jifty::DBI::Collection') ) {
-                my $collection = $class->new( handle => $self->_handle );
-                foreach my $row( sort { $a->{id} <=> $b->{id} }  values %$related_rows ) {
-                    my $entry
-                        = $collection->new_item( handle => $self->_handle );
+                my $collection = $class->new( $self->_new_collection_args,
+                    derived => 1 );
+                foreach my $row (@rows) {
+                    my $entry = $collection->new_item;
                     $entry->load_from_hash($row);
                     $collection->add_record($entry);
                 }
 
-                $item->_prefetched_collection( $col_name => $collection );
+                $item->prefetched( $col_name => $collection );
             } elsif ( $class->isa('Jifty::DBI::Record') ) {
-                foreach my $related_row ( values %$related_rows ) {
-                    my $item = $class->new( handle => $self->_handle );
-                    $item->load_from_hash($related_row);
-                }
+                warn "Multiple rows returned for $class in prefetch"
+                    if @rows > 1;
+                my $entry = $class->new( $self->_new_record_args );
+                $entry->load_from_hash( shift @rows ) if @rows;
+                $item->prefetched( $col_name => $entry );
             } else {
                 Carp::cluck(
-                    "Asked to preload $alias as a $class. Don't know how to handle $class"
+                    "Asked to prefetch $alias as a $class. Don't know how to handle $class"
                 );
             }
-            }
-
-
         }
         $self->add_record($item);
 
@@ -284,6 +278,16 @@
     return $self->_record_count;
 }
 
+sub _new_record_args {
+    my $self = shift;
+    return ( handle => $self->_handle );
+}
+
+sub _new_collection_args {
+    my $self = shift;
+    return ( handle => $self->_handle );
+}
+
 =head2 add_record RECORD
 
 Adds a record object to this collection.
@@ -324,7 +328,7 @@
 
 sub _do_count {
     my $self = shift;
-    my $all  = shift || 0;
+    my $all = shift || 0;
 
     my $query_string = $self->build_select_count_query();
     my $records      = $self->_handle->simple_query($query_string);
@@ -394,12 +398,31 @@
 
 sub _is_joined {
     my $self = shift;
-    if ( $self->{'leftjoins'} && keys %{ $self->{'leftjoins'} } ) {
+    if ( $self->{'joins'} && keys %{ $self->{'joins'} } ) {
         return (1);
     } else {
-        return ( @{ $self->{'aliases'} } );
+        return 0;
     }
+}
+
+=head2 _is_distinctly_joined
+
+Returns true if this collection is joining multiple table, but is
+joining other table's distinct fields, hence resulting in distinct
+resultsets.  The behaviour is undefined if called on a non-joining
+collection.
 
+=cut
+
+sub _is_distinctly_joined {
+    my $self = shift;
+    if ( $self->{'joins'} ) {
+        for ( values %{ $self->{'joins'} } ) {
+            return 0 unless $_->{is_distinct};
+        }
+
+        return 1;
+    }
 }
 
 # LIMIT clauses are used for restricting ourselves to subsets of the
@@ -450,6 +473,8 @@
 sub build_select_query {
     my $self = shift;
 
+    return "" if $self->derived;
+
     # The initial SELECT or SELECT DISTINCT is decided later
 
     my $query_string = $self->_build_joins . " ";
@@ -463,7 +488,7 @@
         $self->_distinct_query( \$query_string );
     } else {
         $query_string
-            = "SELECT " . $self->_preload_columns . " FROM $query_string";
+            = "SELECT " . $self->query_columns . " FROM $query_string";
         $query_string .= $self->_group_clause;
         $query_string .= $self->_order_clause;
     }
@@ -474,73 +499,55 @@
 
 }
 
-=head2 preload_columns
-
-The columns that the query would load for result items.  By default it's everything.
+=head2 query_columns
 
-XXX TODO: in the case of distinct, it needs to work as well.
+The columns that the query would load for result items.  By default
+it's everything.
 
 =cut
 
-sub _preload_columns {
+sub query_columns {
     my $self = shift;
 
-    my @cols            = ();
-    my $item            = $self->new_item;
-    if( $self->{columns} and @{ $self->{columns} } ) {
-         push @cols, @{$self->{columns}};
-         # push @cols, map { warn "Preloading $_"; "main.$_ as main_" . $_ } @{$preload_columns};
+    my @cols = ();
+    my $item = $self->new_item;
+    if ( $self->{columns} and @{ $self->{columns} } ) {
+        push @cols, @{ $self->{columns} };
     } else {
         push @cols, $self->_qualified_record_columns( 'main' => $item );
     }
-    my %preload_related = %{ $self->preload_related || {} };
-    foreach my $alias ( keys %preload_related ) {
-        my $related_obj = $preload_related{$alias};
-        if ( my $col_obj = $item->column($related_obj) ) {
-            my $reference_type = $col_obj->refers_to;
-
-            my $reference_item;
-
-            if ( !$reference_type ) {
-                Carp::cluck(
-                    "Asked to prefetch $col_obj->name for $self. But $col_obj->name isn't a known reference"
-                );
-            } elsif ( $reference_type->isa('Jifty::DBI::Collection') ) {
-                $reference_item = $reference_type->new->new_item();
-            } elsif ( $reference_type->isa('Jifty::DBI::Record') ) {
-                $reference_item = $reference_type->new;
-            } else {
-                Carp::cluck(
-                    "Asked to prefetch $col_obj->name for $self. But $col_obj->name isn't a known type"
-                );
-            }
-
-            push @cols,
-                $self->_qualified_record_columns( $alias => $reference_item );
+    my %prefetch_related = %{ $self->prefetch_related || {} };
+    foreach my $alias ( keys %prefetch_related ) {
+        my $class = $prefetch_related{$alias}{class};
+
+        my $reference;
+        if ( $class->isa('Jifty::DBI::Collection') ) {
+            $reference = $class->new->new_item( $self->_new_collection_args );
+        } elsif ( $class->isa('Jifty::DBI::Record') ) {
+            $reference = $class->new( $self->_new_record_args );
         }
 
-   #     push @cols, map { $_ . ".*" } keys %{ $self->preload_related || {} };
-
+        push @cols, $self->_qualified_record_columns( $alias => $reference );
     }
     return CORE::join( ', ', @cols );
 }
 
 =head2 class_and_column_for_alias
 
-Takes the alias you've assigned to a prefetched related object. Returns the class
-of the column we've declared that alias preloads.
+Takes the alias you've assigned to a prefetched related
+object. Returns the class of the column we've declared that alias
+prefetches.
 
 =cut
 
 sub class_and_column_for_alias {
-    my $self            = shift;
-    my $alias           = shift;
-    my %preload_related = %{ $self->preload_related || {} };
-    my $related_colname = $preload_related{$alias};
-    if ( my $col_obj = $self->new_item->column($related_colname) ) {
-        return ( $col_obj->refers_to => $related_colname );
-    }
-    return undef;
+    my $self     = shift;
+    my $alias    = shift;
+    my %prefetch = %{ $self->prefetch_related || {} };
+    my $related  = $prefetch{$alias};
+    return unless $related;
+
+    return $related->{class}, $related->{name};
 }
 
 sub _qualified_record_columns {
@@ -558,32 +565,230 @@
     } $item->columns;
 }
 
-=head2  prefetch ALIAS_NAME ATTRIBUTE
+=head2 prefetch PARAMHASH
+
+Prefetches properties of a related table, in the same query.  Possible
+keys in the paramhash are:
 
-prefetches all related rows from alias ALIAS_NAME into the record attribute ATTRIBUTE of the
-sort of item this collection is.
+=over
+
+=item name
+
+This argument is required; it specifies the name of the collection or
+record that is to be prefetched.  If the name matches a column with a
+C<refers_to> relationship, the other arguments can be inferred, and
+this is the only parameter which needs to be passed.
+
+It is possible to pass values for C<name> which are not real columns
+in the model; these, while they won't be accessible by calling 
+C<< $record-> I<columnname> >> on records in this collection, will
+still be accessible by calling C<< $record->prefetched( I<columnname> ) >>.
+
+=item reference
+
+Specifies the series of column names to traverse to extract the
+information.  For instance, if groups referred to multiple users, and
+users referred to multiple phone numbers, then providing
+C<users.phones> would do the two necessary joins to produce a phone
+collection for all users in each group.
+
+This option defaults to the name, and is irrelevant if an C<alias> is
+provided.
+
+=item alias
 
-If you have employees who have many phone numbers, this method will let you search for all your employees
-    and prepopulate their phone numbers.
+Specifies an alias which has already been joined to this collection as
+the source of the prefetched data.  C<class> will also need to be
+specified.
+
+=item class
+
+Specifies the class of the data to preload.  This is only necessary if
+C<alias> is provided, and C<name> is not the name of a column which
+provides C<refers_to> information.
 
-Right now, in order to make this work, you need to do an explicit join between your primary table and the subsidiary tables AND then specify the name of the attribute you want to prefetch related data into.
-This method could be a LOT smarter. since we already know what the relationships between our tables are, that could all be precomputed.
+=back
 
-XXX TODO: in the future, this API should be extended to let you specify columns.
+For backwards compatibility, C<prefetch> can instead be called with
+C<alias> and C<name> as its two arguments, instead of a paramhash.
 
 =cut
 
 sub prefetch {
-    my $self           = shift;
-    my $alias          = shift;
-    my $into_attribute = shift;
+    my $self = shift;
+
+    # Back-compat
+    if ( @_ and $self->{joins}{ $_[0] } ) {
+
+        # First argument appears to be an alias
+        @_ = ( alias => $_[0], name => $_[1] );
+    }
+
+    my %args = (
+        alias     => undef,
+        name      => undef,
+        class     => undef,
+        reference => undef,
+        @_,
+    );
+
+    die "Must at least provide name to prefetch"
+        unless $args{name};
 
-    my $preload_related = $self->preload_related() || {};
+    # Reference defaults to name
+    $args{reference} ||= $args{name};
 
-    $preload_related->{$alias} = $into_attribute;
+    # If we don't have an alias, do the join
+    if ( not $args{alias} ) {
+        my ( $class, @columns )
+            = $self->find_class( split /\./, $args{reference} );
+        $args{class} = ref $class;
+        ( $args{alias} ) = $self->resolve_join(@columns);
+    }
+
+    if ( not $args{class} ) {
+
+        # Check the column
+        my $column = $self->new_item->column( $args{name} );
+        $args{class} = $column->refers_to if $column;
+
+        die "Don't know class" unless $args{class};
+    }
+
+    # Check that the class is a Jifty::DBI::Record or Jifty::DBI::Collection
+    unless ( UNIVERSAL::isa( $args{class}, "Jifty::DBI::Record" )
+        or UNIVERSAL::isa( $args{class}, "Jifty::DBI::Collection" ) )
+    {
+        warn
+            "Class ($args{class}) isn't a Jifty::DBI::Record or Jifty::DBI::Collection";
+        return undef;
+    }
 
-    $self->preload_related($preload_related);
+    $self->prefetch_related( {} ) unless $self->prefetch_related;
+    $self->prefetch_related->{ $args{alias} } = {};
+    $self->prefetch_related->{ $args{alias} }{$_} = $args{$_}
+        for qw/alias class name/;
 
+    # Return the alias, in case we made it
+    return $args{alias};
+}
+
+=head2 find_column NAMES
+
+Tales a chained list of column names, where all but the last element
+is the name of a column on the previous class which refers to the next
+collection or record.  Returns a list of L<Jifty::DBI::Column> objects
+for the list.
+
+=cut
+
+sub find_column {
+    my $self  = shift;
+    my @names = @_;
+
+    my $last = pop @names;
+    my ( $class, @columns ) = $self->find_class(@names);
+    $class = $class->new_item
+        if UNIVERSAL::isa( $class, "Jifty::DBI::Collection" );
+    my $column = $class->column($last);
+    die "$class has no column '$last'" unless $column;
+    return @columns, $column;
+}
+
+=head2 find_class NAMES
+
+Tales a chained list of column names, where each element is the name
+of a column on the previous class which refers to the next collection
+or record.  Returns an instance of the ending class, followed by the
+list of L<Jifty::DBI::Column> objects traversed to get there.
+
+=cut
+
+sub find_class {
+    my $self  = shift;
+    my @names = @_;
+
+    my @res;
+    my $object = $self;
+    my $item   = $self->new_item;
+    while ( my $name = shift @names ) {
+        my $column = $item->column($name);
+        die "$item has no column '$name'" unless $column;
+
+        push @res, $column;
+
+        my $classname = $column->refers_to;
+        unless ($classname) {
+            die "column '$name' of $item is not a reference";
+        }
+
+        if ( UNIVERSAL::isa( $classname, 'Jifty::DBI::Collection' ) ) {
+            $object = $classname->new( $self->_new_collection_args );
+            $item   = $object->new_item;
+        } elsif ( UNIVERSAL::isa( $classname, 'Jifty::DBI::Record' ) ) {
+            $object = $item = $classname->new( $self->_new_record_args );
+        } else {
+            die
+                "Column '$name' refers to '$classname' which is not record or collection";
+        }
+    }
+
+    return $object, @res;
+}
+
+=head2 resolve_join COLUMNS
+
+Takes a chained list of L<Jifty::DBI::Column> objects, and performs
+the requisite joins to join all of them.  Returns the alias of the
+last join.
+
+=cut
+
+sub resolve_join {
+    my $self  = shift;
+    my @chain = @_;
+
+    my $last_alias = 'main';
+
+    foreach my $column (@chain) {
+        my $name = $column->name;
+
+        my $classname = $column->refers_to;
+        unless ($classname) {
+            die "column '$name' of is not a reference";
+        }
+
+        if ( UNIVERSAL::isa( $classname, 'Jifty::DBI::Collection' ) ) {
+            my $item
+                = $classname->new( $self->_new_collection_args )->new_item;
+            my $right_alias = $self->new_alias($item);
+            $self->join(
+                type    => 'left',
+                alias1  => $last_alias,
+                column1 => 'id',
+                alias2  => $right_alias,
+                column2 => $column->by || 'id',
+                is_distinct => 1,
+            );
+            $last_alias = $right_alias;
+        } elsif ( UNIVERSAL::isa( $classname, 'Jifty::DBI::Record' ) ) {
+            my $item        = $classname->new( $self->_new_record_args );
+            my $right_alias = $self->new_alias($item);
+            $self->join(
+                type    => 'left',
+                alias1  => $last_alias,
+                column1 => $name,
+                alias2  => $right_alias,
+                column2 => $column->by || 'id',
+                is_distinct => 1,
+            );
+            $last_alias = $right_alias;
+        } else {
+            die
+                "Column '$name' refers to '$classname' which is not record or collection";
+        }
+    }
+    return $last_alias;
 }
 
 =head2 distinct_required
@@ -603,7 +808,7 @@
 
 sub distinct_required {
     my $self = shift;
-    return( $self->_is_joined ? 1 : 0 );
+    return ( $self->_is_joined ? !$self->_is_distinctly_joined : 0 );
 }
 
 =head2 build_select_count_query
@@ -616,6 +821,8 @@
 sub build_select_count_query {
     my $self = shift;
 
+    return "" if $self->derived;
+
     my $query_string = $self->_build_joins . " ";
 
     if ( $self->_is_limited ) {
@@ -645,6 +852,7 @@
 
 sub do_search {
     my $self = shift;
+    return              if $self->derived;
     $self->_do_search() if $self->{'must_redo_search'};
 
 }
@@ -664,8 +872,7 @@
 
     my $item = $self->peek;
 
-    if ( $self->{'itemscount'} < $self->_record_count )
-    {
+    if ( $self->{'itemscount'} < $self->_record_count ) {
         $self->{'itemscount'}++;
     } else {    #we've gone through the whole list. reset the count.
         $self->goto_first_item();
@@ -770,7 +977,6 @@
 =head2 new_item
 
 Should return a new object of the correct type for the current collection.
-Must be overridden by a subclassed.
 
 =cut
 
@@ -781,8 +987,9 @@
     die "Jifty::DBI::Collection needs to be subclassed; override new_item\n"
         unless $class;
 
+    warn "$self $class\n" if $class =~ /::$/;
     $class->require();
-    return $class->new( handle => $self->_handle );
+    return $class->new( $self->_new_record_args );
 }
 
 =head2 record_class
@@ -808,10 +1015,11 @@
         $self->{record_class} = shift if (@_);
         $self->{record_class} = ref $self->{record_class}
             if ref $self->{record_class};
-    } elsif ( not $self->{record_class} ) {
-        my $class = ref($self);
-        $class =~ s/Collection$//
-            or die "Can't guess record class from $class";
+    } elsif ( not ref $self or not $self->{record_class} ) {
+        my $class = ref($self) || $self;
+        $class =~ s/(Collection|s)$//
+            || die "Can't guess record class from $class";
+        return $class unless ref $self;
         $self->{record_class} = $class;
     }
     return $self->{record_class};
@@ -855,7 +1063,6 @@
 
 =cut
 
-
 sub find_all_rows {
     my $self = shift;
     $self->_is_limited(-1);
@@ -917,8 +1124,19 @@
 
 ENDSWITH is like LIKE, except it prepends a % to the beginning of the string
 
+=item "IN"
+
+IN matches a column within a set of values.  The value specified in the limit
+should be an array reference of values.
+
 =back
 
+=item escape
+
+If you need to escape wildcard characters (usually _ or %) in the value *explicitly* with 
+"ESCAPE", set the  escape character here. Note that backslashes may require special treatment 
+(e.g. Postgres dislikes \ or \\ in queries unless we use the E'' syntax).
+
 =item entry_aggregator 
 
 Can be AND or OR (or anything else valid to aggregate two clauses in SQL)
@@ -944,20 +1162,33 @@
         entry_aggregator => 'or',
         case_sensitive   => undef,
         operator         => '=',
+        escape           => undef,
         subclause        => undef,
         leftjoin         => undef,
         @_    # get the real argumentlist
     );
 
-    # We need to be passed a column and a value, at very least
-    croak "Must provide a column to limit"
-        unless defined $args{column};
-    croak "Must provide a value to limit to"
-        unless defined $args{value};
+    return if $self->derived;
 
     # make passing in an object DTRT
-    if ( ref( $args{value} ) && $args{value}->isa('Jifty::DBI::Record') ) {
-        $args{value} = $args{value}->id;
+    my $value_ref = ref( $args{value} );
+    if ($value_ref) {
+        if ( ( $value_ref ne 'ARRAY' )
+            && $args{value}->isa('Jifty::DBI::Record') )
+        {
+            $args{value} = $args{value}->id;
+        } elsif ( $value_ref eq 'ARRAY' ) {
+
+            # Don't modify the original reference, it isn't polite
+            $args{value} = [ @{ $args{value} } ];
+            map {
+                $_ = (
+                      ( ref $_ && $_->isa('Jifty::DBI::Record') )
+                    ? ( $_->id )
+                    : $_
+                    )
+            } @{ $args{value} };
+        }
     }
 
     #since we're changing the search criteria, we need to redo the search
@@ -967,11 +1198,11 @@
 
         #If it's a like, we supply the %s around the search term
         if ( $args{'operator'} =~ /MATCHES/i ) {
-            $args{'value'}    = "%" . $args{'value'} . "%";
+            $args{'value'} = "%" . $args{'value'} . "%";
         } elsif ( $args{'operator'} =~ /STARTSWITH/i ) {
-            $args{'value'}    = $args{'value'} . "%";
+            $args{'value'} = $args{'value'} . "%";
         } elsif ( $args{'operator'} =~ /ENDSWITH/i ) {
-            $args{'value'}    = "%" . $args{'value'};
+            $args{'value'} = "%" . $args{'value'};
         }
         $args{'operator'} =~ s/(?:MATCHES|ENDSWITH|STARTSWITH)/LIKE/i;
 
@@ -979,21 +1210,17 @@
         # we're doing an IS or IS NOT (null), don't quote the operator.
 
         if ( $args{'quote_value'} && $args{'operator'} !~ /IS/i ) {
-            my $tmp = $self->_handle->dbh->quote( $args{'value'} );
-
-            # Accomodate DBI drivers that don't understand UTF8
-            if ( $] >= 5.007 ) {
-                require Encode;
-                if ( Encode::is_utf8( $args{'value'} ) ) {
-                    Encode::_utf8_on($tmp);
-                }
+            if ( $value_ref eq 'ARRAY' ) {
+                map { $_ = $self->_quote_value($_) } @{ $args{'value'} };
+            } else {
+                $args{'value'} = $self->_quote_value( $args{'value'} );
             }
-            $args{'value'} = $tmp;
         }
     }
 
-    #TODO: $args{'value'} should take an array of values and generate
-    # the proper where clause.
+    if ( $args{'escape'} ) {
+        $args{'escape'} = 'ESCAPE ' . $self->_quote_value( $args{escape} );
+    }
 
     #If we're performing a left join, we really want the alias to be the
     #left join criterion.
@@ -1006,7 +1233,7 @@
 
     # {{{ if there's no alias set, we need to set it
 
-    unless ( $args{'alias'} ) {
+    unless ( defined $args{'alias'} ) {
 
         #if the table we're looking at is the same as the main table
         if ( $args{'table'} eq $self->table ) {
@@ -1028,30 +1255,41 @@
     # Set this to the name of the column and the alias, unless we've been
     # handed a subclause name
 
-    my $qualified_column = $args{'alias'} . "." . $args{'column'};
+    my $qualified_column
+        = $args{'alias'}
+        ? $args{'alias'} . "." . $args{'column'}
+        : $args{'column'};
     my $clause_id = $args{'subclause'} || $qualified_column;
 
     # If we're trying to get a leftjoin restriction, lets set
-    # $restriction to point htere. otherwise, lets construct normally
+    # $restriction to point there. otherwise, lets construct normally
 
     my $restriction;
     if ( $args{'leftjoin'} ) {
-        $restriction = $self->{'leftjoins'}{ $args{'leftjoin'} }{'criteria'}
-            { $clause_id } ||= [];
+        $restriction
+            = $self->{'joins'}{ $args{'leftjoin'} }{'criteria'}{$clause_id}
+            ||= [];
     } else {
-        $restriction = $self->{'restrictions'}{ $clause_id } ||= [];
+        $restriction = $self->{'restrictions'}{$clause_id} ||= [];
     }
 
     # If it's a new value or we're overwriting this sort of restriction,
 
+    # XXX: when is column_obj undefined?
+    my $class = $self->{joins}{$args{alias}} && $self->{joins}{$args{alias}}{class} 
+      ? $self->{joins}{$args{alias}}{class}->new($self->_new_collection_args)
+      : $self;
+    my $column_obj = $class->new_item()->column( $args{column} );
+    my $case_sensitive = $column_obj ? $column_obj->case_sensitive : 0;
+    $case_sensitive = $args{'case_sensitive'}
+        if defined $args{'case_sensitive'};
     if (   $self->_handle->case_sensitive
         && defined $args{'value'}
         && $args{'quote_value'}
-        && !$args{'case_sensitive'} )
+        && !$case_sensitive )
     {
 
         # don't worry about case for numeric columns_in_db
-        my $column_obj = $self->new_item()->column( $args{column} );
         if ( defined $column_obj ? $column_obj->is_string : 1 ) {
             ( $qualified_column, $args{'operator'}, $args{'value'} )
                 = $self->_handle->_make_clause_case_insensitive(
@@ -1059,22 +1297,31 @@
         }
     }
 
+    if ( $value_ref eq 'ARRAY' ) {
+        croak
+            'Limits with an array ref are only allowed with operator \'IN\' or \'=\''
+            unless $args{'operator'} =~ /^(IN|=)$/i;
+        $args{'value'} = '( ' . join( ',', @{ $args{'value'} } ) . ' )';
+        $args{'operator'} = 'IN';
+    }
+
     my $clause = {
         column   => $qualified_column,
         operator => $args{'operator'},
         value    => $args{'value'},
+        escape   => $args{'escape'},
     };
 
     # Juju because this should come _AFTER_ the EA
     my @prefix;
-    if ( $self->{'_open_parens'}{ $clause_id } ) {
-        @prefix = ('(') x delete $self->{'_open_parens'}{ $clause_id };
+    if ( $self->{'_open_parens'}{$clause_id} ) {
+        @prefix = ('(') x delete $self->{'_open_parens'}{$clause_id};
     }
 
     if ( lc( $args{'entry_aggregator'} || "" ) eq 'none' || !@$restriction ) {
-        @$restriction = (@prefix, $clause);
+        @$restriction = ( @prefix, $clause );
     } else {
-        push @$restriction, $args{'entry_aggregator'}, @prefix , $clause;
+        push @$restriction, $args{'entry_aggregator'}, @prefix, $clause;
     }
 
     # We're now limited. people can do searches.
@@ -1116,7 +1363,7 @@
 # Immediate Action
 sub close_paren {
     my ( $self, $clause ) = @_;
-    my $restriction = $self->{'restrictions'}{ $clause } ||= [];
+    my $restriction = $self->{'restrictions'}{$clause} ||= [];
     push @$restriction, ')';
 }
 
@@ -1142,7 +1389,8 @@
     #Go through all restriction types. Build the where clause from the
     #Various subclauses.
 
-    my @subclauses = grep defined && length, values %{ $self->{'subclauses'} };
+    my @subclauses = grep defined && length,
+        values %{ $self->{'subclauses'} };
 
     $where_clause = " WHERE " . CORE::join( ' AND ', @subclauses )
         if (@subclauses);
@@ -1158,22 +1406,23 @@
 
     delete $self->{'subclauses'}{'generic_restrictions'};
 
-    # Go through all the restrictions of this type. Buld up the generic subclause
+ # Go through all the restrictions of this type. Buld up the generic subclause
     my $result = '';
-    foreach my $restriction ( grep $_ && @$_, values %{ $self->{'restrictions'} } ) {
+    foreach my $restriction ( grep $_ && @$_,
+        values %{ $self->{'restrictions'} } )
+    {
         $result .= ' AND ' if $result;
         $result .= '(';
-        foreach my $entry ( @$restriction ) {
+        foreach my $entry (@$restriction) {
             unless ( ref $entry ) {
-                $result .= ' '. $entry . ' ';
-            }
-            else {
-                $result .= join ' ', @{$entry}{qw(column operator value)};
+                $result .= ' ' . $entry . ' ';
+            } else {
+                $result .= join ' ', grep { defined } @{$entry}{qw(column operator value escape)};
             }
         }
         $result .= ')';
     }
-    return ($self->{'subclauses'}{'generic_restrictions'} = $result);
+    return ( $self->{'subclauses'}{'generic_restrictions'} = $result );
 }
 
 # set $self->{$type .'_clause'} to new value
@@ -1188,6 +1437,24 @@
     $self->{$type} = $value;
 }
 
+# quote the search value
+sub _quote_value {
+    my $self = shift;
+    my ($value) = @_;
+
+    my $tmp = $self->_handle->dbh->quote($value);
+
+    # Accomodate DBI drivers that don't understand UTF8
+    if ( $] >= 5.007 ) {
+        require Encode;
+        if ( Encode::is_utf8($tmp) ) {
+            Encode::_utf8_on($tmp);
+        }
+    }
+    return $tmp;
+
+}
+
 =head2 order_by_cols DEPRECATED
 
 *DEPRECATED*. Use C<order_by> method.
@@ -1225,6 +1492,7 @@
 
 sub order_by {
     my $self = shift;
+    return if $self->derived;
     if (@_) {
         my @args = @_;
 
@@ -1234,7 +1502,7 @@
         $self->{'order_by'} = \@args;
         $self->redo_search();
     }
-    return ( $self->{'order_by'} || []);
+    return ( $self->{'order_by'} || [] );
 }
 
 =head2 _order_clause
@@ -1268,7 +1536,7 @@
             $clause .= $rowhash{'function'} . ' ';
             $clause .= $rowhash{'order'};
 
-        } elsif ( (defined $rowhash{'alias'} )
+        } elsif ( ( defined $rowhash{'alias'} )
             and ( $rowhash{'column'} ) )
         {
 
@@ -1316,6 +1584,7 @@
 sub group_by {
     my $self = shift;
 
+    return if $self->derived;
     my @args = @_;
 
     unless ( UNIVERSAL::isa( $args[0], 'HASH' ) ) {
@@ -1387,17 +1656,20 @@
 
     my $alias = $self->_get_alias($table);
 
-    my $subclause = "$table $alias";
-
-    push( @{ $self->{'aliases'} }, $subclause );
+    $self->{'joins'}{$alias} = {
+        alias        => $alias,
+        table        => $table,
+        type         => 'CROSS',
+        alias_string => " CROSS JOIN $table $alias ",
+    };
 
     return $alias;
 }
 
 # _get_alias is a private function which takes an tablename and
-# returns a new alias for that table without adding something
-# to self->{'aliases'}.  This function is used by new_alias
-# and the as-yet-unnamed left join code
+# returns a new alias for that table without adding something to
+# self->{'joins'}.  This function is used by new_alias and the
+# as-yet-unnamed left join code
 
 sub _get_alias {
     my $self  = shift;
@@ -1429,6 +1701,10 @@
 The parameter C<operator> defaults C<=>, but you can specify other
 operators to join with.
 
+Passing a true value for the C<is_distinct> parameter allows one to
+specify that, despite the join, the original table's rows are will all
+still be distinct.
+
 Instead of C<alias1>/C<column1>, it's possible to specify expression, to join
 C<alias2>/C<table2> on an arbitrary expression.
 
@@ -1446,6 +1722,7 @@
         @_
     );
 
+    return if $self->derived;
     $self->_handle->join( collection => $self, %args );
 
 }
@@ -1464,17 +1741,20 @@
 sub set_page_info {
     my $self = shift;
     my %args = (
-        per_page     => undef,
-        current_page => undef,    # 1-based
+        per_page     => 0,
+        current_page => 1,    # 1-based
         @_
     );
+    return if $self->derived;
 
-    $self->pager->total_entries( $self->count_all )
+    $self->pager->total_entries( lazy { $self->count_all } )
         ->entries_per_page( $args{'per_page'} )
         ->current_page( $args{'current_page'} );
 
     $self->rows_per_page( $args{'per_page'} );
-    $self->first_row( $self->pager->first || 1 );
+    # We're not using $pager->first because it automatically does a count_all 
+    # to correctly return '0' for empty collections
+    $self->first_row(($args{'current_page'} - 1) * $args{'per_page'} + 1);
 
 }
 
@@ -1717,7 +1997,7 @@
 
     my $column = "col" . @{ $self->{columns} ||= [] };
     $column = $args{column} if $table eq $self->table and !$args{alias};
-    $column = ($args{'alias'}||'main')."_".$column;
+    $column = ( $args{'alias'} || 'main' ) . "_" . $column;
     push @{ $self->{columns} }, "$name AS \L$column";
     return $column;
 }
@@ -1807,8 +2087,8 @@
 
     $obj->redo_search();    # clean out the object of data
 
-    $obj->{$_} = Clone::clone( $obj->{$_} ) for
-        grep exists $self->{ $_ }, $self->_cloned_attributes;
+    $obj->{$_} = Clone::clone( $obj->{$_} )
+        for grep exists $self->{$_}, $self->_cloned_attributes;
     return $obj;
 }
 
@@ -1824,8 +2104,7 @@
 
 sub _cloned_attributes {
     return qw(
-        aliases
-        leftjoins
+        joins
         subclauses
         restrictions
     );
@@ -1855,7 +2134,12 @@
 
 =head1 AUTHOR
 
-Copyright (c) 2001-2005 Jesse Vincent, jesse at fsck.com.
+Jesse Vincent <jesse at bestpractical.com>, Alex Vandiver
+<alexmv at bestpractical.com>, Ruslan Zakirov <ruslan.zakirov at gmail.com>
+
+Based on DBIx::SearchBuilder::Collection, whose credits read:
+
+ Jesse Vincent, <jesse at fsck.com> 
 
 All rights reserved.
 
@@ -1865,7 +2149,6 @@
 
 =head1 SEE ALSO
 
-Jifty::DBI::Handle, Jifty::DBI::Record.
+L<Jifty::DBI>, L<Jifty::DBI::Handle>, L<Jifty::DBI::Record>.
 
 =cut
-

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Column.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Column.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Column.pm	Mon Dec 10 17:40:42 2007
@@ -8,35 +8,51 @@
 use UNIVERSAL::require;
 use version;
 
-
-
-our @ATTRS = qw/
-name
+my @attrs = qw/
+    name
     type
     default
     readable writable
     max_length
     mandatory
     virtual
-    container
     distinct
     sort_order
     refers_to by
     alias_for_column
     aliased_as
     since till
+    indexed
+    _validator
+    _checked_for_validate_sub
+    record_class
+    attributes
+    case_sensitive
+    /;
 
+# these actually live in the attributes hash
+my @handy_attrs = qw/
+    container
     label hints render_as
     valid_values
     available_values
-    indexed
     autocompleted
-    _validator
-    _checked_for_validate_sub
-    record_class
     /;
 
-__PACKAGE__->mk_accessors(@ATTRS);
+# compat: this should probably never exist and be deprecated
+our @ATTRS = (@attrs, @handy_attrs);
+
+__PACKAGE__->mk_accessors(@attrs);
+
+for my $attr (@handy_attrs) {
+    no strict 'refs';
+    *$attr = sub {
+        my $self = shift;
+	$self->attributes({}) unless $self->attributes;
+        return $self->attributes->{$attr} unless @_;
+        $self->attributes->{$attr} = (@_ == 1 ? $_[0] : [@_]);
+    }
+}
 
 =head1 NAME
 
@@ -54,6 +70,26 @@
 
 =cut
 
+=head2 new
+
+=cut
+
+sub new {
+    my ($class, $args) = @_;
+    my $self = $class->SUPER::new({});
+
+    # run through accessors, push unknown keys into the attributes hash
+
+    # XXX: we might want to construct the proper hash (lifting things
+    # not in @attrs into attributes and just pass the whole hash
+    $self->attributes({});
+    for (keys %$args) {
+	$self->can($_) ? $self->$_($args->{$_}) : $self->attributes->{$_} = $args->{$_};
+    }
+
+    return $self;
+}
+
 =head2 is_numeric
 
 Returns true if the column is of some numeric type, otherwise returns false.
@@ -89,17 +125,46 @@
 =head2 serialize_metadata
 
 Returns a hash describing this column object with enough detail to
-fully describe it in the database.  Intentionally skips C<record_class>,
-all column attributes starting with C<_>, and all column attributes
-which are undefined.
+fully describe it in the database.  Intentionally skips
+C<record_class>, all column attributes starting with C<_>, and all
+column attributes which are undefined.  The "known" attributes in the
+C<attributes> hash are flattened and returned as well.  The list of
+known attributes are:
+
+=over
+
+=item     container
+
+=item     label hints render_as
+
+=item     valid_values
+
+=item     available_values
+
+=item     autocompleted
+
+=back
 
 =cut
 
 sub serialize_metadata {
     my $self = shift;
-    return {map { $_ => $self->$_() } grep { $_ ne 'record_class' && $_ !~ /^_/ && defined $self->$_}  @ATTRS};
+    return {map { $_ => $self->$_() } grep { $_ ne 'attributes' && $_ ne 'record_class' && $_ !~ /^_/ && defined $self->$_}
+            @ATTRS};
+}
 
+=head2 serialize_metadata2
 
+Returns a hash describing this column object with enough detail to
+fully describe it in the database.  Intentionally skips C<record_class>,
+all column attributes starting with C<_>, and all column attributes
+which are undefined.
+
+=cut
+
+sub serialize_metadata2 {
+    my $self = shift;
+    return {map { $_ => $self->$_() } grep { $_ ne 'record_class' && $_ !~ /^_/ && defined $self->$_} @attrs};
 }
 
 =head2 validator
@@ -129,6 +194,14 @@
 *read  = \&readable;
 *write = \&writable;
 
+=head2 read
+
+DEPRECATED.  Use C<< $column->readable >> instead.
+
+=head2 write
+
+DEPRECATED.  Use C<< $column->writable >> instead.
+
 =head2 length
 
 DEPRECATED.  Use C<< $column->max_length >> instead.

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter.pm	Mon Dec 10 17:40:42 2007
@@ -109,8 +109,8 @@
 
 C<decode> takes data that the database is handing back to us and gets
 it into a form that's OK to hand back to the user. This could be
-anything from flattening a L<DateTime> object into an ISO date to
-making sure that data is utf8 clean.
+anything from inflating an ISO date to a L<DateTime> object to
+making sure that the string properly has the utf8 flag.
 
 =cut
 

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter/DateTime.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter/DateTime.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter/DateTime.pm	Mon Dec 10 17:40:42 2007
@@ -3,15 +3,15 @@
 use warnings;
 use strict;
 
-use base qw|Jifty::DBI::Filter|;
+use base qw|Jifty::DBI::Filter Class::Data::Inheritable|;
 use DateTime                  ();
 use DateTime::Format::ISO8601 ();
 use DateTime::Format::Strptime ();
 use Carp ();
 
-use constant _time_zone => '';
+use constant _time_zone => 'UTC';
 use constant _strptime  => '%Y-%m-%d %H:%M:%S';
-
+use constant _parser    => DateTime::Format::ISO8601->new();
 
 =head1 NAME
 
@@ -23,6 +23,30 @@
 plain text dates.  If the column type is "date", then the hour,
 minute, and second information is discarded when encoding.
 
+Both input and output will always be coerced into UTC (or, in the case of
+Dates, the Floating timezone) for consistency.
+
+=head2 formatter
+
+This is an instance of the DateTime::Format object used for inflating the
+string in the database to a DateTime object. By default it is a
+L<DateTime::Format::Strptime> object that uses the C<_strptime> method as its
+pattern.
+
+You can use the _formatter classdata storage as a cache so you don't need
+to re-instantiate your format object every C<decode>.
+
+=cut
+
+__PACKAGE__->mk_classdata("_formatter");
+sub formatter {
+    my $self = shift;
+    unless ($self->_formatter) {
+         $self->_formatter(DateTime::Format::Strptime->new(pattern => $self->_strptime));
+    }
+    return $self->_formatter;
+}
+
 =head2 encode
 
 If value is DateTime object then converts it into ISO format
@@ -49,8 +73,8 @@
 
     return unless $$value_ref;
     if (my $tz = $self->_time_zone) {
-	$$value_ref = $$value_ref->clone;
-	$$value_ref->time_zone('floating');
+        $$value_ref = $$value_ref->clone;
+        $$value_ref->set_time_zone($tz);
     }
     $$value_ref = $$value_ref->strftime($self->_strptime);
     return 1;
@@ -78,7 +102,7 @@
 
     my $str = join('T', split ' ', $$value_ref, 2);
     my $dt;
-    eval { $dt  = DateTime::Format::ISO8601->parse_datetime($str) };
+    eval { $dt  = $self->_parser->parse_datetime($str) };
 
     if ($@) { # if datetime can't decode this, scream loudly with a useful error message
         Carp::cluck($@);
@@ -87,9 +111,9 @@
 
     if ($dt) {
 	my $tz = $self->_time_zone;
-	$dt->time_zone($tz) if $tz;
+	$dt->set_time_zone($tz) if $tz;
 
-        $dt->set_formatter(DateTime::Format::Strptime->new(pattern => $self->_strptime));
+        $dt->set_formatter($self->formatter);
         $$value_ref = $dt;
     } else {
         return;

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter/YAML.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter/YAML.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Filter/YAML.pm	Mon Dec 10 17:40:42 2007
@@ -8,7 +8,10 @@
 
 eval "use YAML::Syck ()";
 if ($@) { 
-    use YAML (); 
+    # We don't actually need to "use", which is checked at compile 
+    # time and would cause error when YAML is not installed.
+    # Or, eval this, too?
+    require YAML;
     $Dump = \&YAML::Dump;
     $Load = \&YAML::Load;
 }

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle.pm	Mon Dec 10 17:40:42 2007
@@ -13,13 +13,16 @@
 
 our $VERSION = '0.01';
 
-if (my $pattern = $ENV{JIFTY_DBQUERY_CALLER}) {
+if ( my $pattern = $ENV{JIFTY_DBQUERY_CALLER} ) {
     require Hook::LexWrap;
-    Hook::LexWrap::wrap('Jifty::DBI::Handle::simple_query', pre => sub {
-        return unless $_[1] =~ m/$pattern/;
-        warn $_[1].'   '.join(',', @_[2..$#_])."\n";
-        Carp::cluck;
-    });
+    Hook::LexWrap::wrap(
+        'Jifty::DBI::Handle::simple_query',
+        pre => sub {
+            return unless $_[1] =~ m/$pattern/;
+            warn $_[1] . '   ' . CORE::join( ',', @_[ 2 .. $#_ ] ) . "\n";
+            Carp::cluck;
+        }
+    );
 }
 
 =head1 NAME
@@ -188,12 +191,15 @@
         @_
     );
 
-
     my $driver = delete $args{'driver'};
     $args{'dbname'} ||= delete $args{'database'};
 
-    $self->{'dsn'} =
-    "dbi:$driver:" . join(';', map { $_ ."=".$args{$_} } grep { defined $args{$_} } keys %args);
+    delete $args{'user'};
+    delete $args{'password'};
+
+    $self->{'dsn'} = "dbi:$driver:"
+        . CORE::join( ';',
+        map { $_ . "=" . $args{$_} } grep { defined $args{$_} } keys %args );
 }
 
 =head2 dsn
@@ -249,7 +255,31 @@
     return ( $self->{'_dologsql'} );
 }
 
-=head2 _log_sql_statement STATEMENT DURATION
+=head2 log_sql_hook NAME [, CODE]
+
+Used in instrumenting the SQL logging. You can use this to, for example, get a
+stack trace for each query (so you can find out where the query is being made).
+The name is required so that multiple hooks can be installed without stepping
+on eachother's toes.
+
+The coderef is run in scalar context and (currently) receives no arguments.
+
+If you don't pass CODE in, then the coderef currently assigned under
+NAME is returned.
+
+=cut
+
+sub log_sql_hook {
+    my $self = shift;
+    my $name = shift;
+
+    if (@_) {
+        $self->{'_logsqlhooks'}{$name} = shift;
+    }
+    return ( $self->{'_logsqlhooks'}{$name} );
+}
+
+=head2 _log_sql_statement STATEMENT DURATION BINDINGS
 
 add an SQL statement to our query log
 
@@ -260,8 +290,14 @@
     my $statement = shift;
     my $duration  = shift;
     my @bind      = @_;
+
+    my $results = {};
+    while (my ($name, $code) = each %{ $self->{'_logsqlhooks'} || {} }) {
+        $results->{$name} = $code->();
+    }
+
     push @{ $self->{'StatementLog'} },
-        ( [ Time::HiRes::time(), $statement, [@bind], $duration ] );
+        ( [ Time::HiRes::time(), $statement, [@bind], $duration, $results ] );
 
 }
 
@@ -278,9 +314,12 @@
 
 =head2 sql_statement_log
 
-Returns the current SQL statement log as an array of arrays. Each entry is a list of 
+Returns the current SQL statement log as an array of arrays. Each entry is a list of:
+
+(Time, Statement, [Bindings], Duration, {HookResults})
 
-(Time, Statement, [Bindings], Duration)
+Bindings is an arrayref of the values of any placeholders. HookResults is a
+hashref keyed by hook name.
 
 =cut
 
@@ -346,7 +385,7 @@
 
     my @bind  = ();
     my $where = 'WHERE ';
-    while (my $key = shift @pairs) {
+    while ( my $key = shift @pairs ) {
         $where .= $key . "=?" . " AND ";
         push( @bind, shift(@pairs) );
     }
@@ -364,7 +403,7 @@
 
 sub insert {
     my ( $self, $table, @pairs ) = @_;
-    my ( @cols, @vals,  @bind );
+    my ( @cols, @vals, @bind );
 
 #my %seen; #only the *first* value is used - allows drivers to specify default
     while ( my $key = shift @pairs ) {
@@ -376,7 +415,8 @@
         push @bind, $value;
     }
 
-    my $query_string = "INSERT INTO $table ("
+    my $query_string
+        = "INSERT INTO $table ("
         . CORE::join( ", ", @cols )
         . ") VALUES " . "("
         . CORE::join( ", ", @vals ) . ")";
@@ -407,7 +447,7 @@
         @_
     );
 
-    return 1 unless grep {defined} values %{$args{primary_keys}};
+    return 1 unless grep {defined} values %{ $args{primary_keys} };
 
     my @bind  = ();
     my $query = 'UPDATE ' . $args{'table'} . ' ';
@@ -523,8 +563,9 @@
                 . $self->dbh->errstr . "\n";
 
         } else {
-            # XXX: This warn doesn't show up because we mask logging in Jifty::Test::END.
-            # and it usually fails because the test server is still running.
+
+ # XXX: This warn doesn't show up because we mask logging in Jifty::Test::END.
+ # and it usually fails because the test server is still running.
             warn "$self couldn't execute the query '$query_string'";
 
             my $ret = Class::ReturnValue->new();
@@ -609,7 +650,7 @@
         my $ver = '';
         $ver = ( $sth->fetchrow_arrayref->[0] || '' ) if $sth;
         $ver =~ /(\d+(?:\.\d+)*(?:-[a-z0-9]+)?)/i;
-        $self->{'database_version'}       = $ver;
+        $self->{'database_version'} = $ver;
         $self->{'database_version_short'} = $1 || $ver;
 
         $self->raise_error($re);
@@ -648,10 +689,11 @@
     my $value    = shift;
 
     return $value ne ''
-      && $value   ne "''"
-      && ( $operator !~ /IS/ && $value !~ /^null$/i )
-      # don't downcase integer values
-      && $value !~ /^['"]?\d+['"]?$/;
+        && $value ne "''"
+        && ( $operator !~ /IS/ && $value !~ /^null$/i )
+
+        # don't downcase integer values
+        && $value !~ /^['"]?\d+['"]?$/;
 }
 
 sub _make_clause_case_insensitive {
@@ -660,9 +702,13 @@
     my $operator = shift;
     my $value    = shift;
 
-    if ($self->_case_insensitivity_valid($column, $operator, $value)) {
-      $column = "lower($column)";
-      $value  = "lower($value)";
+    if ( $self->_case_insensitivity_valid( $column, $operator, $value ) ) {
+        $column = "lower($column)";
+        if ( ref $value eq 'ARRAY' ) {
+            map { $_ = "lower($_)" } @{$value};
+        } else {
+            $value = "lower($value)";
+        }
     }
     return ( $column, $operator, $value );
 }
@@ -806,237 +852,214 @@
 
     my $self = shift;
     my %args = (
-        collection => undef,
-        type       => 'normal',
-        alias1     => 'main',
-        column1    => undef,
-        table2     => undef,
-        alias2     => undef,
-        column2    => undef,
-        expression => undef,
-        operator   => '=',
+        collection  => undef,
+        type        => 'normal',
+        alias1      => 'main',
+        column1     => undef,
+        table2      => undef,
+        alias2      => undef,
+        column2     => undef,
+        expression  => undef,
+        operator    => '=',
+        is_distinct => 0,
         @_
     );
 
     my $alias;
 
-    #If we're handed in an alias2, we need to go remove it from the
-    # Aliases array.  Basically, if anyone generates an alias and then
-    # tries to use it in a join later, we want to be smart about creating
-    # joins, so we need to go rip it out of the old aliases table and drop
-    # it in as an explicit join
-    if ( $args{'alias2'} ) {
-
-        # this code is slow and wasteful, but it's clear.
-        my @aliases = @{ $args{'collection'}->{'aliases'} };
-        my @new_aliases;
-        foreach my $old_alias (@aliases) {
-            if ( $old_alias =~ /^(.*?) (\Q$args{'alias2'}\E)$/ ) {
-                $args{'table2'} = $1;
-                $alias = $2;
-
-            } else {
-                push @new_aliases, $old_alias;
+    # If we're handed in a table2 as a Collection object, make notes
+    # about if the result of the join is still distinct for the
+    # calling collection
+    if ( $args{'table2'}
+        && UNIVERSAL::isa( $args{'table2'}, 'Jifty::DBI::Collection' ) )
+    {
+        my $c = ref $args{'table2'} ? $args{'table2'} : $args{'table2'}->new($args{collection}->_new_collection_args);
+        if ( $args{'operator'} eq '=' ) {
+            my $x = $c->new_item->column( $args{column2} );
+            if ( $x->type eq 'serial' || $x->distinct ) {
+                $args{'is_distinct'} = 1;
             }
         }
+        $args{'class2'} = ref $c;
+        $args{'table2'} = $c->table;
+    }
 
-# If we found an alias, great. let's just pull out the table and alias for the other item
-        unless ($alias) {
+    if ( $args{'alias2'} ) {
+        if ( $args{'collection'}{'joins'}{ $args{alias2} } and lc $args{'collection'}{'joins'}{ $args{alias2} }{type} eq "cross" ) {
+            my $join = $args{'collection'}{'joins'}{ $args{alias2} };
+            $args{'table2'} = $join->{table};
+            $alias = $join->{alias};
+        } else {
 
             # if we can't do that, can we reverse the join and have it work?
-            my $a1 = $args{'alias1'};
-            my $f1 = $args{'column1'};
-            $args{'alias1'}  = $args{'alias2'};
-            $args{'column1'} = $args{'column2'};
-            $args{'alias2'}  = $a1;
-            $args{'column2'} = $f1;
-
-            @aliases     = @{ $args{'collection'}->{'aliases'} };
-            @new_aliases = ();
-            foreach my $old_alias (@aliases) {
-                if ( $old_alias =~ /^(.*?) ($args{'alias2'})$/ ) {
-                    $args{'table2'} = $1;
-                    $alias = $2;
-
-                } else {
-                    push @new_aliases, $old_alias;
-                }
-            }
+            @args{qw/alias1 alias2/}   = @args{qw/alias2 alias1/};
+            @args{qw/column1 column2/} = @args{qw/column2 column1/};
 
-        }
+            if ( $args{'collection'}{'joins'}{ $args{alias2} } and lc $args{'collection'}{'joins'}{ $args{alias2} }{type} eq "cross" ) {
+                my $join = $args{'collection'}{'joins'}{ $args{alias2} };
+                $args{'table2'} = $join->{table};
+                $alias = $join->{alias};
+            } else {
 
-        unless ( $alias ) {
-            return $self->_normal_join(%args);
+                # Swap back
+                @args{qw/alias1 alias2/}   = @args{qw/alias2 alias1/};
+                @args{qw/column1 column2/} = @args{qw/column2 column1/};
+
+                return $self->Jifty::DBI::Collection::limit(
+                    entry_aggregator => 'AND',
+                    @_,
+                    quote_value => 0,
+                    alias       => $args{'alias1'},
+                    column      => $args{'column1'},
+                    value       => $args{'alias2'} . "." . $args{'column2'},
+                );
+            }
         }
-
-        $args{'collection'}->{'aliases'} = \@new_aliases;
-    }
-
-    else {
+    } else {
         $alias = $args{'collection'}->_get_alias( $args{'table2'} );
-
     }
 
-    my $meta = $args{'collection'}->{'leftjoins'}{ $alias } ||= {};
+    my $meta = $args{'collection'}->{'joins'}{$alias} ||= {};
+    $meta->{alias} = $alias;
     if ( $args{'type'} =~ /LEFT/i ) {
-        $meta->{'alias_string'} = " LEFT JOIN " . $args{'table2'} . " $alias ";
+        $meta->{'alias_string'}
+            = " LEFT JOIN " . $args{'table2'} . " $alias ";
         $meta->{'type'} = 'LEFT';
 
     } else {
         $meta->{'alias_string'} = " JOIN " . $args{'table2'} . " $alias ";
-        $meta->{'type'} = 'NORMAL';
+        $meta->{'type'}         = 'NORMAL';
     }
-    $meta->{'depends_on'} = $args{'alias1'};
+    $meta->{'depends_on'}       = $args{'alias1'};
+    $meta->{'is_distinct'}      = $args{'is_distinct'};
+    $meta->{'class'}            = $args{'class2'} if $args{'class2'};
     $meta->{'entry_aggregator'} = $args{'entry_aggregator'}
         if $args{'entry_aggregator'};
 
     my $criterion = $args{'expression'} || "$args{'alias1'}.$args{'column1'}";
-    $meta->{'criteria'}{ 'base_criterion' } = [{
-        column   => $criterion,
-        operator => $args{'operator'},
-        value    => "$alias.$args{'column2'}",
-    }];
-
-    return ($alias);
-}
-
-sub _normal_join {
-
-    my $self = shift;
-    my %args = (
-        collection => undef,
-        type       => 'normal',
-        column1    => undef,
-        alias1     => undef,
-        table2     => undef,
-        column2    => undef,
-        alias2     => undef,
-        operator   => '=',
-        @_
-    );
-
-    my $sb = $args{'collection'};
-
-    if ( $args{'type'} =~ /LEFT/i ) {
-        my $alias = $sb->_get_alias( $args{'table2'} );
-        my $meta  = $sb->{'leftjoins'}{ $alias } ||= {};
-        $meta->{'alias_string'} = " LEFT JOIN $args{'table2'} $alias ";
-        $meta->{'depends_on'}   = $args{'alias1'};
-        $meta->{'type'}         = 'LEFT';
-        $meta->{'base_criterion'} = [ {
-            column   => "$args{'alias1'}.$args{'column1'}",
+    $meta->{'criteria'}{'base_criterion'} = [
+        {   column   => $criterion,
             operator => $args{'operator'},
             value    => "$alias.$args{'column2'}",
-        } ];
+        }
+    ];
 
-        return ($alias);
-    } else {
-        $sb->Jifty::DBI::Collection::limit(
-            entry_aggregator => 'AND',
-            @_,
-            quote_value      => 0,
-            alias            => $args{'alias1'},
-            column           => $args{'column1'},
-            value            => $args{'alias2'} . "." . $args{'column2'},
-        );
-    }
+    return ($alias);
 }
 
 # this code is all hacky and evil. but people desperately want _something_ and I'm
 # super tired. refactoring gratefully appreciated.
 
 sub _build_joins {
-    my $self = shift;
-    my $sb   = shift;
+    my $self       = shift;
+    my $collection = shift;
 
-    $self->_optimize_joins( collection => $sb );
+    $self->_optimize_joins( collection => $collection );
 
-    my $join_clause = CORE::join " CROSS JOIN ", ($sb->table ." main"), @{ $sb->{'aliases'} };
-    my %processed = map { /^\S+\s+(\S+)$/; $1 => 1 } @{ $sb->{'aliases'} };
+    my @cross = grep { lc $_->{type} eq "cross" }
+        values %{ $collection->{'joins'} };
+    my $join_clause = ( $collection->table . " main" )
+        . CORE::join( " ", map { $_->{alias_string} } @cross );
+    my %processed = map { $_->{alias} => 1 } @cross;
     $processed{'main'} = 1;
 
-    # get a @list of joins that have not been processed yet, but depend on processed join
-    my $joins = $sb->{'leftjoins'};
-    while ( my @list = grep !$processed{ $_ }
-            && $processed{ $joins->{ $_ }{'depends_on'} }, keys %$joins )
+# get a @list of joins that have not been processed yet, but depend on processed join
+    my $joins = $collection->{'joins'};
+    while ( my @list = grep !$processed{$_}
+        && $processed{ $joins->{$_}{'depends_on'} }, keys %$joins )
     {
-        foreach my $join ( @list ) {
-            $processed{ $join }++;
+        foreach my $join (@list) {
+            $processed{$join}++;
 
-            my $meta = $joins->{ $join };
+            my $meta = $joins->{$join};
             my $aggregator = $meta->{'entry_aggregator'} || 'AND';
 
             $join_clause .= $meta->{'alias_string'} . " ON ";
             my @tmp = map {
-                    ref($_)?
-                        $_->{'column'} .' '. $_->{'operator'} .' '. $_->{'value'}:
-                        $_
+                ref($_)
+                    ? $_->{'column'} . ' '
+                    . $_->{'operator'} . ' '
+                    . $_->{'value'}
+                    : $_
                 }
-                map { ('(', @$_, ')', $aggregator) } values %{ $meta->{'criteria'} };
+                map {
+                ( '(', @$_, ')', $aggregator )
+                } values %{ $meta->{'criteria'} };
+
             # delete last aggregator
             pop @tmp;
             $join_clause .= CORE::join ' ', @tmp;
         }
     }
 
-    # here we could check if there is recursion in joins by checking that all joins
-    # are processed
-    if ( my @not_processed = grep !$processed{ $_ }, keys %$joins ) {
+# here we could check if there is recursion in joins by checking that all joins
+# are processed
+    if ( my @not_processed = grep !$processed{$_}, keys %$joins ) {
         die "Unsatisfied dependency chain in joins @not_processed";
     }
     return $join_clause;
 }
 
 sub _optimize_joins {
-    my $self = shift;
-    my %args = ( collection => undef, @_ );
-    my $joins = $args{'collection'}->{'leftjoins'};
+    my $self  = shift;
+    my %args  = ( collection => undef, @_ );
+    my $joins = $args{'collection'}->{'joins'};
 
-    my %processed = map { /^\S+\s+(\S+)$/; $1 => 1 } @{ $args{'collection'}->{'aliases'} };
-    $processed{ $_ }++ foreach grep $joins->{ $_ }{'type'} ne 'LEFT', keys %$joins;
+    my %processed;
+    $processed{$_}++
+        foreach grep {lc $joins->{$_}{'type'} ne 'left'} keys %$joins;
     $processed{'main'}++;
 
     my @ordered;
-    # get a @list of joins that have not been processed yet, but depend on processed join
-    # if we are talking about forest then we'll get the second level of the forest,
-    # but we should process nodes on this level at the end, so we build FILO ordered list.
-    # finally we'll get ordered list with leafes in the beginning and top most nodes at
-    # the end.
-    while ( my @list = grep !$processed{ $_ }
-            && $processed{ $joins->{ $_ }{'depends_on'} }, keys %$joins )
+
+# get a @list of joins that have not been processed yet, but depend on processed join
+# if we are talking about forest then we'll get the second level of the forest,
+# but we should process nodes on this level at the end, so we build FILO ordered list.
+# finally we'll get ordered list with leafes in the beginning and top most nodes at
+# the end.
+    while ( my @list = grep !$processed{$_}
+        && $processed{ $joins->{$_}{'depends_on'} }, keys %$joins )
     {
         unshift @ordered, @list;
-        $processed{ $_ }++ foreach @list;
+        $processed{$_}++ foreach @list;
     }
 
-    foreach my $join ( @ordered ) {
-        next if $self->may_be_null( collection => $args{'collection'}, alias => $join );
+    foreach my $join (@ordered) {
+        next
+            if $self->may_be_null(
+            collection => $args{'collection'},
+            alias      => $join
+            );
 
-        $joins->{ $join }{'alias_string'} =~ s/^\s*LEFT\s+/ /i;
-        $joins->{ $join }{'type'} = 'NORMAL';
+        $joins->{$join}{'alias_string'} =~ s/^\s*LEFT\s+/ /i;
+        $joins->{$join}{'type'} = 'NORMAL';
     }
 
 }
 
 =head2 may_be_null
 
-Takes a C<collection> and C<alias> in a hash and returns
-true if restrictions of the query allow NULLs in a table joined with
-the alias, otherwise returns false value which means that you can
-use normal join instead of left for the aliased table.
-
-Works only for queries have been built with L<Jifty::DBI::Collection/join> and
-L<Jifty::DBI::Collection/limit> methods, for other cases return true value to
-avoid fault optimizations.
+Takes a C<collection> and C<alias> in a hash and returns true if
+restrictions of the query allow NULLs in a table joined with the
+alias, otherwise returns false value which means that you can use
+normal join instead of left for the aliased table.
+
+Works only for queries have been built with
+L<Jifty::DBI::Collection/join> and L<Jifty::DBI::Collection/limit>
+methods, for other cases return true value to avoid fault
+optimizations.
 
 =cut
 
 sub may_be_null {
     my $self = shift;
-    my %args = (collection => undef, alias => undef, @_);
-    # if we have at least one subclause that is not generic then we should get out
-    # of here as we can't parse subclauses
-    return 1 if grep $_ ne 'generic_restrictions', keys %{ $args{'collection'}->{'subclauses'} };
+    my %args = ( collection => undef, alias => undef, @_ );
+
+# if we have at least one subclause that is not generic then we should get out
+# of here as we can't parse subclauses
+    return 1
+        if grep $_ ne 'generic_restrictions',
+        keys %{ $args{'collection'}->{'subclauses'} };
 
     # build full list of generic conditions
     my @conditions;
@@ -1046,16 +1069,18 @@
     }
 
     # find tables that depends on this alias and add their join conditions
-    foreach my $join ( values %{ $args{'collection'}->{'leftjoins'} } ) {
+    foreach my $join ( values %{ $args{'collection'}->{'joins'} } ) {
+
         # left joins on the left side so later we'll get 1 AND x expression
         # which equal to x, so we just skip it
         next if $join->{'type'} eq 'LEFT';
         next unless $join->{'depends_on'} eq $args{'alias'};
 
-        my @tmp = map { ('(', @$_, ')', $join->{'entry_aggregator'}) } values %{ $join->{'criteria'} };
+        my @tmp = map { ( '(', @$_, ')', $join->{'entry_aggregator'} ) }
+            values %{ $join->{'criteria'} };
         pop @tmp;
 
-        @conditions = ('(', @conditions, ')', 'AND', '(', @tmp ,')');
+        @conditions = ( '(', @conditions, ')', 'AND', '(', @tmp, ')' );
 
     }
     return 1 unless @conditions;
@@ -1065,12 +1090,15 @@
         unless ( ref $_ ) {
             push @conditions, $_;
         } elsif ( $_->{'column'} =~ /^\Q$args{'alias'}./ ) {
+
             # only operator IS allows NULLs in the aliased table
             push @conditions, lc $_->{'operator'} eq 'is';
         } elsif ( $_->{'value'} && $_->{'value'} =~ /^\Q$args{'alias'}./ ) {
+
             # right operand is our alias, such condition don't allow NULLs
             push @conditions, 0;
         } else {
+
             # conditions on other aliases
             push @conditions, 1;
         }
@@ -1078,13 +1106,12 @@
 
     # returns index of closing paren by index of openning paren
     my $closing_paren = sub {
-        my $i = shift;
+        my $i     = shift;
         my $count = 0;
         for ( ; $i < @conditions; $i++ ) {
-            if ( $conditions[$i] eq '(' ) {
+            if ( $conditions[$i] && $conditions[$i] eq '(' ) {
                 $count++;
-            }
-            elsif ( $conditions[$i] eq ')' ) {
+            } elsif ( $conditions[$i] && $conditions[$i] eq ')' ) {
                 $count--;
             }
             return $i unless $count;
@@ -1094,11 +1121,12 @@
 
     # solve boolean expression we have, an answer is our result
     my @tmp = ();
-    while ( defined ( my $e = shift @conditions ) ) {
+    while ( defined( my $e = shift @conditions ) ) {
+
         #warn "@tmp >>>$e<<< @conditions";
         return $e if !@conditions && !@tmp;
 
-        unless ( $e ) {
+        unless ($e) {
             if ( $conditions[0] eq ')' ) {
                 push @tmp, $e;
                 next;
@@ -1106,9 +1134,11 @@
 
             my $aggreg = uc shift @conditions;
             if ( $aggreg eq 'OR' ) {
+
                 # 0 OR x == x
                 next;
             } elsif ( $aggreg eq 'AND' ) {
+
                 # 0 AND x == 0
                 my $close_p = $closing_paren->(0);
                 splice @conditions, 0, $close_p + 1, (0);
@@ -1123,10 +1153,12 @@
 
             my $aggreg = uc shift @conditions;
             if ( $aggreg eq 'OR' ) {
+
                 # 1 OR x == 1
                 my $close_p = $closing_paren->(0);
                 splice @conditions, 0, $close_p + 1, (1);
             } elsif ( $aggreg eq 'AND' ) {
+
                 # 1 AND x == x
                 next;
             } else {
@@ -1152,26 +1184,27 @@
 
 takes an incomplete SQL SELECT statement and massages it to return a DISTINCT result set.
 
-
 =cut
 
 sub distinct_query {
     my $self         = shift;
     my $statementref = shift;
-    my $sb           = shift;
+    my $collection   = shift;
 
     # Prepend select query for DBs which allow DISTINCT on all column types.
-    $$statementref = "SELECT DISTINCT ".$sb->_preload_columns." FROM $$statementref";
+    $$statementref
+        = "SELECT DISTINCT "
+        . $collection->query_columns
+        . " FROM $$statementref";
 
-    $$statementref .= $sb->_group_clause;
-    $$statementref .= $sb->_order_clause;
+    $$statementref .= $collection->_group_clause;
+    $$statementref .= $collection->_order_clause;
 }
 
 =head2 distinct_count STATEMENTREF 
 
 takes an incomplete SQL SELECT statement and massages it to return a DISTINCT result set.
 
-
 =cut
 
 sub distinct_count {
@@ -1223,7 +1256,7 @@
 
 =head1 AUTHOR
 
-Jesse Vincent, jesse at fsck.com
+Jesse Vincent, <jesse at bestpractical.com>
 
 =head1 SEE ALSO
 

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Informix.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Informix.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Informix.pm	Mon Dec 10 17:40:42 2007
@@ -1,5 +1,3 @@
-# $Header:  $
-
 package Jifty::DBI::Handle::Informix;
 use Jifty::DBI::Handle;
 @ISA = qw(Jifty::DBI::Handle);
@@ -103,10 +101,10 @@
 sub distinct_query {
     my $self         = shift;
     my $statementref = shift;
-    my $sb           = shift;
-    my $table        = $sb->table;
+    my $collection   = shift;
+    my $table        = $collection->table;
 
-    if ( $sb->_order_clause =~ /(?<!main)\./ ) {
+    if ( $collection->_order_clause =~ /(?<!main)\./ ) {
 
         # Don't know how to do ORDER BY when the DISTINCT is in a subquery
         warn
@@ -119,8 +117,8 @@
         $$statementref
             = "SELECT * FROM $table main WHERE id IN ( SELECT DISTINCT main.id FROM $$statementref )";
     }
-    $$statementref .= $sb->_group_clause;
-    $$statementref .= $sb->_order_clause;
+    $$statementref .= $collection->_group_clause;
+    $$statementref .= $collection->_order_clause;
 }
 
 1;

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/ODBC.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/ODBC.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/ODBC.pm	Mon Dec 10 17:40:42 2007
@@ -39,10 +39,10 @@
 sub build_dsn {
     my $self = shift;
     my %args = (
-        driver     => undef,
-        database   => undef,
-        host       => undef,
-        port       => undef,
+        driver   => undef,
+        database => undef,
+        host     => undef,
+        port     => undef,
         @_
     );
 
@@ -77,12 +77,11 @@
 sub distinct_query {
     my $self         = shift;
     my $statementref = shift;
-
-    my $sb = shift;
+    my $collection   = shift;
 
     $$statementref = "SELECT main.* FROM $$statementref";
-    $$statementref .= $sb->_group_clause;
-    $$statementref .= $sb->_order_clause;
+    $$statementref .= $collection->_group_clause;
+    $$statementref .= $collection->_order_clause;
 }
 
 =head2 encoding

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Oracle.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Oracle.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Oracle.pm	Mon Dec 10 17:40:42 2007
@@ -54,10 +54,9 @@
 =cut
 
 sub database_version {
-    return ''. ORA_OCI;
+    return '' . ORA_OCI;
 }
 
-
 =head2 insert
 
 Takes a table name as the first argument and assumes that the rest of
@@ -239,33 +238,33 @@
 sub distinct_query {
     my $self         = shift;
     my $statementref = shift;
-    my $sb           = shift;
-    my $table        = $sb->Table;
+    my $collection   = shift;
+    my $table        = $collection->Table;
 
     # Wrapp select query in a subselect as Oracle doesn't allow
     # DISTINCT against CLOB/BLOB column types.
-    if ( $sb->_order_clause =~ /(?<!main)\./ ) {
+    if ( $collection->_order_clause =~ /(?<!main)\./ ) {
 
         # If we are ordering by something not in 'main', we need to GROUP
         # BY and adjust the ORDER_BY accordingly
-        local $sb->{group_by}
-            = [ @{ $sb->{group_by} || [] }, { column => 'id' } ];
-        local $sb->{order_by} = [
+        local $collection->{group_by}
+            = [ @{ $collection->{group_by} || [] }, { column => 'id' } ];
+        local $collection->{order_by} = [
             map {
                       ( $_->{alias} and $_->{alias} ne "main" )
                     ? { %{$_}, column => "min(" . $_->{column} . ")" }
                     : $_
-                } @{ $sb->{order_by} }
+                } @{ $collection->{order_by} }
         ];
-        my $group = $sb->_group_clause;
-        my $order = $sb->_order_clause;
+        my $group = $collection->_group_clause;
+        my $order = $collection->_order_clause;
         $$statementref
             = "SELECT main.* FROM ( SELECT main.id FROM $$statementref $group $order ) distinctquery, $table main WHERE (main.id = distinctquery.id)";
     } else {
         $$statementref
             = "SELECT main.* FROM ( SELECT DISTINCT main.id FROM $$statementref ) distinctquery, $table main WHERE (main.id = distinctquery.id) ";
-        $$statementref .= $sb->_group_clause;
-        $$statementref .= $sb->_order_clause;
+        $$statementref .= $collection->_group_clause;
+        $$statementref .= $collection->_order_clause;
     }
 }
 

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Pg.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Pg.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Pg.pm	Mon Dec 10 17:40:42 2007
@@ -120,12 +120,13 @@
 =cut
 
 sub blob_params {
-    my $self   = shift;
+    my $self = shift;
     my $name = shift;
     my $type = shift;
 
     # Don't assign to key 'value' as it is defined later.
-    return ( { pg_type => DBD::Pg::PG_BYTEA() } ) if $type =~ /^(?:blob|bytea)$/;
+    return ( { pg_type => DBD::Pg::PG_BYTEA() } )
+        if $type =~ /^(?:blob|bytea)$/;
     return ( {} );
 }
 
@@ -171,10 +172,14 @@
     my $operator = shift;
     my $value    = shift;
 
-    if ($self->_case_insensitivity_valid($column, $operator, $value)) {
-        if ( $operator =~ /(?:LIKE|=)/i ) {
+    if ( $self->_case_insensitivity_valid( $column, $operator, $value ) ) {
+        if ( $operator =~ /(?:LIKE|=|IN)/i ) {
             $column = "LOWER($column)";
-            $value = "LOWER($value)";
+            if ( $operator eq 'IN' and ref($value) eq 'ARRAY' ) {
+                $value = [ map {"LOWER($_)"} @$value ];
+            } else {
+                $value = "LOWER($value)";
+            }
         }
     }
     return ( $column, $operator, $value );
@@ -187,44 +192,48 @@
 =cut
 
 sub distinct_query {
-  my $self         = shift;
-  my $statementref = shift;
-  my $sb           = shift;
-  my $table        = $sb->table;
-
-  if (
-    grep {
-      ( defined $_->{'alias'} and $_->{'alias'} ne 'main' )
-        || defined $_->{'function'}
-    } @{ $sb->order_by }
-    )
-  {
-
-    # If we are ordering by something not in 'main', we need to GROUP
-    # BY and adjust the ORDER_BY accordingly
-    local $sb->{group_by}
-      = [ @{ $sb->{group_by} || [] }, { column => 'id' } ];
-    local $sb->{order_by} = [
-      map {
-          my $alias = $_->{alias} || '';
-          my $column = $_->{column};
-          $alias .= '.' if $alias;
-          #warn "alias $alias => column $column\n";
-            ((!$alias or $alias eq 'main.') and $column eq 'id')
-          ? $_
-          : { %{$_}, alias => '', column => "min($alias$column)" }
-        } @{ $sb->{order_by} }
-    ];
-    my $group = $sb->_group_clause;
-    my $order = $sb->_order_clause;
-    $$statementref
-      = "SELECT ".$sb->_preload_columns." FROM ( SELECT main.id FROM $$statementref $group $order ) distinctquery, $table main WHERE (main.id = distinctquery.id)";
-  }
-  else {
-    $$statementref = "SELECT DISTINCT ".$sb->_preload_columns." FROM $$statementref";
-    $$statementref .= $sb->_group_clause;
-    $$statementref .= $sb->_order_clause;
-  }
+    my $self         = shift;
+    my $statementref = shift;
+    my $collection   = shift;
+    my $table        = $collection->table;
+
+    if (grep {
+            ( defined $_->{'alias'} and $_->{'alias'} ne 'main' )
+                || defined $_->{'function'}
+        } @{ $collection->order_by }
+        )
+    {
+
+        # If we are ordering by something not in 'main', we need to GROUP
+        # BY and adjust the ORDER_BY accordingly
+        local $collection->{group_by}
+            = [ @{ $collection->{group_by} || [] }, { column => 'id' } ];
+        local $collection->{order_by} = [
+            map {
+                my $alias = $_->{alias} || '';
+                my $column = $_->{column};
+                $alias .= '.' if $alias;
+
+                #warn "alias $alias => column $column\n";
+                ( ( !$alias or $alias eq 'main.' ) and $column eq 'id' )
+                    ? $_
+                    : { %{$_}, alias => '', column => "min($alias$column)" }
+                } @{ $collection->{order_by} }
+        ];
+        my $group = $collection->_group_clause;
+        my $order = $collection->_order_clause;
+        $$statementref
+            = "SELECT "
+            . $collection->query_columns
+            . " FROM ( SELECT main.id FROM $$statementref $group $order ) distinctquery, $table main WHERE (main.id = distinctquery.id)";
+    } else {
+        $$statementref
+            = "SELECT DISTINCT "
+            . $collection->query_columns
+            . " FROM $$statementref";
+        $$statementref .= $collection->_group_clause;
+        $$statementref .= $collection->_order_clause;
+    }
 }
 
 1;

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Sybase.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Sybase.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Handle/Sybase.pm	Mon Dec 10 17:40:42 2007
@@ -82,7 +82,7 @@
 #     my $statementref = shift;
 #     my $per_page     = shift;
 #     my $first        = shift;
-# 
+#
 # }
 
 =head2 distinct_query STATEMENTREF
@@ -95,10 +95,10 @@
 sub distinct_query {
     my $self         = shift;
     my $statementref = shift;
-    my $sb           = shift;
-    my $table        = $sb->table;
+    my $collection   = shift;
+    my $table        = $collection->table;
 
-    if ( $sb->_order_clause =~ /(?<!main)\./ ) {
+    if ( $collection->_order_clause =~ /(?<!main)\./ ) {
 
         # Don't know how to do ORDER BY when the DISTINCT is in a subquery
         warn
@@ -111,8 +111,8 @@
         $$statementref
             = "SELECT main.* FROM ( SELECT DISTINCT main.id FROM $$statementref ) distinctquery, $table main WHERE (main.id = distinctquery.id) ";
     }
-    $$statementref .= $sb->_group_clause;
-    $$statementref .= $sb->_order_clause;
+    $$statementref .= $collection->_group_clause;
+    $$statementref .= $collection->_order_clause;
 }
 
 1;

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Record.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Record.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Record.pm	Mon Dec 10 17:40:42 2007
@@ -7,9 +7,8 @@
 use Lingua::EN::Inflect ();
 use Jifty::DBI::Column  ();
 use UNIVERSAL::require  ();
-use Scalar::Util      qw(blessed);
-use Jifty::DBI::Class::Trigger; # exports by default
-
+use Scalar::Util qw(blessed);
+use Class::Trigger;    # exports by default
 
 use base qw/
     Class::Data::Inheritable
@@ -18,11 +17,12 @@
 
 our $VERSION = '0.01';
 
-Jifty::DBI::Record->mk_classdata(qw/COLUMNS/);
-Jifty::DBI::Record->mk_classdata(qw/TABLE_NAME/ );
-Jifty::DBI::Record->mk_classdata(qw/_READABLE_COLS_CACHE/);
-Jifty::DBI::Record->mk_classdata(qw/_WRITABLE_COLS_CACHE/);
-Jifty::DBI::Record->mk_classdata(qw/_COLUMNS_CACHE/ );
+Jifty::DBI::Record->mk_classdata('COLUMNS');
+Jifty::DBI::Record->mk_classdata('TABLE_NAME');
+Jifty::DBI::Record->mk_classdata('_READABLE_COLS_CACHE');
+Jifty::DBI::Record->mk_classdata('_WRITABLE_COLS_CACHE');
+Jifty::DBI::Record->mk_classdata('_COLUMNS_CACHE');
+Jifty::DBI::Record->mk_classdata('RECORD_MIXINS' => []);
 
 =head1 NAME
 
@@ -63,7 +63,8 @@
     $self->input_filters('Jifty::DBI::Filter::Truncate');
 
     if ( scalar(@_) == 1 ) {
-        Carp::cluck("new(\$handle) is deprecated, use new( handle => \$handle )");
+        Carp::cluck(
+            "new(\$handle) is deprecated, use new( handle => \$handle )");
         $self->_init( handle => shift );
     } else {
         $self->_init(@_);
@@ -74,8 +75,8 @@
 
 # Not yet documented here.  Should almost certainly be overloaded.
 sub _init {
-    my $self   = shift;
-    my %args   = (@_);
+    my $self = shift;
+    my %args = (@_);
     if ( $args{'handle'} ) {
         $self->_handle( $args{'handle'} );
     }
@@ -85,10 +86,10 @@
 sub import {
     my $class = shift;
     my ($flag) = @_;
-    if ($class->isa(__PACKAGE__) and defined $flag and $flag eq '-base') {
+    if ( $class->isa(__PACKAGE__) and defined $flag and $flag eq '-base' ) {
         my $descendant = (caller)[0];
         no strict 'refs';
-        push @{$descendant . '::ISA'}, $class;
+        push @{ $descendant . '::ISA' }, $class;
         shift;
 
         # run the schema callback
@@ -98,7 +99,7 @@
     $class->SUPER::import(@_);
 
     # Turn off redefinition warnings in the caller's scope
-    @_ = (warnings => 'redefine');
+    @_ = ( warnings => 'redefine' );
     goto &warnings::unimport;
 }
 
@@ -127,7 +128,6 @@
     return (%hash);
 }
 
-
 =head2 _accessible COLUMN ATTRIBUTE
 
 Private method. 
@@ -145,7 +145,7 @@
     my $self        = shift;
     my $column_name = shift;
     my $attribute   = lc( shift || '' );
-    my $col = $self->column($column_name);
+    my $col         = $self->column($column_name);
     return undef unless ( $col and $col->can($attribute) );
     return $col->$attribute();
 
@@ -198,33 +198,49 @@
 
 =head2 _init_methods_for_columns
 
-This is an internal method responsible for calling L</_init_methods_for_column> for each column that has been configured.
+This is an internal method responsible for calling
+L</_init_methods_for_column> for each column that has been configured.
 
 =cut
 
 sub _init_methods_for_columns {
     my $self = shift;
 
-    for my $column (sort keys %{ $self->COLUMNS || {} }) {
-        $self->_init_methods_for_column($self->COLUMNS->{ $column });
+    for my $column ( sort keys %{ $self->COLUMNS || {} } ) {
+        $self->_init_methods_for_column( $self->COLUMNS->{$column} );
     }
 }
 
 =head2 schema_version
 
-If present, this method must return a string in '1.2.3' format to be used to determine which columns are currently active in the schema. That is, this value is used to determine which columns are defined, based upon comparison to values set in C<till> and C<since>.
-
-If no implementation is present, the "latest" schema version is assumed, meaning that any column defining a C<till> is not active and all others are.
+If present, this method must return a string in '1.2.3' format to be
+used to determine which columns are currently active in the
+schema. That is, this value is used to determine which columns are
+defined, based upon comparison to values set in C<till> and C<since>.
+
+If no implementation is present, the "latest" schema version is
+assumed, meaning that any column defining a C<till> is not active and
+all others are.
 
 =head2 _init_methods_for_column COLUMN
 
-This method is used internally to update the symbol table for the record class to include an accessor and mutator for each column based upon the column's name.
-
-In addition, if your record class defines the method L</schema_version>, it will automatically generate methods according to whether the column currently exists for the current application schema version returned by that method. The C<schema_version> method must return a value in the same form used by C<since> and C<till>.
+This method is used internally to update the symbol table for the
+record class to include an accessor and mutator for each column based
+upon the column's name.
+
+In addition, if your record class defines the method
+L</schema_version>, it will automatically generate methods according
+to whether the column currently exists for the current application
+schema version returned by that method. The C<schema_version> method
+must return a value in the same form used by C<since> and C<till>.
+
+If the column doesn't currently exist, it will create the methods, but
+they will die with an error message stating that the column does not
+exist for the current version of the application. If it does exist, a
+normal accessor and mutator will be created.
 
-If the column doesn't currently exist, it will create the methods, but they will die with an error message stating that the column does not exist for the current version of the application. If it does exist, a normal accessor and mutator will be created.
-
-See also L<Jifty::DBI::Column/active>, L<Jifty::DBI::Schema/since>, L<Jifty::DBI::Schema/till> for more information.
+See also L<Jifty::DBI::Column/active>, L<Jifty::DBI::Schema/since>,
+L<Jifty::DBI::Schema/till> for more information.
 
 =cut
 
@@ -237,30 +253,42 @@
 
     # Make sure column has a record_class set as not all columns are added
     # through add_column
-    $column->record_class( $package ) if not $column->record_class;
+    $column->record_class($package) if not $column->record_class;
 
     # Check for the correct column type when the Storable filter is in use
     if ( grep { $_ eq 'Jifty::DBI::Filter::Storable' }
-              ($column->input_filters, $column->output_filters)
-         and $column->type !~ /^(blob|bytea)$/i)
+        ( $column->input_filters, $column->output_filters )
+            and $column->type !~ /^(blob|bytea)$/i )
     {
-        die "Column '$column_name' in @{[$column->record_class]} ".
-            "uses the Storable filter but is not of type 'blob'.\n";
+        die "Column '$column_name' in @{[$column->record_class]} "
+            . "uses the Storable filter but is not of type 'blob'.\n";
     }
 
     no strict 'refs';    # We're going to be defining subs
 
     if ( not $self->can($column_name) ) {
+
         # Accessor
         my $subref;
         if ( $column->active ) {
-            
 
             if ( $column->readable ) {
-                if ( UNIVERSAL::isa( $column->refers_to, "Jifty::DBI::Record" ) )
+                if (UNIVERSAL::isa(
+                        $column->refers_to, "Jifty::DBI::Record"
+                    )
+                    )
                 {
                     $subref = sub {
-                        if ( @_ > 1 ) { Carp::carp "Value passed to column accessor.  You probably want to use the mutator." }
+                        if ( @_ > 1 ) {
+                            Carp::carp
+                                "Value passed to column accessor.  You probably want to use the mutator.";
+                        }
+                        # This should be using _value, so we acl_check
+                        # appropriately, except the acl checks often
+                        # involve object references.  So even if you
+                        # don't have rights to $object->foo_id,
+                        # $object->foo->id will always have to
+                        # work. :/
                         $_[0]->_to_record( $column_name,
                             $_[0]->__value($column_name) );
                     };
@@ -273,18 +301,23 @@
                     $subref = sub { $_[0]->_collection_value($column_name) };
                 } else {
                     $subref = sub {
-                        if ( @_ > 1 ) { Carp::carp "Value passed to column accessor.  You probably want to use the mutator." }
+                        if ( @_ > 1 ) {
+                            Carp::carp
+                                "Value passed to column accessor.  You probably want to use the mutator.";
+                        }
                         return ( $_[0]->_value($column_name) );
                     };
                 }
             } else {
                 $subref = sub { return '' }
             }
-        }
-        else {
-            # XXX sterling: should this be done with Class::ReturnValue instead
+        } else {
+
+           # XXX sterling: should this be done with Class::ReturnValue instead
             $subref = sub {
-                Carp::croak("column $column_name is not available for $package for schema version ".$self->schema_version);
+                Carp::croak(
+                    "column $column_name is not available for $package for schema version "
+                        . $self->schema_version );
             };
         }
         *{ $package . "::" . $column_name } = $subref;
@@ -292,11 +325,15 @@
     }
 
     if ( not $self->can( "set_" . $column_name ) ) {
+
         # Mutator
         my $subref;
         if ( $column->active ) {
             if ( $column->writable ) {
-                if ( UNIVERSAL::isa( $column->refers_to, "Jifty::DBI::Record" ) )
+                if (UNIVERSAL::isa(
+                        $column->refers_to, "Jifty::DBI::Record"
+                    )
+                    )
                 {
                     $subref = sub {
                         my $self = shift;
@@ -305,7 +342,10 @@
                         $val = $val->id
                             if UNIVERSAL::isa( $val, 'Jifty::DBI::Record' );
                         return (
-                            $self->_set( column => $column_name, value => $val )
+                            $self->_set(
+                                column => $column_name,
+                                value  => $val
+                            )
                         );
                     };
                 } elsif (
@@ -314,8 +354,9 @@
                     )
                     )
                 {    # XXX elw: collections land here, now what?
-                    my $ret     = Class::ReturnValue->new();
-                    my $message = "Collection column '$column_name' not writable";
+                    my $ret = Class::ReturnValue->new();
+                    my $message
+                        = "Collection column '$column_name' not writable";
                     $ret->as_array( 0, $message );
                     $ret->as_error(
                         errno        => 3,
@@ -326,7 +367,10 @@
                 } else {
                     $subref = sub {
                         return (
-                            $_[0]->_set( column => $column_name, value => $_[1] )
+                            $_[0]->_set(
+                                column => $column_name,
+                                value  => $_[1]
+                            )
                         );
                     };
                 }
@@ -341,17 +385,36 @@
                 );
                 $subref = sub { return ( $ret->return_value ); };
             }
-        }
-        else {
-            # XXX sterling: should this be done with Class::ReturnValue instead
+        } else {
+
+           # XXX sterling: should this be done with Class::ReturnValue instead
             $subref = sub {
-                Carp::croak("column $column_name is not available for $package for schema version ".$self->schema_version);
+                Carp::croak(
+                    "column $column_name is not available for $package for schema version "
+                        . $self->schema_version );
             };
         }
         *{ $package . "::" . "set_" . $column_name } = $subref;
     }
 }
 
+=head2 null_reference 
+
+By default, Jifty::DBI::Record will return C<undef> for non-existant
+foreign references which don't exist.  That is, if each Employee
+C<refers_to> a Department, but isn't required to,
+C<<$model->department>> will return C<undef> for employees not in a
+department.
+
+Overriding this method to return 0 will cause it to return a record
+with no id.  That is, C<<$model->department>> will return a Department
+object, but C<<$model->department->id>> will be C<undef>.
+
+=cut
+
+sub null_reference {
+    return 1;
+}
 
 =head2 _to_record COLUMN VALUE
 
@@ -374,50 +437,76 @@
     my $classname     = $column->refers_to();
     my $remote_column = $column->by() || 'id';
 
-    return       unless defined $value;
+    return undef if not defined $value and $self->null_reference;
     return undef unless $classname;
-    return       unless UNIVERSAL::isa( $classname, 'Jifty::DBI::Record' );
+    return unless UNIVERSAL::isa( $classname, 'Jifty::DBI::Record' );
+
+    if ( my $prefetched = $self->prefetched($column_name) ) {
+        return $prefetched;
+    }
 
-    # XXX TODO FIXME we need to figure out the right way to call new here
-    # perhaps the handle should have an initiializer for records/collections
-    my $object = $classname->new( handle => $self->_handle );
-    $object->load_by_cols( $remote_column => $value );
+    my $object = $classname->new( $self->_new_record_args );
+    $object->load_by_cols( $remote_column => $value ) if defined $value;
     return $object;
 }
 
-sub _collection_value {
+sub _new_record_args {
     my $self = shift;
+    return ( handle => $self->_handle );
+}
 
-    my $method_name = shift;
-    return unless defined $method_name;
+sub _collection_value {
+    my $self        = shift;
+    my $column_name = shift;
 
-    my $column    = $self->column($method_name);
+    my $column    = $self->column($column_name);
     my $classname = $column->refers_to();
 
     return undef unless $classname;
     return unless UNIVERSAL::isa( $classname, 'Jifty::DBI::Collection' );
 
-    if ( my $prefetched_col = $self->_prefetched_collection($method_name)) {
-        return $prefetched_col;
+    if ( my $prefetched = $self->prefetched($column_name) ) {
+        return $prefetched;
     }
 
-    my $coll = $classname->new( handle => $self->_handle );
-    $coll->limit( column => $column->by(), value => $self->id );
+    my $coll = $classname->new( $self->_new_collection_args );
+    $coll->limit( column => $column->by, value => $self->id )
+        if $column->by and $self->id;
     return $coll;
 }
 
-sub _prefetched_collection {
-    my $self =shift;
+sub _new_collection_args {
+    my $self = shift;
+    return ( handle => $self->_handle );
+}
+
+=head2 prefetched NAME
+
+Returns the prefetched value for column of property C<NAME>, if it
+exists.
+
+=cut
+
+sub prefetched {
+    my $self        = shift;
     my $column_name = shift;
     if (@_) {
-        $self->{'_prefetched_collections'}->{$column_name} = shift;
+        my $column = $self->column($column_name);
+        if ( $column and not $column->refers_to ) {
+            warn "$column_name isn't supposed to be an object reference!";
+            return;
+        } elsif ( $column
+            and not UNIVERSAL::isa( $_[0], $column->refers_to ) )
+        {
+            warn "$column_name is supposed to be a @{[$column->refers_to]}!";
+        } else {
+            $self->{'_prefetched'}->{$column_name} = shift;
+        }
     } else {
-        return $self->{'_prefetched_collections'}->{$column_name};
+        return $self->{'_prefetched'}->{$column_name};
     }
-
 }
 
-
 =head2 add_column
 
 =cut
@@ -425,33 +514,34 @@
 sub add_column {
     my $self = shift;
     my $name = shift;
-    $name = lc $name;
-    
+
+    #$name = lc $name;
+
     $self->COLUMNS->{$name} = Jifty::DBI::Column->new()
-    unless exists $self->COLUMNS->{$name};
+        unless exists $self->COLUMNS->{$name};
     $self->_READABLE_COLS_CACHE(undef);
     $self->_WRITABLE_COLS_CACHE(undef);
-    $self->_COLUMNS_CACHE(undef );
+    $self->_COLUMNS_CACHE(undef);
     $self->COLUMNS->{$name}->name($name);
 
-    my $class = ref( $self ) || $self;
-    $self->COLUMNS->{$name}->record_class( $class );
+    my $class = ref($self) || $self;
+    $self->COLUMNS->{$name}->record_class($class);
 
     return $self->COLUMNS->{$name};
 }
 
 =head2 column
 
-    my $value = $self->column($column);
+    my $column = $self->column($column_name);
 
-Returns the $value of a $column.
+Returns the L<Jifty::DBI::Column> object of the specified column name.
 
 =cut
 
 sub column {
     my $self = shift;
-    my $name = lc( shift || '' );
-    my $col = $self->_columns_hashref;
+    my $name = ( shift || '' );
+    my $col  = $self->_columns_hashref;
     return undef unless $col && exists $col->{$name};
     return $col->{$name};
 
@@ -467,14 +557,20 @@
 
 sub columns {
     my $self = shift;
-    return @{$self->_COLUMNS_CACHE() || $self->_COLUMNS_CACHE([
-        sort {
-            ( ( ( $b->type || '' ) eq 'serial' )
-                <=> ( ( $a->type || '' ) eq 'serial' ) )
-                or ( ($a->sort_order || 0) <=> ($b->sort_order || 0))
-                or ( $a->name cmp $b->name )
-            } grep { $_->active } values %{ $self->_columns_hashref }
-	])}
+    return @{
+        $self->_COLUMNS_CACHE() || $self->_COLUMNS_CACHE(
+            [   sort {
+                    ( ( ( $b->type || '' ) eq 'serial' )
+                        <=> ( ( $a->type || '' ) eq 'serial' ) )
+                        or (
+                        ( $a->sort_order || 0 ) <=> ( $b->sort_order || 0 ) )
+                        or ( $a->name cmp $b->name )
+                    } grep {
+                    $_->active
+                    } values %{ $self->_columns_hashref }
+            ]
+        )
+        };
 }
 
 =head2 all_columns
@@ -489,22 +585,20 @@
     my $self = shift;
 
     # Not cached because it's not expected to be used often
-    return
-        sort {
-            ( ( ( $b->type || '' ) eq 'serial' )
-                <=> ( ( $a->type || '' ) eq 'serial' ) )
-                or ( ($a->sort_order || 0) <=> ($b->sort_order || 0))
-                or ( $a->name cmp $b->name )
-            } values %{ $self->_columns_hashref || {} }
+    return sort {
+        ( ( ( $b->type || '' ) eq 'serial' )
+            <=> ( ( $a->type || '' ) eq 'serial' ) )
+            or ( ( $a->sort_order || 0 ) <=> ( $b->sort_order || 0 ) )
+            or ( $a->name cmp $b->name )
+    } values %{ $self->_columns_hashref || {} };
 }
 
 sub _columns_hashref {
     my $self = shift;
 
-      return ($self->COLUMNS||{});
+    return ( $self->COLUMNS || {} );
 }
 
-
 # sub {{{ readable_attributes
 
 =head2 readable_attributes
@@ -515,16 +609,21 @@
 
 sub readable_attributes {
     my $self = shift;
-    return @{$self->_READABLE_COLS_CACHE() || $self->_READABLE_COLS_CACHE([sort map { $_->name } grep { $_->readable } $self->columns])};
+    return @{
+        $self->_READABLE_COLS_CACHE() || $self->_READABLE_COLS_CACHE(
+            [ sort map { $_->name } grep { $_->readable } $self->columns ]
+        )
+        };
 }
 
 =head2 serialize_metadata
 
-Returns a hash which describes how this class is stored in the database. 
-Right now, the keys are C<class>, C<table>, and C<columns>. C<class> and C<table>
-return simple scalars, but C<columns> returns a hash of C<name =&gt; value> pairs
-for all the columns in this model. See C<Jifty::DBI::Column/serialize_metadata> for 
-the format of that hash.
+Returns a hash which describes how this class is stored in the
+database.  Right now, the keys are C<class>, C<table>, and
+C<columns>. C<class> and C<table> return simple scalars, but
+C<columns> returns a hash of C<name =&gt; value> pairs for all the
+columns in this model. See C<Jifty::DBI::Column/serialize_metadata>
+for the format of that hash.
 
 
 =cut
@@ -532,25 +631,22 @@
 sub serialize_metadata {
     my $self = shift;
     return {
-            class => (ref($self) || $self),
-            table => $self->table,
-            columns => { $self->_serialize_columns },
-    }
+        class => ( ref($self) || $self ),
+        table => $self->table,
+        columns => { $self->_serialize_columns },
+    };
 }
 
 sub _serialize_columns {
     my $self = shift;
     my %serialized_columns;
-    foreach my $column ( $self->columns  ) {
+    foreach my $column ( $self->columns ) {
         $serialized_columns{ $column->name } = $column->serialize_metadata();
     }
 
     return %serialized_columns;
 }
 
-
-
-
 =head2 writable_attributes
 
 Returns a list of this table's writable columns
@@ -560,17 +656,22 @@
 
 sub writable_attributes {
     my $self = shift;
-    return @{$self->_WRITABLE_COLS_CACHE() || $self->_WRITABLE_COLS_CACHE([sort map { $_->name } grep { $_->writable } $self->columns])};
+    return @{
+        $self->_WRITABLE_COLS_CACHE() || $self->_WRITABLE_COLS_CACHE(
+            [ sort map { $_->name } grep { $_->writable } $self->columns ]
+        )
+        };
 }
 
 =head2 record values
 
-As you've probably already noticed, C<Jifty::DBI::Record> autocreates methods for your
-standard get/set accessors. It also provides you with some hooks to massage the values
-being loaded or stored.
-
-When you fetch a record value by calling C<$my_record-E<gt>some_field>, C<Jifty::DBI::Record>
-provides the following hook
+As you've probably already noticed, C<Jifty::DBI::Record> autocreates
+methods for your standard get/set accessors. It also provides you with
+some hooks to massage the values being loaded or stored.
+
+When you fetch a record value by calling
+C<$my_record-E<gt>some_field>, C<Jifty::DBI::Record> provides the
+following hook
 
 =over
 
@@ -611,6 +712,11 @@
 
 If before_set_I<column_name> returns false, the new value isn't set.
 
+=item before_set PARAMHASH
+
+This is identical to the C<before_set_I<column_name>>, but is called
+for every column set.
+
 =item after_set_I<column_name> PARAMHASH
 
 This hook will be called after a value is successfully set in the
@@ -620,15 +726,20 @@
 
 This hook's return value is ignored.
 
+=item after_set PARAMHASH
+
+This is identical to the C<after_set_I<column_name>>, but is called
+for every column set.
+
 =item validate_I<column_name> VALUE
 
 This hook is called just before updating the database. It expects the
 actual new value you're trying to set I<column_name> to. It returns
 two values.  The first is a boolean with truth indicating success. The
-second is an optional message. Note that validate_I<column_name> may be
-called outside the context of a I<set> operation to validate a potential
-value. (The Jifty application framework uses this as part of its AJAX
-validation system.)
+second is an optional message. Note that validate_I<column_name> may
+be called outside the context of a I<set> operation to validate a
+potential value. (The Jifty application framework uses this as part of
+its AJAX validation system.)
 
 =back
 
@@ -648,8 +759,10 @@
     my $column = shift;
 
     my $value = $self->__value( $column => @_ );
-    $self->_run_callback( name => "after_".$column,
-                          args => \$value);
+    $self->_run_callback(
+        name => "after_" . $column,
+        args => \$value
+    );
     return $value;
 }
 
@@ -661,13 +774,14 @@
 =cut
 
 sub __value {
-    my $self        = shift;
+    my $self = shift;
+
+    my $column_name = shift;
 
-    my $column_name = lc(shift);
     # If the requested column is actually an alias for another, resolve it.
     my $column = $self->column($column_name);
-    if  ($column   and defined $column->alias_for_column ) {
-        $column = $self->column($column->alias_for_column());
+    if ( $column and defined $column->alias_for_column ) {
+        $column      = $self->column( $column->alias_for_column() );
         $column_name = $column->name;
     }
 
@@ -675,38 +789,38 @@
 
     # In the default case of "yeah, we have a value", return it as
     # fast as we can.
-    return   $self->{'values'}{$column_name}
+    return $self->{'values'}{$column_name}
         if ( $self->{'fetched'}{$column_name}
-          && $self->{'decoded'}{$column_name} );
+        && $self->{'decoded'}{$column_name} );
 
-    if ( !$self->{'fetched'}{ $column_name } and my $id = $self->id() ) {
-        my $pkey         = $self->_primary_key();
-        my $query_string = "SELECT "
+    if ( !$self->{'fetched'}{$column_name} and my $id = $self->id() ) {
+        my $pkey = $self->_primary_key();
+        my $query_string
+            = "SELECT "
             . $column_name
             . " FROM "
             . $self->table
             . " WHERE $pkey = ?";
         my $sth = $self->_handle->simple_query( $query_string, $id );
         my ($value) = eval { $sth->fetchrow_array() };
-        warn $@ if $@;
-
-        $self->{'values'}{ $column_name }  = $value;
-        $self->{'fetched'}{ $column_name } = 1;
+        $self->{'values'}{$column_name}  = $value;
+        $self->{'fetched'}{$column_name} = 1;
     }
-    unless ( $self->{'decoded'}{ $column_name } ) {
+    unless ( $self->{'decoded'}{$column_name} ) {
         $self->_apply_output_filters(
             column    => $column,
-            value_ref => \$self->{'values'}{ $column_name },
-        ) if exists $self->{'values'}{ $column_name };
-        $self->{'decoded'}{ $column_name } = 1;
+            value_ref => \$self->{'values'}{$column_name},
+        ) if exists $self->{'values'}{$column_name};
+        $self->{'decoded'}{$column_name} = 1;
     }
 
-    return $self->{'values'}{ $column_name };
+    return $self->{'values'}{$column_name};
 }
 
 =head2 as_hash 
 
-Returns a version of this record's readable columns rendered as a hash of key => value pairs
+Returns a version of this record's readable columns rendered as a hash
+of key => value pairs
 
 =cut
 
@@ -717,13 +831,11 @@
     return %values;
 }
 
-
-
 =head2 _set
 
-_set takes a single column name and a single unquoted value.
-It updates both the in-memory value of this column and the in-database copy.
-Subclasses can override _set to insert custom access control.
+_set takes a single column name and a single unquoted value.  It
+updates both the in-memory value of this column and the in-database
+copy.  Subclasses can override _set to insert custom access control.
 
 =cut
 
@@ -736,18 +848,37 @@
         @_
     );
 
+    # Call the general before_set triggers
+    my $ok = $self->_run_callback(
+        name => "before_set",
+        args => \%args,
+    );
+    return $ok if ( not defined $ok );
 
-    my $ok = $self->_run_callback( name => "before_set_" . $args{column},
-                           args => \%args);
-    return $ok if( not defined $ok);
+    # Call the specific before_set_column triggers
+    $ok = $self->_run_callback(
+        name => "before_set_" . $args{column},
+        args => \%args,
+    );
+    return $ok if ( not defined $ok );
 
     $ok = $self->__set(%args);
     return $ok if not $ok;
 
-        # Fetch the value back to make sure we have the actual value
-        my $value = $self->_value($args{column});
-        my $after_set_ret = $self->_run_callback( name => "after_set_" . $args{column}, args => 
-        {column => $args{column}, value => $value});
+    # Fetch the value back to make sure we have the actual value
+    my $value = $self->_value( $args{column} );
+
+    # Call the general after_set triggers
+    $self->_run_callback(
+        name => "after_set",
+        args => { column => $args{column}, value => $value },
+    );
+
+    # Call the specific after_set_column triggers
+    $self->_run_callback(
+        name => "after_set_" . $args{column},
+        args => { column => $args{column}, value => $value },
+    );
 
     return $ok;
 }
@@ -818,12 +949,11 @@
             return ( $ret->return_value );
         }
     }
-    
 
     # Implement 'is distinct' checking
     if ( $column->distinct ) {
         my $ret = $self->is_distinct( $column->name, $args{'value'} );
-        return ( $ret ) if not ( $ret );
+        return ($ret) if not($ret);
     }
 
     # The blob handling will destroy $args{'value'}. But we assign
@@ -831,7 +961,8 @@
     my $unmunged_value = $args{'value'};
 
     if ( $column->type =~ /^(text|longtext|clob|blob|lob|bytea)$/i ) {
-        my $bhash = $self->_handle->blob_params( $column->name, $column->type );
+        my $bhash
+            = $self->_handle->blob_params( $column->name, $column->type );
         $bhash->{'value'} = $args{'value'};
         $args{'value'} = $bhash;
     }
@@ -862,7 +993,7 @@
         # XXX TODO primary_keys
         $self->load_by_cols( id => $self->id );
     } else {
-        $self->{'values'}{ $column->name } = $unmunged_value;
+        $self->{'values'}{ $column->name }  = $unmunged_value;
         $self->{'decoded'}{ $column->name } = 0;
     }
     $ret->as_array( 1, "The new value has been set." );
@@ -873,7 +1004,7 @@
 
 C<load> can be called as a class or object method.
 
-Takes a single argument, $id. Calls load_by_cols to retrieve the row 
+Takes a single argument, $id. Calls load_by_cols to retrieve the row
 whose primary key is $id.
 
 =cut
@@ -889,24 +1020,24 @@
 
 C<load_by_cols> can be called as a class or object method.
 
-Takes a hash of columns and values. Loads the first record that matches all
-keys.
+Takes a hash of columns and values. Loads the first record that
+matches all keys.
 
 The hash's keys are the columns to look at.
 
-The hash's values are either: scalar values to look for
-OR hash references which contain 'operator' and 'value'
+The hash's values are either: scalar values to look for OR hash
+references which contain 'operator' and 'value'
 
 =cut
 
 sub load_by_cols {
-    my $class    = shift;
-    my %hash = (@_);
+    my $class = shift;
+    my %hash  = (@_);
     my ($self);
-    if (ref($class)) {
-            ($self,$class) = ($class,undef);
+    if ( ref($class) ) {
+        ( $self, $class ) = ( $class, undef );
     } else {
-            $self = $class->new( handle => (delete $hash{'_handle'} || undef));
+        $self = $class->new( handle => ( delete $hash{'_handle'} || undef ) );
     }
 
     my ( @bind, @phrases );
@@ -914,25 +1045,42 @@
         if ( defined $hash{$key} && $hash{$key} ne '' ) {
             my $op;
             my $value;
-            my $function = "?";
+            my $function   = "?";
+            my $column_obj = $self->column($key);
+            Carp::confess(
+                "Unknown column '$key' in class '" . ref($self) . "'" )
+                if !defined $column_obj;
+            my $case_sensitive = $column_obj->case_sensitive;
             if ( ref $hash{$key} eq 'HASH' ) {
-                $op       = $hash{$key}->{operator};
-                $value    = $hash{$key}->{value};
-                $function = $hash{$key}->{function} || "?";
+                $op             = $hash{$key}->{operator};
+                $value          = $hash{$key}->{value};
+                $function       = $hash{$key}->{function} || "?";
+                $case_sensitive = $hash{$key}->{case_sensitive}
+                    if exists $hash{$key}->{case_sensitive};
             } else {
                 $op    = '=';
                 $value = $hash{$key};
             }
 
-            if (blessed $value && $value->isa('Jifty::DBI::Record') ) {
+            if ( blessed $value && $value->isa('Jifty::DBI::Record') ) {
+
                 # XXX TODO: check for proper foriegn keyness here
                 $value = $value->id;
             }
 
+            # if the handle is in a case_sensitive world and we need to make
+            # a case-insensitive query
+            if ( $self->_handle->case_sensitive && $value ) {
+                if ( $column_obj->is_string && !$case_sensitive ) {
+                    ( $key, $op, $function )
+                        = $self->_handle->_make_clause_case_insensitive( $key,
+                        $op, $function );
+                }
+            }
 
             push @phrases, "$key $op $function";
             push @bind,    $value;
-	} elsif (!defined $hash{$key}) {
+        } elsif ( !defined $hash{$key} ) {
             push @phrases, "$key IS NULL";
         } else {
             push @phrases, "($key IS NULL OR $key = ?)";
@@ -947,12 +1095,17 @@
         }
     }
 
-    my $query_string = "SELECT  * FROM "
+    my $query_string
+        = "SELECT  * FROM "
         . $self->table
         . " WHERE "
         . join( ' AND ', @phrases );
-    if ($class) { $self->_load_from_sql( $query_string, @bind ); return $self}
-    else {return $self->_load_from_sql( $query_string, @bind );}
+    if ($class) {
+        $self->_load_from_sql( $query_string, @bind );
+        return $self;
+    } else {
+        return $self->_load_from_sql( $query_string, @bind );
+    }
 
 }
 
@@ -976,28 +1129,34 @@
 
 =head2 load_from_hash
 
-Takes a hashref, such as created by Jifty::DBI and populates this record's
-loaded values hash.
+Takes a hashref, such as created by Jifty::DBI and populates this
+record's loaded values hash.
 
 =cut
 
 sub load_from_hash {
-    my $class    = shift;
+    my $class   = shift;
     my $hashref = shift;
     my ($self);
 
-    if (ref($class)) {
-            ($self,$class) = ($class,undef);
+    if ( ref($class) ) {
+        ( $self, $class ) = ( $class, undef );
     } else {
-            $self = $class->new( handle => (delete $hashref->{'_handle'} || undef));
+        $self = $class->new(
+            handle => ( delete $hashref->{'_handle'} || undef ) );
     }
-    
 
-    foreach my $f ( keys %$hashref ) {
-        $self->{'fetched'}{ lc $f } = 1;
+    $self->{values} = {};
+
+    #foreach my $f ( keys %$hashref ) { $self->{'fetched'}{  $f } = 1; }
+    foreach my $col ( map { $_->name } $self->columns ) {
+        next unless exists $hashref->{ lc($col) };
+        $self->{'fetched'}{$col} = 1;
+        $self->{'values'}->{$col} = $hashref->{ lc($col) };
+
     }
 
-    $self->{'values'}  = $hashref;
+    #$self->{'values'}  = $hashref;
     $self->{'decoded'} = {};
     return $self->id();
 }
@@ -1019,9 +1178,17 @@
 
     return ( 0, "Couldn't execute query" ) unless $sth;
 
-    $self->{'values'}  = $sth->fetchrow_hashref;
+    my $hashref = $sth->fetchrow_hashref;
+    delete $self->{values};
     $self->{'fetched'} = {};
     $self->{'decoded'} = {};
+
+    #foreach my $f ( keys %$hashref ) { $self->{'fetched'}{  $f } = 1; }
+    foreach my $col ( map { $_->name } $self->columns ) {
+        next unless exists $hashref->{ lc($col) };
+        $self->{'fetched'}{$col} = 1;
+        $self->{'values'}->{$col} = $hashref->{ lc($col) };
+    }
     if ( !$self->{'values'} && $sth->err ) {
         return ( 0, "Couldn't fetch row: " . $sth->err );
     }
@@ -1038,7 +1205,7 @@
     }
 
     foreach my $f ( keys %{ $self->{'values'} } ) {
-        $self->{'fetched'}{ lc $f } = 1;
+        $self->{'fetched'}{$f} = 1;
     }
     return ( 1, "Found object" );
 
@@ -1056,6 +1223,11 @@
 
 =item before_create
 
+When adding the C<before_create> trigger, you can determine whether
+the trigger may cause an abort or not by passing the C<abortable>
+parameter to the C<add_trigger> method. If this is not set, then the
+return value is ignored regardless.
+
   sub before_create {
       my $self = shift;
       my $args = shift;
@@ -1070,10 +1242,16 @@
 This method is called before trying to create our row in the
 database. It's handed a reference to your paramhash. (That means it
 can modify your parameters on the fly).  C<before_create> returns a
-true or false value. If it returns false, the create is aborted.
+true or false value. If it returns C<undef> and the trigger has been
+added as C<abortable>, the create is aborted.
 
 =item after_create
 
+When adding the C<after_create> trigger, you can determine whether the
+trigger may cause an abort or not by passing the C<abortable>
+parameter to the C<add_trigger> method. If this is not set, then the
+return value is ignored regardless.
+
   sub after_create {
       my $self                    = shift;
       my $insert_return_value_ref = shift;
@@ -1083,12 +1261,17 @@
 
       # Do whatever needs to be done here
 
-      return; # return value is ignored
+      return;   # aborts the create, possibly preventing a load
+      return 1; # continue normally
   }
 
 This method is called after attempting to insert the record into the
 database. It gets handed a reference to the return value of the
-insert. That'll either be a true value or a L<Class::ReturnValue>
+insert. That'll either be a true value or a L<Class::ReturnValue>.
+
+Aborting the trigger merely causes C<create> to return a false
+(undefined) value even thought he create may have succeeded. This
+prevents the loading of the record that would normally be returned.
 
 =back
 
@@ -1096,43 +1279,46 @@
 =cut 
 
 sub create {
-    my $class    = shift;
+    my $class   = shift;
     my %attribs = @_;
 
     my ($self);
-    if (ref($class)) {
-            ($self,$class) = ($class,undef);
+    if ( ref($class) ) {
+        ( $self, $class ) = ( $class, undef );
     } else {
-            $self = $class->new( handle => (delete $attribs{'_handle'} || undef));
+        $self = $class->new(
+            handle => ( delete $attribs{'_handle'} || undef ) );
     }
 
-
-
-    my $ok = $self->_run_callback( name => "before_create", args => \%attribs);
-    return $ok if ( not defined $ok);
+    my $ok
+        = $self->_run_callback( name => "before_create", args => \%attribs );
+    return $ok if ( not defined $ok );
 
     my $ret = $self->__create(%attribs);
 
-    $ok = $self->_run_callback( name => "after_create",
-                           args => \$ret);
-    return $ok if (not defined $ok);
-    
+    $ok = $self->_run_callback(
+        name => "after_create",
+        args => \$ret
+    );
+    return $ok if ( not defined $ok );
+
     if ($class) {
-        $self->load_by_cols(id => $ret);
+        $self->load_by_cols( id => $ret );
         return ($self);
-    }
-    else {
-     return ($ret);
+    } else {
+        return ($ret);
     }
 }
 
 sub __create {
-    my ($self, %attribs) = @_;
+    my ( $self, %attribs ) = @_;
 
     foreach my $column_name ( keys %attribs ) {
         my $column = $self->column($column_name);
         unless ($column) {
-            # "Virtual" columns beginning with __ is passed through to handle without munging.
+
+            # "Virtual" columns beginning with __ are passed through
+            # to handle without munging.
             next if $column_name =~ /^__/;
 
             Carp::confess "$column_name isn't a column we know about";
@@ -1153,28 +1339,47 @@
 
         # Implement 'is distinct' checking
         if ( $column->distinct ) {
-            my $ret = $self->is_distinct( $column_name, $attribs{$column_name} );
-            if (not $ret ) {
-                Carp::cluck("$self failed a 'is_distinct' check for $column_name on ".$attribs{$column_name});
-            return ( $ret ) 
+            my $ret
+                = $self->is_distinct( $column_name, $attribs{$column_name} );
+            if ( not $ret ) {
+                Carp::cluck(
+                    "$self failed a 'is_distinct' check for $column_name on "
+                        . $attribs{$column_name} );
+                return ($ret);
             }
         }
 
         if ( $column->type =~ /^(text|longtext|clob|blob|lob|bytea)$/i ) {
-            my $bhash = $self->_handle->blob_params( $column_name, $column->type );
+            my $bhash
+                = $self->_handle->blob_params( $column_name, $column->type );
             $bhash->{'value'} = $attribs{$column_name};
             $attribs{$column_name} = $bhash;
         }
     }
 
-    for my $column ($self->columns) {
-        if (not defined $attribs{$column->name} and defined $column->default and not ref $column->default) {
-            $attribs{$column->name} = $column->default;
+    for my $column ( $self->columns ) {
+        if (    not defined $attribs{ $column->name }
+            and defined $column->default
+            and not ref $column->default )
+        {
+            $attribs{ $column->name } = $column->default;
         }
-        if (not defined $attribs{$column->name} and $column->mandatory and $column->type ne "serial" ) {
+
+        if (    not defined $attribs{ $column->name }
+            and $column->mandatory
+            and $column->type ne "serial" )
+        {
+
             # Enforce "mandatory"
-            Carp::carp "Did not supply value for mandatory column ".$column->name;
-            return ( 0 );
+            Carp::carp "Did not supply value for mandatory column "
+                . $column->name;
+            unless ( $column->active ) {
+                Carp::carp "The mandatory column "
+                    . $column->name
+                    . " is no longer active. This is likely to cause problems!";
+            }
+
+            return (0);
         }
     }
 
@@ -1186,7 +1391,7 @@
 Delete this record from the database. On failure return a
 Class::ReturnValue with the error. On success, return 1;
 
-This method has two hooks
+This method has two hooks:
 
 =over 
 
@@ -1196,8 +1401,8 @@
 failure it returns a L<Class::ReturnValue> with the error.  On success
 it returns 1.
 
-If this method returns an error, it causes the delete to abort and return
-the return value from this hook.
+If this method returns an error, it causes the delete to abort and
+return the return value from this hook. 
 
 =item after_delete
 
@@ -1211,12 +1416,12 @@
 sub delete {
     my $self = shift;
     my $before_ret = $self->_run_callback( name => 'before_delete' );
-    return $before_ret unless (defined $before_ret);
+    return $before_ret unless ( defined $before_ret );
     my $ret = $self->__delete;
 
     my $after_ret
         = $self->_run_callback( name => 'after_delete', args => \$ret );
-    return $after_ret unless (defined $after_ret);
+    return $after_ret unless ( defined $after_ret );
     return ($ret);
 
 }
@@ -1229,7 +1434,7 @@
 
     ## Constructs the where clause.
     my %pkeys = $self->primary_keys();
-    my $return       = $self->_handle->delete( $self->table, $self->primary_keys );
+    my $return = $self->_handle->delete( $self->table, $self->primary_keys );
 
     if ( UNIVERSAL::isa( 'Class::ReturnValue', $return ) ) {
         return ($return);
@@ -1254,20 +1459,22 @@
 
 sub table {
     my $self = shift;
-    $self->TABLE_NAME($self->_guess_table_name) unless ($self->TABLE_NAME());
+    $self->TABLE_NAME( $self->_guess_table_name )
+        unless ( $self->TABLE_NAME() );
     return $self->TABLE_NAME();
 }
 
 =head2 collection_class
 
-Returns the collection class which this record belongs to; override this to
-subclass.  If you haven't specified a collection class, this returns a best
-guess at the name of the collection class for this collection.
-
-It uses a simple heuristic to determine the collection class name -- It
-appends "Collection" to its own name. If you want to name your records
-and collections differently, go right ahead, but don't say we didn't
-warn you.
+Returns the collection class which this record belongs to; override
+this to subclass.  If you haven't specified a collection class, this
+returns a best guess at the name of the collection class for this
+collection.
+
+It uses a simple heuristic to determine the collection class name --
+It appends "Collection" to its own name. If you want to name your
+records and collections differently, go right ahead, but don't say we
+didn't warn you.
 
 =cut
 
@@ -1315,7 +1522,6 @@
 
 used for the declarative syntax
 
-
 =cut
 
 sub _filters {
@@ -1353,8 +1559,8 @@
     my $action = $args{'direction'} eq 'output' ? 'decode' : 'encode';
     foreach my $filter_class (@filters) {
         local $UNIVERSAL::require::ERROR;
-        $filter_class->require() unless 
-         $INC{ join('/', split(/::/,$filter_class)).".pm" };
+        $filter_class->require()
+            unless $INC{ join( '/', split( /::/, $filter_class ) ) . ".pm" };
 
         if ($UNIVERSAL::require::ERROR) {
             warn $UNIVERSAL::require::ERROR;
@@ -1382,17 +1588,17 @@
 =cut 
 
 sub is_distinct {
-    my $self = shift;
+    my $self   = shift;
     my $column = shift;
-    my $value = shift;
+    my $value  = shift;
 
-    my $record = $self->new( handle => $self->_handle );
-    $record->load_by_cols ( $column => $value );
+    my $record = $self->new( $self->_new_record_args );
+    $record->load_by_cols( $column => $value );
 
     my $ret = Class::ReturnValue->new();
 
-    if( $record->id ) {
-        $ret->as_array( 0, "Value already exists for unique column $column");
+    if ( $record->id ) {
+        $ret->as_array( 0, "Value already exists for unique column $column" );
         $ret->as_error(
             errno        => 3,
             do_backtrace => 0,
@@ -1404,7 +1610,6 @@
     }
 }
 
-
 =head2 run_canonicalization_for_column column => 'COLUMN', value => 'VALUE'
 
 Runs all canonicalizers for the specified column.
@@ -1413,13 +1618,22 @@
 
 sub run_canonicalization_for_column {
     my $self = shift;
-    my %args = ( column => undef,
-                 value => undef,
-                 @_);
+    my %args = (
+        column => undef,
+        value  => undef,
+        @_
+    );
 
-    my ($ret,$value_ref) = $self->_run_callback ( name => "canonicalize_".$args{'column'}, args => $args{'value'});
+    my ( $ret, $value_ref ) = $self->_run_callback(
+        name => "canonicalize_" . $args{'column'},
+        args => $args{'value'}
+    );
     return unless defined $ret;
-    return ( exists $value_ref->[-1]->[0] ? $value_ref->[-1]->[0] : $args{'value'});
+    return (
+        exists $value_ref->[-1]->[0]
+        ? $value_ref->[-1]->[0]
+        : $args{'value'}
+    );
 }
 
 =head2 has_canonicalizer_for_column COLUMN
@@ -1429,17 +1643,16 @@
 =cut
 
 sub has_canonicalizer_for_column {
-    my $self = shift;
-    my $key = shift;
-        my $method = "canonicalize_$key";
-     if( $self->can($method) ) {
-         return 1;
-     } else {
-         return undef;
-     }
+    my $self   = shift;
+    my $key    = shift;
+    my $method = "canonicalize_$key";
+    if ( $self->can($method) ) {
+        return 1;
+    } else {
+        return undef;
+    }
 }
 
-
 =head2 run_validation_for_column column => 'COLUMN', value => 'VALUE'
 
 Runs all validators for the specified column.
@@ -1453,19 +1666,18 @@
         value  => undef,
         @_
     );
-    my $key    = $args{'column'};
-    my $attr   = $args{'value'};
+    my $key  = $args{'column'};
+    my $attr = $args{'value'};
 
+    my ( $ret, $results )
+        = $self->_run_callback( name => "validate_" . $key, args => $attr );
 
-    my ($ret, $results)  = $self->_run_callback( name => "validate_".$key, args => $attr );
-
-    if (defined $ret) {
+    if ( defined $ret ) {
         return ( 1, 'Validation ok' );
+    } else {
+        return ( @{ $results->[-1] } );
     }
-    else {
-        return (@{ $results->[-1]});
-    }
-    
+
 }
 
 =head2 has_validator_for_column COLUMN
@@ -1484,7 +1696,6 @@
     }
 }
 
-
 sub _run_callback {
     my $self = shift;
     my %args = (
@@ -1498,14 +1709,15 @@
     my @results;
     if ( my $func = $self->can($method) ) {
         @results = $func->( $self, $args{args} );
-        return ( wantarray ? ( undef, [[@results]] ) : undef )
+        return ( wantarray ? ( undef, [ [@results] ] ) : undef )
             unless $results[0];
     }
     $ret = $self->call_trigger( $args{'name'} => $args{args} );
     return (
         wantarray
         ? ( $ret, [ [@results], @{ $self->last_trigger_results } ] )
-        : $ret );
+        : $ret
+    );
 }
 
 1;
@@ -1516,7 +1728,9 @@
 
 =head1 AUTHOR
 
-Jesse Vincent <jesse at bestpractical.com>, Alex Vandiver <alexmv at bestpractical.com>, David Glasser <glasser at bestpractical.com>, Ruslan Zakirov <ruslan.zakirov at gmail.com>
+Jesse Vincent <jesse at bestpractical.com>, Alex Vandiver
+<alexmv at bestpractical.com>, David Glasser <glasser at bestpractical.com>,
+Ruslan Zakirov <ruslan.zakirov at gmail.com>
 
 Based on DBIx::SearchBuilder::Record, whose credits read:
 
@@ -1526,8 +1740,6 @@
 
 =head1 SEE ALSO
 
-L<Jifty::DBI>
+L<Jifty::DBI>, L<Jifty::DBI::Handle>, L<Jifty::DBI::Collection>.
 
 =cut
-
-

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Record/Plugin.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Record/Plugin.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Record/Plugin.pm	Mon Dec 10 17:40:42 2007
@@ -15,6 +15,11 @@
   package MyApp::FavoriteColor;
   use base qw/ Jifty::DBI::Record::Plugin /;
 
+  # Define which methods you want to put in the host model
+  our @EXPORT = qw(
+      favorite_complementary_color
+  );
+
   use Jifty::DBI::Schema;
   use Jifty::DBI::Record schema {
       column favorite_color =>
@@ -23,6 +28,16 @@
           valid_values are qw/ red green blue yellow /;
   };
 
+  sub favorite_complementary_color {
+      my $self = shift; # whatever host object thing we've mixed with
+      my $color = $self->favorite_color;
+      return $color eq 'red'    ? 'green'
+           : $color eq 'green'  ? 'red'
+           : $color eq 'blue'   ? 'orange'
+           : $color eq 'yellow' ? 'purple'
+           :                      undef;
+  }
+
   # Use the mixin
   package MyApp::Model::User;
 
@@ -44,6 +59,14 @@
       return "The favorite color of $name is $color.";
   }
 
+  sub name_and_complementary_color {
+      my $self  = shift;
+      my $name  = $self->name;
+      my $color = $self->favorite_complementary_color;
+
+      return "The complement of $name's favorite color is $color.";
+  }
+
 =head1 DESCRIPTION
 
 By using this package you may provide models that are built from one or more mixins. In fact, your whole table could be defined in the mixins without a single column declared within the model class itself.
@@ -63,6 +86,27 @@
           default is 'african';
   };
 
+=head3 @EXPORT
+
+A mixin may define an C<@EXPORT> variable, which works exactly as advertised in L<Exporter>. That is, given the name of any methods or variable names in the mixin, the host model will gain those methods. 
+
+  our @EXPORT = qw( autocomplete_swallow_type );
+
+  sub autocomplete_swallow_type {
+      my $self  = shift;
+      my $value = quotemeta(shift);
+
+      # You should probably find a better way than actually doing this...
+
+      my @values;
+      push @values, 'african'  if 'african'  =~ /$value/;
+      push @values, 'european' if 'european' =~ /$value/;
+
+      return @values;
+  }
+
+That way if you have any custom methods you want to throw into the host model, just define them in the mixin and add them to the C<@EXPORT> variable.
+
 =head3 register_triggers
 
 Your mixin may also want to register triggers for the records to which it will be added. You can do this by defining a method named C<register_triggers>:
@@ -80,7 +124,35 @@
       # do something...
   }
 
-See L<Jifty::DBI::Class::Trigger>.
+See L<Class::Trigger>.
+
+=head3 register_triggers_for_column
+
+In addition to the general L</register_triggers> method described above, the mixin may also implement a C<register_triggers_for_column> method. This is called for each column in the table. This is primarily helpful for registering the C<after_set_*> and C<before_set_*> columns.
+
+For example:
+
+  sub register_triggers_for_column {
+      my $self   = shift;
+      my $column = shift;
+
+      return unless $column ne 'updated_on';
+
+      $self->add_trigger( 
+          name      => 'after_set_'.$column, 
+          callback  => \&touch_update_time,
+          abortable => 1,
+      );
+  }
+
+  sub touch_update_time {
+      my $self = shift;
+      $self->set_updated_on(DateTime->now);
+  }
+
+This has the additional advantage of being callable when new columns are added to a table while the application is running. This can happen when using database-backed models in Jifty (which, as of this writing, has not been released or made part of the development trunk of Jifty, but is part of the virtual-models branch).
+
+See L<Class::Trigger>.
 
 =head2 MODELS USING MIXINS
 
@@ -112,11 +184,19 @@
     if (my $triggers =  $self->can('register_triggers') ) {
         $triggers->($caller)
     }
+
+    if (my $triggers_for_column =  $self->can('register_triggers_for_column') ) {
+        for my $column (map { $_->name } $caller->columns) {
+            $triggers_for_column->($caller, $column)
+        }
+    }
+
+    push(@{ $caller->RECORD_MIXINS }, $self)
 }
 
 =head1 SEE ALSO
 
-L<Jifty::DBI::Record>, L<Jifty::DBI::Class::Trigger>
+L<Jifty::DBI::Record>, L<Class::Trigger>
 
 =head1 LICENSE
 

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Schema.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Schema.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/Schema.pm	Mon Dec 10 17:40:42 2007
@@ -317,11 +317,11 @@
     croak "Illegal column definition for column $name in $from"
       if grep {not UNIVERSAL::isa($_, "Jifty::DBI::Schema::Trait")} @_;
 
-    $column->readable(!(delete $column->{unreadable}));
-    $column->writable(!(delete $column->{immutable}));
+    $column->readable(!(delete $column->attributes->{unreadable}));
+    $column->writable(!(delete $column->attributes->{immutable}));
 
     # XXX: deprecated
-    $column->mandatory(1) if delete $column->{not_null};
+    $column->mandatory(1) if delete $column->attributes->{not_null};
 
     $column->sort_order($SORT_ORDERS->{$from}++);
 
@@ -338,13 +338,18 @@
         }
 
         # Load the class we reference
-        $refclass->require();
-
+        unless (UNIVERSAL::isa($refclass, 'Jifty::DBI::Record') || UNIVERSAL::isa($refclass, 'Jifty::DBI::Collection')) {
+            local $UNIVERSAL::require::ERROR;
+            $refclass->require();
+            die $UNIVERSAL::require::ERROR if ($UNIVERSAL::require::ERROR);
+        }
         # References assume a refernce to an integer ID unless told otherwise
         $column->type('integer') unless ( $column->type );
 
         # A one-to-one or one-to-many relationship is requested
         if ( UNIVERSAL::isa( $refclass, 'Jifty::DBI::Record' ) ) {
+            # Assume we refer to the ID column unless told otherwise
+            $column->by('id') unless $column->by;
 
             # Handle *_id reference columns specially
             if ( $name =~ /(.*)_id$/ ) {
@@ -360,20 +365,19 @@
                 # Note the original column
                 $virtual_column->aliased_as($aliased_as);
                 $virtual_column->alias_for_column($name);
+                $virtual_column->virtual(1);
 
                 # Create the helper methods for the virtual column too
                 $from->_init_methods_for_column($virtual_column);
             }
 
-            # Assume we refer to the ID column unless told otherwise
-            $column->by('id') unless $column->by;
         } elsif ( UNIVERSAL::isa( $refclass, 'Jifty::DBI::Collection' ) ) {
             $column->by('id') unless $column->by;
             $column->virtual('1');
         } else {
-            warn "Error in $from: $refclass neither Record nor Collection";
+            warn "Error in $from: $refclass neither Record nor Collection. Perhaps it couldn't be loaded?";
         }
-    } elsif (my $handler = $column->{_init_handler}) {
+    } elsif (my $handler = $column->attributes->{_init_handler}) {
         $handler->($column, $from);
     } else {
         $column->type('varchar(255)') unless $column->type;

Modified: Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/SchemaGenerator.pm
==============================================================================
--- Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/SchemaGenerator.pm	(original)
+++ Jifty-DBI/branches/index-relationship/lib/Jifty/DBI/SchemaGenerator.pm	Mon Dec 10 17:40:42 2007
@@ -227,7 +227,7 @@
 Returns a string containing a sequence of SQL statements to create tables for all of
 the models added to the SchemaGenerator.
 
-This is just a trivial wrapper around L</create_Table_sql_statements>.
+This is just a trivial wrapper around L</create_table_sql_statements>.
 
 =cut
 
@@ -258,7 +258,6 @@
 
         # Skip "Virtual" columns - (foreign keys to collections)
         next if $column->virtual;
-        next if defined $column->alias_for_column;
 
         # If schema_version is defined, make sure columns are for that version
         if ($model->can('schema_version') and defined $model->schema_version) {

Modified: Jifty-DBI/branches/index-relationship/t/01records.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/01records.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/01records.t	Mon Dec 10 17:40:42 2007
@@ -77,7 +77,6 @@
         my ($val, $msg) = $rec->set_name('Obra');
         ok($val, $msg) ;
         is($rec->name, 'Obra', "We did actually change the name");
-
 # Validate immutability of the column id
         ($val, $msg) = $rec->set_id( $rec->id + 1 );
         ok(!$val, $msg);
@@ -140,7 +139,6 @@
         ($val, $msg) = $newrec->_load_from_sql('SELECT id FROM addresses WHERE id = ?', 0);
         is($val, 0, "didn't find object");
         is($msg, "Couldn't find row", "reason is wrong id");
-
 # _load_from_sql and wrong SQL
         $newrec = TestApp::Address->new( handle => $handle );
         {
@@ -282,9 +280,9 @@
     "CREATE SEQUENCE addresses_seq",
     "CREATE TABLE addresses (
         id integer CONSTRAINT address_key PRIMARY KEY,
-        Name varchar(36),
-        Phone varchar(18),
-        EmployeeId integer
+        name varchar(36),
+        phone varchar(18),
+        employee_id integer
     )",
 ] }
 

Modified: Jifty-DBI/branches/index-relationship/t/01searches.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/01searches.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/01searches.t	Mon Dec 10 17:40:42 2007
@@ -8,7 +8,7 @@
 BEGIN { require "t/utils.pl" }
 our (@available_drivers);
 
-use constant TESTS_PER_DRIVER => 82;
+use constant TESTS_PER_DRIVER => 109;
 
 my $total = scalar(@available_drivers) * TESTS_PER_DRIVER;
 plan tests => $total;
@@ -118,6 +118,40 @@
         isa_ok( $first_rec, 'Jifty::DBI::Record', 'First returns record object' );
         is( $first_rec->login, 'glasser', 'login is correct' );
 
+        # LIKE with wildcard
+        $users_obj->clean_slate;
+        is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
+        $users_obj->limit( column => 'name', operator => 'MATCHES', value => 'G_ass' );
+        is( $users_obj->count, 1, "found one user with 'Glass' in the name" );
+        $first_rec = $users_obj->first;
+        isa_ok( $first_rec, 'Jifty::DBI::Record', 'First returns record object' );
+        is( $first_rec->login, 'glasser', 'login is correct' );
+
+        # LIKE with escaped wildcard
+        $users_obj->clean_slate;
+        is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
+        # XXX: don't use backslashes; Pg (only Pg?) requires special
+        # treatment like "LIKE E'%g\\_ass%'" for that case, 
+        # which is not supported yet (but this should be fixed)
+        $users_obj->limit( column => 'name', operator => 'MATCHES', value => 'G at _ass', escape => '@' );
+        is( $users_obj->count, 0, "should not find users with 'Glass' in the name" );
+
+        # LIKE with wildcard
+        $users_obj->clean_slate;
+        is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
+        $users_obj->limit( column => 'name', operator => 'MATCHES', value => 'Glass%' );
+        is( $users_obj->count, 1, "found one user with 'Glass' in the name" );
+        $first_rec = $users_obj->first;
+        isa_ok( $first_rec, 'Jifty::DBI::Record', 'First returns record object' );
+        is( $first_rec->login, 'glasser', 'login is correct' );
+
+        # MATCHES with escaped wildcard
+        $users_obj->clean_slate;
+        is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
+        # XXX: don't use backslashes; reason above
+        $users_obj->limit( column => 'name', operator => 'MATCHES', value => 'Glass@%', escape => '@' );
+        is( $users_obj->count, 0, "should not find users with 'Glass' in the name" );
+
         # STARTSWITH
         $users_obj->clean_slate;
         is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
@@ -136,6 +170,19 @@
         isa_ok( $first_rec, 'Jifty::DBI::Record', 'First returns record object' );
         is( $first_rec->login, 'audreyt', 'login is correct' );
 
+        # IN
+        $users_obj->clean_slate;
+        is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
+        $users_obj->limit( column => 'login', operator => 'IN', 
+                           value => ['cubic', 'obra', 'glasser', 'audreyt'] );
+        is( $users_obj->count, 4, "found 4 user ids" );
+        my %logins = (cubic => 1, obra => 1, glasser => 1, audreyt => 1);
+        while ( my $user = $users_obj->next ) {
+          is ( defined $logins{$user->login}, 1, 'Found login' );
+          delete $logins{$user->login};
+        }
+        is ( scalar( keys( %logins ) ), 0, 'All logins found' );
+
         # IS NULL
         # XXX TODO FIXME: column => undef should be handled as NULL
         $users_obj->clean_slate;
@@ -166,6 +213,14 @@
         is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
         $users_obj->limit( column => 'name', value => 'jesse vincent' );
         is( $users_obj->count, 1, "case insensitive, non-matched case, should find one row");
+        $users_obj->clean_slate;
+        is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
+        $users_obj->limit( column => 'name', value => ['Jesse Vincent', 'Audrey Tang'], operator => 'IN');
+        is( $users_obj->count, 2, "case insensitive, matching case, should find two rows");
+        $users_obj->clean_slate;
+        is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
+        $users_obj->limit( column => 'name', value => ['jesse vincent', 'audrey tang'], operator => 'IN');
+        is( $users_obj->count, 2, "case insensitive, non-matched case, should find two rows");
 
         # CASE SENSITIVITY, testing with case_sensitive => 1
         $users_obj->clean_slate;
@@ -179,6 +234,19 @@
             local $TODO = "MySQL still needs case sensitive fixes" if ( $d eq 'mysql' || $d eq 'mysqlPP' );
             is( $users_obj->count, 0, "case sensitive search, should find zero rows");
         }
+        $users_obj->clean_slate;
+        is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
+        $users_obj->limit( column => 'name', value => ['Jesse Vincent', 'Audrey Tang'], operator => 'IN',
+                           case_sensitive => 1 );
+        is( $users_obj->count, 2, "case sensitive search, should find two rows");
+        $users_obj->clean_slate;
+        is_deeply( $users_obj, $clean_obj, 'after clean_slate looks like new object');
+        $users_obj->limit( column => 'name', value => ['jesse vincent', 'audrey tang'], operator => 'IN', 
+                           case_sensitive => 1 );
+        TODO: {
+            local $TODO = "MySQL still needs case sensitive fixes" if ( $d eq 'mysql' || $d eq 'mysqlPP' );
+            is( $users_obj->count, 0, "case sensitive search, should find zero rows");
+        }
 
         # ORDER BY / GROUP BY
         $users_obj->clean_slate;
@@ -273,17 +341,17 @@
 
 sub schema_oracle { [
     "CREATE SEQUENCE Users_seq",
-    "CREATE TABLE Users (
-        id integer CONSTRAINT Users_Key PRIMARY KEY,
+    "CREATE TABLE users (
+        id integer CONSTRAINT users_key PRIMARY KEY,
         Login varchar(18) NOT NULL,
-        Name varchar(36),
-        Phone varchar(18)
+        name varchar(36),
+        phone varchar(18)
     )",
 ] }
 
 sub cleanup_schema_oracle { [
-    "DROP SEQUENCE Users_seq",
-    "DROP TABLE Users", 
+    "DROP SEQUENCE users_seq",
+    "DROP TABLE users", 
 ] }
 
 

Modified: Jifty-DBI/branches/index-relationship/t/02records_cachable.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/02records_cachable.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/02records_cachable.t	Mon Dec 10 17:40:42 2007
@@ -34,7 +34,7 @@
             isa_ok( $rec, 'Jifty::DBI::Record' );
 
             my ($id)
-                = $rec->create( Name => 'Jesse', Phone => '617 124 567' );
+                = $rec->create( name => 'Jesse', phone => '617 124 567' );
             ok( $id, "Created record #$id" );
 
             ok( $rec->load($id), "Loaded the record" );
@@ -52,7 +52,7 @@
 
         {    # load by name then load by id, check that we fetch from hash
             my $rec = TestApp::Address->new( handle => $handle );
-            ok( $rec->load_by_cols( Name => 'Jesse' ), "Loaded the record" );
+            ok( $rec->load_by_cols( name => 'Jesse' ), "Loaded the record" );
             is( $rec->name, 'Jesse', "The record's name is Jesse" );
 
             my $rec_cache = TestApp::Address->new( handle => $handle );
@@ -66,44 +66,44 @@
 
         {    # load_by_cols and undef, 0 or '' values
             my $rec = TestApp::Address->new( handle => $handle );
-            my ($id) = $rec->create( Name => 'EmptyPhone', Phone => '' );
+            my ($id) = $rec->create( name => 'Emptyphone', phone => '' );
             ok( $id, "Created record #$id" );
-            ($id) = $rec->create( Name => 'ZeroPhone', Phone => 0 );
+            ($id) = $rec->create( name => 'Zerophone', phone => 0 );
             ok( $id, "Created record #$id" );
-            ($id) = $rec->create( Name => 'UndefPhone', Phone => undef );
+            ($id) = $rec->create( name => 'Undefphone', phone => undef );
             ok( $id, "Created record #$id" );
 
             Jifty::DBI::Record::Cachable->flush_cache;
 
-            ok( $rec->load_by_cols( Phone => undef ), "Loaded the record" );
-            is( $rec->name, 'UndefPhone', "UndefPhone record" );
+            ok( $rec->load_by_cols( phone => undef ), "Loaded the record" );
+            is( $rec->name, 'Undefphone', "Undefphone record" );
 
-            is( $rec->phone, undef, "Phone number is undefined" );
+            is( $rec->phone, undef, "phone number is undefined" );
 
-            ok( $rec->load_by_cols( Phone => '' ), "Loaded the record" );
-            is( $rec->name,  'EmptyPhone', "EmptyPhone record" );
-            is( $rec->phone, '',           "Phone number is empty string" );
-
-            ok( $rec->load_by_cols( Phone => 0 ), "Loaded the record" );
-            is( $rec->name,  'ZeroPhone', "ZeroPhone record" );
-            is( $rec->phone, 0,           "Phone number is zero" );
+            ok( $rec->load_by_cols( phone => '' ), "Loaded the record" );
+            is( $rec->name,  'Emptyphone', "Emptyphone record" );
+            is( $rec->phone, '',           "phone number is empty string" );
+
+            ok( $rec->load_by_cols( phone => 0 ), "Loaded the record" );
+            is( $rec->name,  'Zerophone', "Zerophone record" );
+            is( $rec->phone, 0,           "phone number is zero" );
 
      # XXX: next thing fails, looks like operator is mandatory
-     # ok($rec->load_by_cols( Phone => { value => 0 } ), "Loaded the record");
+     # ok($rec->load_by_cols( phone => { value => 0 } ), "Loaded the record");
             ok( $rec->load_by_cols(
-                    Phone => { operator => '=', value => 0 }
+                    phone => { operator => '=', value => 0 }
                 ),
                 "Loaded the record"
             );
-            is( $rec->name,  'ZeroPhone', "ZeroPhone record" );
-            is( $rec->phone, 0,           "Phone number is zero" );
+            is( $rec->name,  'Zerophone', "Zerophone record" );
+            is( $rec->phone, 0,           "phone number is zero" );
         }
 
         Jifty::DBI::Record::Cachable->flush_cache;
 
         {    # case insensetive columns names
             my $rec = TestApp::Address->new( handle => $handle );
-            ok( $rec->load_by_cols( Name => 'Jesse' ), "Loaded the record" );
+            ok( $rec->load_by_cols( name => 'Jesse' ), "Loaded the record" );
             is( $rec->name, 'Jesse', "loaded record" );
 
             my $rec_cache = TestApp::Address->new( handle => $handle );
@@ -117,6 +117,7 @@
         Jifty::DBI::Record::Cachable->flush_cache;
 
         cleanup_schema( 'TestApp::Address', $handle );
+        disconnect_handle($handle);
     }
 }    # SKIP, foreach blocks
 

Modified: Jifty-DBI/branches/index-relationship/t/02records_object.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/02records_object.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/02records_object.t	Mon Dec 10 17:40:42 2007
@@ -31,7 +31,7 @@
         isa_ok($ret,'DBI::st', "Inserted the schema. got a statement handle back" );}
 
         my $emp = TestApp::Employee->new( handle => $handle );
-        my $e_id = $emp->create( Name => 'RUZ' );
+        my $e_id = $emp->create( name => 'RUZ' );
         ok($e_id, "Got an id for the new emplyee");
         my $phone = TestApp::Phone->new( handle => $handle );
         isa_ok( $phone, 'TestApp::Phone', "it's a TestApp::Phone");
@@ -40,7 +40,7 @@
         is($p_id, 1, "Loaded record $p_id");
         $phone->load( $p_id );
 
-        my $obj = $phone->employee( handle => $handle );
+        my $obj = $phone->employee();
         ok($obj, "Employee #$e_id has phone #$p_id");
         isa_ok( $obj, 'TestApp::Employee');
         is($obj->id, $e_id);
@@ -106,7 +106,7 @@
     "CREATE SEQUENCE employees_seq",
     "CREATE TABLE employees (
         id integer CONSTRAINT employees_key PRIMARY KEY,
-        Name varchar(36)
+        name varchar(36)
     )",
     "CREATE SEQUENCE phones_seq",
     "CREATE TABLE phones (

Modified: Jifty-DBI/branches/index-relationship/t/02searches_joins.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/02searches_joins.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/02searches_joins.t	Mon Dec 10 17:40:42 2007
@@ -131,8 +131,8 @@
         ),
         "joined table"
     );
-    $users_obj->limit( alias => $groups_alias, column => 'Name', value => 'Developers' );
-    diag $users_obj->build_select_query;
+    $users_obj->limit( alias => $groups_alias, column => 'name', value => 'Developers' );
+    #diag $users_obj->build_select_query;
     is( $users_obj->count, 3, "three members" );
 }
 
@@ -153,7 +153,7 @@
         "joined table"
     );
     $users_obj->limit( alias => $g2u_alias, column => 'group_id', value => "$groups_alias.id", quote_value => 0);
-    $users_obj->limit( alias => $groups_alias, column => 'Name', value => 'Developers' );
+    $users_obj->limit( alias => $groups_alias, column => 'name', value => 'Developers' );
     #diag $users_obj->build_select_query;
     is( $users_obj->count, 3, "three members" );
 }
@@ -213,7 +213,7 @@
 }
 
     cleanup_schema( 'TestApp', $handle );
-
+    disconnect_handle($handle);
 }} # SKIP, foreach blocks
 
 1;

Modified: Jifty-DBI/branches/index-relationship/t/04memcached.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/04memcached.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/04memcached.t	Mon Dec 10 17:40:42 2007
@@ -44,7 +44,7 @@
         # Create a record, load from cache
         my $rec = TestApp::Address->new( handle => $handle );
 
-        my ($id) = $rec->create( Name => 'Jesse', Phone => '617 124 567' );
+        my ($id) = $rec->create( name => 'Jesse', phone => '617 124 567' );
         ok( $id, "Created record #$id" );
 
         ok( $rec->load($id), "Loaded the record" );
@@ -65,6 +65,7 @@
         $rec = TestApp::Address->new( handle => $handle );
         $rec->load($id);
         is($rec->phone, '555 543 6789', "Loaded changed data from cache OK");
+        disconnect_handle($handle);
 }}
 
 package TestApp::Address;

Modified: Jifty-DBI/branches/index-relationship/t/10schema.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/10schema.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/10schema.t	Mon Dec 10 17:40:42 2007
@@ -5,7 +5,7 @@
 use Test::More;
 use version;
 
-use constant TESTS_PER_DRIVER => 48;
+use constant TESTS_PER_DRIVER => 77;
 our @available_drivers;
 
 BEGIN {
@@ -29,7 +29,8 @@
   SKIP: {
     my $address_schema = has_schema('Sample::Address',$d);
     my $employee_schema = has_schema('Sample::Employee',$d);
-    unless ($address_schema && $employee_schema) {
+    my $corporation_schema = has_schema('Sample::Corporation',$d);
+    unless ($address_schema && $employee_schema && $corporation_schema) {
       skip "need to work on $d", TESTS_PER_DRIVER;
     }
     
@@ -87,6 +88,19 @@
                       Sample::Address->$address_schema. Sample::Employee->$employee_schema, 
                       "got the right Address+Employee schema for $d");
     
+    my $corporation = Sample::Corporation->new;
+    
+    isa_ok($corporation, 'Sample::Corporation');
+    can_ok($corporation, qw( name ));
+    
+    $ret = $SG->add_model($corporation);
+
+    ok($ret != 0, "added model from an instantiated object");
+
+    is_ignoring_space($SG->create_table_sql_text, 
+                      Sample::Address->$address_schema. Sample::Employee->$employee_schema . Sample::Corporation->$corporation_schema, 
+                      "got the right Address+Employee+Corporation schema for $d");
+    
     my $manually_make_text = join ' ', map { "$_;" } $SG->create_table_sql_statements;
      is_ignoring_space($SG->create_table_sql_text, 
                        $manually_make_text, 
@@ -126,6 +140,37 @@
                         "got the right Address schema for $d version $version");
     }
 
+    for my $version (qw/ 0.2.0 0.2.4 0.2.6 0.2.8 0.2.9 /) {
+
+        Sample::Corporation->schema_version($version);
+
+        my $SG = Jifty::DBI::SchemaGenerator->new($handle, $version);
+        $SG->add_model('Sample::Corporation');
+
+        my $needs_state
+            = version->new($version) >= $version_024_min
+           && version->new($version) <  $version_024_max;
+
+        ok(Sample::Corporation->COLUMNS->{id}->active, 'id active');
+        ok(Sample::Corporation->COLUMNS->{name}->active, 'name active');
+        if ($needs_state) {
+            ok(Sample::Corporation->COLUMNS->{us_state}->active, "state active for version $version");
+            ok(Sample::Corporation->COLUMNS->{us_state}->mandatory, "state mandatory for version $version");
+        }
+
+        else {
+            ok(!Sample::Corporation->COLUMNS->{us_state}->active, "state not active for version $version");
+            ok(Sample::Corporation->COLUMNS->{us_state}->mandatory, "state still mandatory for version $version");
+        }
+
+        my $corporation_version_schema = $needs_state ? "${corporation_schema}_024"
+            :                                             $corporation_schema;
+
+        is_ignoring_space($SG->create_table_sql_text,
+                        Sample::Corporation->$corporation_version_schema,
+                        "got the right Corporation schema for $d version $version");
+    }
+
     cleanup_schema( 'TestApp', $handle );
     disconnect_handle( $handle );
 }

Modified: Jifty-DBI/branches/index-relationship/t/11schema_records.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/11schema_records.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/11schema_records.t	Mon Dec 10 17:40:42 2007
@@ -31,7 +31,7 @@
         isa_ok($ret,'DBI::st', "Inserted the schema. got a statement handle back" );}
 
         my $emp = TestApp::Employee->new( handle => $handle );
-        my $e_id = $emp->create( Name => 'RUZ' );
+        my $e_id = $emp->create( name => 'RUZ' );
         ok($e_id, "Got an id for the new employee: $e_id");
         $emp->load($e_id);
         is($emp->id, $e_id);

Modified: Jifty-DBI/branches/index-relationship/t/12prefetch.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/12prefetch.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/12prefetch.t	Mon Dec 10 17:40:42 2007
@@ -1,6 +1,5 @@
 #!/usr/bin/env perl -w
 
-
 use strict;
 use warnings;
 use File::Spec;
@@ -8,115 +7,175 @@
 
 BEGIN { require "t/utils.pl" }
 our (@available_drivers);
-use constant TESTS_PER_DRIVER => 41;
+use constant TESTS_PER_DRIVER => 58;
 
 my $total = scalar(@available_drivers) * TESTS_PER_DRIVER;
-#plan tests => $total;
-plan tests => TESTS_PER_DRIVER;
 
-foreach my $d ('SQLite'){ # @available_drivers ) {
+plan tests => $total;
+
+foreach my $d (@available_drivers) {
 SKIP: {
-        unless( has_schema( 'TestApp', $d ) ) {
-                skip "No schema for '$d' driver", TESTS_PER_DRIVER;
+        unless ( has_schema( 'TestApp', $d ) ) {
+            skip "No schema for '$d' driver", TESTS_PER_DRIVER;
         }
-        unless( should_test( $d ) ) {
-                skip "ENV is not defined for driver '$d'", TESTS_PER_DRIVER;
+        unless ( should_test($d) ) {
+            skip "ENV is not defined for driver '$d'", TESTS_PER_DRIVER;
         }
 
-        my $handle = get_handle( $d );
-        connect_handle( $handle );
-        isa_ok($handle->dbh, 'DBI::db', "Got handle for $d");
-
-        {my $ret = init_schema( 'TestApp', $handle );
-        isa_ok($ret,'DBI::st', "Inserted the schema. got a statement handle back" );}
+        my $handle = get_handle($d);
+        connect_handle($handle);
+        isa_ok( $handle->dbh, 'DBI::db', "Got handle for $d" );
+
+        {
+            my $ret = init_schema( 'TestApp', $handle );
+            isa_ok( $ret, 'DBI::st',
+                "Inserted the schema. got a statement handle back" );
+        }
 
-        
-        my $emp = TestApp::Employee->new( handle => $handle );
-        my $e_id = $emp->create( Name => 'RUZ' );
-        ok($e_id, "Got an id for the new employee: $e_id");
+        my $emp  = TestApp::Employee->new( handle => $handle );
+        my $e_id = $emp->create( name             => 'RUZ' );
+        ok( $e_id, "Got an id for the new employee: $e_id" );
         $emp->load($e_id);
-        is($emp->id, $e_id);
-        
+        is( $emp->id, $e_id );
+
         my $phone_collection = $emp->phones;
-        isa_ok($phone_collection, 'TestApp::PhoneCollection');
-        { 
-        my $phone = TestApp::Phone->new( handle => $handle );
-        isa_ok( $phone, 'TestApp::Phone');
-        my $p_id = $phone->create( employee => $e_id, phone => '+7(903)264-03-51');
-        is($p_id, 1, "Loaded phone $p_id");
-        $phone->load( $p_id );
-
-        my $obj = $phone->employee;
-
-        ok($obj, "Employee #$e_id has phone #$p_id");
-        isa_ok( $obj, 'TestApp::Employee');
-        is($obj->id, $e_id);
-        is($obj->name, 'RUZ');
-        }
-
-        my $emp2 = TestApp::Employee->new( handle => $handle );
-        my $e2_id = $emp2->create( Name => 'JESSE' );
-        my $phone2 = TestApp::Phone->new( handle => $handle );
-        my $p2_id = $phone2->create( employee => $e2_id, phone => '+16173185823');
-
-        for (3..6){
-        my $i = $_;
-        diag("loading phone $i");
-        my $phone = TestApp::Phone->new( handle => $handle );
-        isa_ok( $phone, 'TestApp::Phone');
-        my $p_id = $phone->create( employee => $e_id, phone => "+1 $i");
-        is($p_id, $i, "Loaded phone $p_id");
-        $phone->load( $p_id );
-
-        my $obj = $phone->employee;
-
-        ok($obj, "Employee #$e_id has phone #$p_id");
-        isa_ok( $obj, 'TestApp::Employee');
-        is($obj->id, $e_id);
-        is($obj->name, 'RUZ');
-         
-        
-       } 
-        
+        isa_ok( $phone_collection, 'TestApp::PhoneCollection' );
+        {
+            my $phone = TestApp::Phone->new( handle => $handle );
+            isa_ok( $phone, 'TestApp::Phone' );
+            my $p_id = $phone->create(
+                employee => $e_id,
+                phone    => '+7(903)264-03-51'
+            );
+            is( $p_id, 1, "Loaded phone $p_id" );
+            $phone->load($p_id);
+
+            my $obj = $phone->employee;
+
+            ok( $obj, "Employee #$e_id has phone #$p_id" );
+            isa_ok( $obj, 'TestApp::Employee' );
+            is( $obj->id,   $e_id );
+            is( $obj->name, 'RUZ' );
+        }
+
+        my $emp2   = TestApp::Employee->new( handle => $handle );
+        my $e2_id  = $emp2->create( name            => 'JESSE' );
+        my $phone2 = TestApp::Phone->new( handle    => $handle );
+        my $p2_id
+            = $phone2->create( employee => $e2_id, phone => '+16173185823' );
+
+        for ( 3 .. 6 ) {
+            my $i = $_;
+            my $phone = TestApp::Phone->new( handle => $handle );
+            isa_ok( $phone, 'TestApp::Phone' );
+            my $p_id = $phone->create( employee => $e_id, phone => "+1 $i" );
+            is( $p_id, $i, "Loaded phone $p_id" );
+            $phone->load($p_id);
+
+            my $obj = $phone->employee;
+
+            ok( $obj, "Employee #$e_id has phone #$p_id" );
+            isa_ok( $obj, 'TestApp::Employee' );
+            is( $obj->id,   $e_id );
+            is( $obj->name, 'RUZ' );
+
+        }
+
         $handle->log_sql_statements(1);
 
-        my $collection
-            = TestApp::EmployeeCollection->new( handle => $handle );
-        my $phones_alias = $collection->join(
-            alias1  => 'main',
-            column1 => 'id',
-            table2  => 'phones',
-            column2 => 'employee'
-        );
-        $collection->prefetch( $phones_alias => 'phones'); #
-        $collection->limit( column => 'id', value => '1', operator => '>=' );
-        my $user = $collection->next;
-        is( $user->id, 1, "got our user" );
-        my $phones = $user->phones;
-        is( $phones->first->id, 1 );
-        is( $phones->count, 5 );
-
-
-        my $jesse = $collection->next;
-        is ($jesse->name, 'JESSE');
-        my $jphone = $jesse->phones;
-        is ($jphone->count,1);
+        {    # Old prefetch syntax
+            $handle->clear_sql_statement_log;
+            my $collection
+                = TestApp::EmployeeCollection->new( handle => $handle );
+            $collection->unlimit;
+            my $phones_alias = $collection->join(
+                alias1  => 'main',
+                column1 => 'id',
+                table2  => 'phones',
+                column2 => 'employee'
+            );
+            $collection->prefetch( $phones_alias => 'phones' );
+            is( $collection->count, 2 );
+            is( scalar( $handle->sql_statement_log ),
+                1, "count is one statement" );
+
+            $handle->clear_sql_statement_log;
+            my $user = $collection->next;
+            is( $user->id, 1, "got our user" );
+            my $phones = $user->phones;
+            is( $phones->first->id, 1 );
+            is( $phones->count, 5 );
+
+            my $jesse = $collection->next;
+            is( $jesse->name, 'JESSE' );
+            my $jphone = $jesse->phones;
+            is( $jphone->count, 1 );
+
+            is( scalar( $handle->sql_statement_log ),
+                1, "all that. just one sql statement" );
+        }
+
+        {    # New syntax, one-to-many
+            $handle->clear_sql_statement_log;
+            my $collection
+                = TestApp::EmployeeCollection->new( handle => $handle );
+            $collection->unlimit;
+            $collection->prefetch( name => 'phones' );
+            is( $collection->count, 2 );
+            is( scalar( $handle->sql_statement_log ),
+                1, "count is one statement" );
+
+            $handle->clear_sql_statement_log;
+            my $user = $collection->next;
+            is( $user->id, 1, "got our user" );
+            my $phones = $user->phones;
+            is( $phones->first->id, 1 );
+            is( $phones->count, 5 );
+
+            my $jesse = $collection->next;
+            is( $jesse->name, 'JESSE' );
+            my $jphone = $jesse->phones;
+            is( $jphone->count, 1 );
+
+            is( scalar( $handle->sql_statement_log ),
+                1, "all that. just one sql statement" );
+        }
 
-        my @statements = $handle->sql_statement_log;
+        {    # New syntax, one-to-one
+            $handle->clear_sql_statement_log;
+            my $collection
+                = TestApp::PhoneCollection->new( handle => $handle );
+            $collection->unlimit;
+            $collection->prefetch( name => 'employee' );
+            is( $collection->count, 6 );
+            is( scalar( $handle->sql_statement_log ),
+                1, "count is one statement" );
+
+            $handle->clear_sql_statement_log;
+            my $phone = $collection->next;
+            is( $phone->id, 1, "Got a first phone" );
+            is( $phone->phone, '+7(903)264-03-51', "Got ruz's phone number" );
+            my $employee = $phone->employee;
+            is( $employee->id, 1 );
+            is( $employee->name, "RUZ", "Employee matches" );
 
-        is (scalar @statements, 1, "all that. just one sql statement");
+            is( scalar( $handle->sql_statement_log ),
+                1, "all that. just one sql statement" );
+        }
 
         cleanup_schema( 'TestApp', $handle );
-        disconnect_handle( $handle );
-}} # SKIP, foreach blocks
+        disconnect_handle($handle);
+    }
 
-1;
 
+}    # SKIP, foreach blocks
+
+1;
 
 package TestApp;
+
 sub schema_sqlite {
-[
-q{
+    [   q{
 CREATE table employees (
         id integer primary key,
         name varchar(36)
@@ -127,11 +186,11 @@
         employee integer NOT NULL,
         phone varchar(18)
 ) }
-]
+    ];
 }
 
 sub schema_mysql {
-[ q{
+    [   q{
 CREATE TEMPORARY table employees (
         id integer AUTO_INCREMENT primary key,
         name varchar(36)
@@ -142,11 +201,12 @@
         employee integer NOT NULL,
         phone varchar(18)
 )
-} ]
+}
+    ];
 }
 
 sub schema_pg {
-[ q{
+    [   q{
 CREATE TEMPORARY table employees (
         id serial PRIMARY KEY,
         name varchar
@@ -157,34 +217,33 @@
         employee integer references employees(id),
         phone varchar
 )
-} ]
 }
-
+    ];
+}
 
 package TestApp::PhoneCollection;
 use base qw/Jifty::DBI::Collection/;
 
 sub table {
     my $self = shift;
-    my $tab = $self->new_item->table();
+    my $tab  = $self->new_item->table();
     return $tab;
 }
 
-
 package TestApp::Employee;
 use base qw/Jifty::DBI::Record/;
 
-sub _value  {
-  my $self = shift;
-  my $x =  ($self->__value(@_));
-  return $x;
+sub _value {
+    my $self = shift;
+    my $x    = ( $self->__value(@_) );
+    return $x;
 }
 
 BEGIN {
     use Jifty::DBI::Schema;
     use Jifty::DBI::Record schema {
-    column name => type is 'varchar';
-    column phones => references TestApp::PhoneCollection by 'employee';
+        column name   => type is 'varchar';
+        column phones => references TestApp::PhoneCollection by 'employee';
     }
 }
 
@@ -194,8 +253,8 @@
 BEGIN {
     use Jifty::DBI::Schema;
     use Jifty::DBI::Record schema {
-    column employee => references TestApp::Employee;
-    column phone    => type is 'varchar';
+        column employee => references TestApp::Employee;
+        column phone    => type is 'varchar';
     }
 }
 
@@ -203,6 +262,4 @@
 
 use base qw/Jifty::DBI::Collection/;
 
-
-
 1;

Modified: Jifty-DBI/branches/index-relationship/t/14handle-pg.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/14handle-pg.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/14handle-pg.t	Mon Dec 10 17:40:42 2007
@@ -15,7 +15,7 @@
 package Foo::Bar::Collection;
 our @ISA = 'Jifty::DBI::Collection';
 
-sub _preload_columns { "blah" }
+sub query_columns { "blah" }
 sub table { "bars" }
 
 package main;

Modified: Jifty-DBI/branches/index-relationship/t/17virtualtypes.t
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/17virtualtypes.t	(original)
+++ Jifty-DBI/branches/index-relationship/t/17virtualtypes.t	Mon Dec 10 17:40:42 2007
@@ -38,6 +38,7 @@
         is($rec->location_x, 10);
         is($rec->location_y, 20);
         is_deeply($rec->location, { x => 10, y => 20});
+        disconnect_handle($handle);
     }
 }
 
@@ -87,7 +88,6 @@
     my ($column, $from) = @_;
     my $name = $column->name;
     $column->virtual(1);
-    use Data::Dumper;
     for (qw(x y)) {
         Jifty::DBI::Schema::_init_column_for(
             Jifty::DBI::Column->new({ type => 'double',

Added: Jifty-DBI/branches/index-relationship/t/18triggers.t
==============================================================================
--- (empty file)
+++ Jifty-DBI/branches/index-relationship/t/18triggers.t	Mon Dec 10 17:40:42 2007
@@ -0,0 +1,220 @@
+#!/usr/bin/env perl -w
+
+use strict;
+use warnings;
+use File::Spec;
+use Test::More;
+BEGIN { require "t/utils.pl" }
+our (@available_drivers);
+
+use constant TESTS_PER_DRIVER => 62;
+
+my $total = scalar(@available_drivers) * TESTS_PER_DRIVER;
+plan tests => $total;
+
+foreach my $d ( @available_drivers ) {
+SKIP: {
+    unless (has_schema('TestApp::Address', $d)) {
+        skip "No schema for '$d' driver", TESTS_PER_DRIVER;
+    }
+    unless (should_test($d)) {
+        skip "ENV is not defined for driver '$d'", TESTS_PER_DRIVER;
+    }
+
+    my $handle = get_handle($d);
+    connect_handle($handle);
+    isa_ok($handle->dbh, 'DBI::db');
+
+    {my $ret = init_schema('TestApp::Address', $handle);
+    isa_ok($ret, 'DBI::st', "Inserted the schema. got a statement handle back");}
+
+    my $rec = TestApp::Address->new( handle => $handle );
+    isa_ok($rec, 'Jifty::DBI::Record');
+
+    my $rid = $rec->create(
+        name  => 'Sterling',
+        phone => '123 456 7890',
+    );
+    ok($rid, 'created a record');
+    $rec->load($rid);
+    ok($rec->id, 'loaded a record');
+
+    $rec->set_name('zostay');
+    $rec->set_phone('098 765 4321');
+
+    my $ret = $rec->delete;
+    ok($ret, 'deleted a record');
+    disconnect_handle($handle);
+};
+}
+
+package TestApp::TestMixin;
+use base qw/ Jifty::DBI::Record::Plugin /;
+
+use Jifty::DBI::Schema;
+use Jifty::DBI::Record schema { };
+
+use Test::More;
+
+sub register_triggers {
+    my $self = shift;
+
+    $self->add_trigger(before_create => sub {
+        my $self = shift;
+        my $columns = shift;
+        isa_ok($self, 'TestApp::Address');
+        is(ref $columns, 'HASH', 'arg is a hash');
+        is(scalar(keys %$columns), 2, 'arg has 2 keys');
+        is($columns->{name}, 'Sterling', 'name is Sterling');
+        is($columns->{phone}, '123 456 7890', 'phone is 123 456 7890');
+    });
+    $self->add_trigger(after_create => sub {
+        my $self = shift;
+        my $ret = shift;
+        isa_ok($self, 'TestApp::Address');
+        is(ref $ret, 'SCALAR', 'arg is a scalar ref');
+        ok($$ret, 'create was sucessful');
+    });
+    $self->add_trigger(before_set => sub {
+        my $self = shift;
+        my $arg = shift;
+        isa_ok($self, 'TestApp::Address');
+        is(ref $arg, 'HASH', 'arg is a hash');
+        is(scalar(keys %$arg), 3, 'hash has 2 keys');
+        ok($arg->{column}, "column arg is set");
+        ok($arg->{value}, "value arg set");
+        is($arg->{is_sql_function}, undef, 'is_sql_function is undef');
+    });
+    $self->add_trigger(after_set => sub {
+        my $self = shift;
+        my $arg = shift;
+        isa_ok($self, 'TestApp::Address');
+        is(ref $arg, 'HASH', 'arg is a hash');
+        is(scalar(keys %$arg), 2, 'hash has 2 keys');
+        ok($arg->{column}, "column arg is set");
+        ok($arg->{value}, "value arg is set");
+    });
+    $self->add_trigger(before_delete => sub {
+        my $self = shift;
+        isa_ok($self, 'TestApp::Address');
+    });
+    $self->add_trigger(after_delete => sub {
+        my $self = shift;
+        my $ret = shift;
+        isa_ok($self, 'TestApp::Address');
+        is(ref $ret, 'SCALAR', 'arg is a scalar ref');
+        ok($$ret, 'delete was successful');
+    });
+}
+
+sub register_triggers_for_column {
+    my $self   = shift;
+    my $column = shift;
+
+    my $value = $column eq 'name' ? 'zostay'
+              :                     '098 765 4321';
+
+    $self->add_trigger('before_set_'.$column => sub {
+        my $self = shift;
+        my $arg = shift;
+        isa_ok($self, 'TestApp::Address');
+        is(ref $arg, 'HASH', 'arg is a hash');
+        is(scalar(keys %$arg), 3, 'hash has 2 keys');
+        is($arg->{column}, $column, "column arg is $column");
+        is($arg->{value}, $value, "value arg is $value");
+        is($arg->{is_sql_function}, undef, 'is_sql_function is undef');
+    });
+    $self->add_trigger('after_set_'.$column => sub {
+        my $self = shift;
+        my $arg = shift;
+        isa_ok($self, 'TestApp::Address');
+        is(ref $arg, 'HASH', 'arg is a hash');
+        is(scalar(keys %$arg), 2, 'hash has 2 keys');
+        is($arg->{column}, $column, "column arg is $column");
+        is($arg->{value}, $value, "value arg is $value");
+    });
+}
+
+1;
+
+package TestApp::Address;
+use base qw/ Jifty::DBI::Record /;
+
+sub schema_mysql {
+<<EOF;
+CREATE TEMPORARY table addresses (
+        id integer AUTO_INCREMENT,
+        name varchar(36),
+        phone varchar(18),
+        address varchar(50),
+        employee_id int(8),
+        PRIMARY KEY (id))
+EOF
+
+}
+
+sub schema_pg {
+<<EOF;
+CREATE TEMPORARY table addresses (
+        id serial PRIMARY KEY,
+        name varchar,
+        phone varchar,
+        address varchar,
+        employee_id integer
+)
+EOF
+
+}
+
+sub schema_sqlite {
+
+<<EOF;
+CREATE table addresses (
+        id  integer primary key,
+        name varchar(36),
+        phone varchar(18),
+        address varchar(50),
+        employee_id int(8))
+EOF
+
+}
+
+sub schema_oracle { [
+    "CREATE SEQUENCE addresses_seq",
+    "CREATE TABLE addresses (
+        id integer CONSTRAINT address_key PRIMARY KEY,
+        name varchar(36),
+        phone varchar(18),
+        employee_id integer
+    )",
+] }
+
+sub cleanup_schema_oracle { [
+    "DROP SEQUENCE addresses_seq",
+    "DROP TABLE addresses",
+] }
+
+1;
+
+BEGIN {
+use Jifty::DBI::Schema;
+use Jifty::DBI::Record schema {
+
+column name =>
+  till 999,
+  type is 'varchar(14)';
+
+column phone =>
+  type is 'varchar(18)';
+
+column address =>
+  type is 'varchar(50)',
+  default is '';
+
+column employee_id =>
+  type is 'int(8)';
+};
+TestApp::TestMixin->import();
+}
+1;
+

Added: Jifty-DBI/branches/index-relationship/t/19reference.t
==============================================================================
--- (empty file)
+++ Jifty-DBI/branches/index-relationship/t/19reference.t	Mon Dec 10 17:40:42 2007
@@ -0,0 +1,202 @@
+#!/usr/bin/env perl -w
+
+use strict;
+
+use Test::More;
+BEGIN { require "t/utils.pl" }
+our (@available_drivers);
+
+use constant TESTS_PER_DRIVER => 13;
+
+my $total = scalar(@available_drivers) * TESTS_PER_DRIVER;
+plan tests => $total;
+
+foreach my $d ( @available_drivers ) {
+SKIP: {
+        unless( has_schema( 'TestApp::User', $d ) ) {
+                skip "No schema for '$d' driver", TESTS_PER_DRIVER;
+        }
+        unless( should_test( $d ) ) {
+                skip "ENV is not defined for driver '$d'", TESTS_PER_DRIVER;
+        }
+        diag("start testing with '$d' handle") if $ENV{TEST_VERBOSE};
+
+        my $handle = get_handle( $d );
+        connect_handle( $handle );
+        isa_ok($handle->dbh, 'DBI::db');
+
+        {my $ret = init_schema( 'TestApp::User', $handle );
+        isa_ok($ret,'DBI::st', "Inserted the schema. got a statement handle back" );}
+
+        {my $ret = init_schema( 'TestApp::Currency', $handle );
+        isa_ok($ret,'DBI::st', "Inserted the schema. got a statement handle back" );}
+
+        {my $ret = init_schema( 'TestApp::Food', $handle );
+        isa_ok($ret,'DBI::st', "Inserted the schema. got a statement handle back" );}
+
+        my $rec = TestApp::Currency->new( handle => $handle );
+        isa_ok($rec, 'Jifty::DBI::Record');
+
+        my ($id) = $rec->create( name => "USD" );
+
+        $rec = TestApp::Food->new( handle => $handle );
+        isa_ok($rec, 'Jifty::DBI::Record');
+
+        my ($paella) = $rec->create( name => "paella" );
+        $rec->create( name => "nigiri" );
+
+        $rec = TestApp::User->new( handle => $handle );
+        ($id) = $rec->create( currency => 'USD' );
+
+        ok($id);
+        ok($rec->load($id), "Loaded the record");
+        isa_ok($rec->currency, 'TestApp::Currency');
+        is($rec->currency->name, 'USD');
+
+        is( $rec->food, undef, 'null_reference option in effect' );
+
+        no warnings 'once';
+        local *TestApp::User::null_reference = sub {0};
+        $rec->load($id);
+        isa_ok($rec->food, 'TestApp::Food', 'referee is null but shuold still return an object');
+        is($rec->food->id, undef);
+}
+}
+
+package TestApp::Currency;
+use base qw/Jifty::DBI::Record/;
+sub schema_sqlite {
+
+<<EOF;
+CREATE table currencies (
+        id integer primary key,
+        name varchar
+)
+EOF
+
+}
+
+sub schema_mysql {
+
+<<EOF;
+CREATE TEMPORARY table currencies (
+        id integer auto_increment primary key,
+        name varchar(50)
+)
+EOF
+
+}
+
+sub schema_pg {
+
+<<EOF;
+CREATE TEMPORARY table currencies (
+        id serial primary key,
+        name varchar
+)
+EOF
+}
+
+use Jifty::DBI::Schema;
+use Jifty::DBI::Record schema {
+
+column name => type is 'varchar';
+
+};
+
+package TestApp::Food;
+use base qw/Jifty::DBI::Record/;
+
+sub schema_sqlite {
+
+<<EOF;
+CREATE table foods (
+        id integer primary key,
+        name varchar
+)
+EOF
+
+}
+
+sub schema_mysql {
+
+<<EOF;
+CREATE TEMPORARY table foods (
+        id integer auto_increment primary key,
+        name varchar(50)
+)
+EOF
+
+}
+
+sub schema_pg {
+
+<<EOF;
+CREATE TEMPORARY table foods (
+        id serial primary key,
+        name varchar
+)
+EOF
+}
+
+use Jifty::DBI::Schema;
+use Jifty::DBI::Record schema {
+
+column name    =>
+  type is 'varchar';
+
+};
+
+
+package TestApp::User;
+use base qw/Jifty::DBI::Record/;
+
+sub schema_sqlite {
+
+<<EOF;
+CREATE table users (
+        id integer primary key,
+        food integer,
+        currency varchar
+)
+EOF
+
+}
+
+sub schema_mysql {
+
+<<EOF;
+CREATE TEMPORARY table users (
+        id integer auto_increment primary key,
+        food integer,
+        currency varchar(50)
+)
+EOF
+
+}
+
+sub schema_pg {
+
+<<EOF;
+CREATE TEMPORARY table users (
+        id serial primary key,
+        food integer,
+        currency varchar
+)
+EOF
+
+}
+
+use Jifty::DBI::Schema;
+use Jifty::DBI::Record schema {
+
+column currency    =>
+  type is 'varchar',
+  refers_to TestApp::Currency by 'name';
+
+column food    =>
+  refers_to TestApp::Food;
+
+};
+
+1;

Added: Jifty-DBI/branches/index-relationship/t/case_sensitivity.t
==============================================================================
--- (empty file)
+++ Jifty-DBI/branches/index-relationship/t/case_sensitivity.t	Mon Dec 10 17:40:42 2007
@@ -0,0 +1,112 @@
+#!/usr/bin/env perl -w
+
+use strict;
+
+use Test::More;
+BEGIN { require "t/utils.pl" }
+our (@available_drivers);
+
+use constant TESTS_PER_DRIVER => 9;
+
+my $total = scalar(@available_drivers) * TESTS_PER_DRIVER;
+plan tests => $total;
+
+use DateTime ();
+
+foreach my $d ( @available_drivers ) {
+SKIP: {
+        unless( has_schema( 'TestApp::User', $d ) ) {
+                skip "No schema for '$d' driver", TESTS_PER_DRIVER;
+        }
+        unless( should_test( $d ) ) {
+                skip "ENV is not defined for driver '$d'", TESTS_PER_DRIVER;
+        }
+        diag("start testing with '$d' handle") if $ENV{TEST_VERBOSE};
+
+        my $handle = get_handle( $d );
+        connect_handle( $handle );
+        isa_ok($handle->dbh, 'DBI::db');
+
+        {my $ret = init_schema( 'TestApp::User', $handle );
+        isa_ok($ret,'DBI::st', "Inserted the schema. got a statement handle back" );}
+
+        my $rec = TestApp::User->new( handle => $handle );
+        isa_ok($rec, 'Jifty::DBI::Record');
+
+        my ($id) = $rec->create( name => 'Foobar', interests => 'Slacking' );
+        ok($id, "Successfuly created ticket");
+
+	$rec->load_by_cols( name => 'foobar');
+    TODO: {
+        local $TODO = "How do we force mysql to be case sensitive?" if ( $d eq 'mysql' || $d eq 'mysqlPP' );
+        is($rec->id, undef);
+    }
+
+	$rec->load_by_cols( name => { value => 'foobar', case_sensitive => 0, operator => '=' });
+	is($rec->id, $id);
+
+	$rec->load_by_cols( name => 'Foobar');
+	is($rec->id, $id);
+
+	$rec->load_by_cols( interests => 'slacking');
+	is($rec->id, $id);;
+
+	$rec->load_by_cols( interests => 'Slacking');
+	is($rec->id, $id);;
+
+        cleanup_schema( 'TestApp', $handle );
+        disconnect_handle( $handle );
+}
+}
+
+package TestApp::User;
+use base qw/Jifty::DBI::Record/;
+
+1;
+
+sub schema_sqlite {
+
+<<EOF;
+CREATE table users (
+        id integer primary key,
+        name varchar,
+        interests varchar
+)
+EOF
+
+}
+
+sub schema_mysql {
+
+<<EOF;
+CREATE TEMPORARY table users (
+        id integer auto_increment primary key,
+        name varchar(255),
+        interests varchar(255)
+)
+EOF
+
+}
+
+sub schema_pg {
+
+<<EOF;
+CREATE TEMPORARY table users (
+        id serial primary key,
+        name varchar,
+        interests varchar
+)
+EOF
+
+}
+
+use Jifty::DBI::Schema;
+
+use Jifty::DBI::Record schema {
+    column name      => type is 'varchar', label is 'Name', is case_sensitive;
+    column interests => type is 'varchar';
+};
+
+
+1;
+

Added: Jifty-DBI/branches/index-relationship/t/metadata.t
==============================================================================
--- (empty file)
+++ Jifty-DBI/branches/index-relationship/t/metadata.t	Mon Dec 10 17:40:42 2007
@@ -0,0 +1,94 @@
+#!/usr/bin/env perl -w
+
+use strict;
+
+use Test::More;
+BEGIN { require "t/utils.pl" }
+our (@available_drivers);
+
+use constant TESTS_PER_DRIVER => 6;
+
+my $total = scalar(@available_drivers) * TESTS_PER_DRIVER;
+plan tests => $total;
+
+use DateTime ();
+
+foreach my $d ( @available_drivers ) {
+SKIP: {
+        unless( has_schema( 'TestApp::User', $d ) ) {
+                skip "No schema for '$d' driver", TESTS_PER_DRIVER;
+        }
+        unless( should_test( $d ) ) {
+                skip "ENV is not defined for driver '$d'", TESTS_PER_DRIVER;
+        }
+        diag("start testing with '$d' handle") if $ENV{TEST_VERBOSE};
+
+        my $handle = get_handle( $d );
+        connect_handle( $handle );
+        isa_ok($handle->dbh, 'DBI::db');
+
+        {my $ret = init_schema( 'TestApp::User', $handle );
+        isa_ok($ret,'DBI::st', "Inserted the schema. got a statement handle back" );}
+
+        my $rec = TestApp::User->new( handle => $handle );
+        isa_ok($rec, 'Jifty::DBI::Record');
+
+	my $col = $rec->column('name');
+	is($col->label, 'Name');
+
+	is($col->attributes->{arbitary_data}, 'fooo');
+
+	is_deeply($col->serialize_metadata, { type => 'varchar', label => 'Name', sort_order => 0, writable => 1, name => 'name', readable => 1 });
+
+        cleanup_schema( 'TestApp', $handle );
+        disconnect_handle( $handle );
+}
+}
+
+package TestApp::User;
+use base qw/Jifty::DBI::Record/;
+
+1;
+
+sub schema_sqlite {
+
+<<EOF;
+CREATE table users (
+        id integer primary key,
+        name varchar
+)
+EOF
+
+}
+
+sub schema_mysql {
+
+<<EOF;
+CREATE TEMPORARY table users (
+        id integer auto_increment primary key,
+        name varchar(255)
+)
+EOF
+
+}
+
+sub schema_pg {
+
+<<EOF;
+CREATE TEMPORARY table users (
+        id serial primary key,
+        name varchar
+)
+EOF
+
+}
+
+use Jifty::DBI::Schema;
+
+use Jifty::DBI::Record schema {
+    column name     => type is 'varchar', label is 'Name', arbitary_data is 'fooo';
+};
+
+
+1;
+

Modified: Jifty-DBI/branches/index-relationship/t/testmodels.pl
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/testmodels.pl	(original)
+++ Jifty-DBI/branches/index-relationship/t/testmodels.pl	Mon Dec 10 17:40:42 2007
@@ -118,4 +118,67 @@
     };
 }
 
+package Sample::Corporation;
+use Jifty::DBI::Schema;
+use Jifty::DBI::Record schema {
+
+column name =>
+    type is 'varchar',
+    is mandatory;
+
+column us_state =>
+    type is 'varchar',
+    is mandatory,
+    since '0.2.4',
+    till '0.2.8';
+
+};
+
+sub schema_sqlite {
+    return q{
+    CREATE TABLE corporations (
+     id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL  ,
+     name varchar NOT NULL
+    ) ;
+    }
+}
+
+sub schema_sqlite_024 {
+    return q{
+    CREATE TABLE corporations (
+     id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL  ,
+     name varchar NOT NULL ,
+     us_state varchar NOT NULL
+    ) ;
+    }
+}
+
+sub schema_pg {
+    return q{
+    CREATE TABLE corporations ( 
+      id serial NOT NULL , 
+      name varchar NOT NULL ,
+      PRIMARY KEY (id)
+    ) ;
+    };
+}
+
+sub schema_pg_024 {
+    return q{
+    CREATE TABLE corporations ( 
+      id serial NOT NULL , 
+      name varchar NOT NULL ,
+      us_state varchar NOT NULL ,
+      PRIMARY KEY (id)
+    ) ;
+    };
+}
+
+sub schema_version {
+    my $class = shift;
+    my $new_schema_version = shift;
+    $schema_version = $new_schema_version if defined $new_schema_version;
+    return $schema_version;
+}
+
 1;

Modified: Jifty-DBI/branches/index-relationship/t/utils.pl
==============================================================================
--- Jifty-DBI/branches/index-relationship/t/utils.pl	(original)
+++ Jifty-DBI/branches/index-relationship/t/utils.pl	Mon Dec 10 17:40:42 2007
@@ -1,7 +1,7 @@
 #!/usr/bin/env perl -w
 
 use strict;
-use File::Spec ();
+use File::Temp ();
 
 =head1 VARIABLES
 
@@ -22,6 +22,9 @@
         Sybase
 );
 
+
+
+
 =head2 @available_drivers
 
 Array that lists only drivers from supported list
@@ -93,13 +96,15 @@
         goto &$call;
 }
 
+
+our $SQLITE_FILENAME;
 sub connect_sqlite
 {
         my $handle = shift;
+        (undef, $SQLITE_FILENAME ) = File::Temp::tempfile();
         return $handle->connect(
                 driver => 'SQLite',
-                database => File::Spec->catfile(File::Spec->tmpdir(), "sb-test.$$")
-        );
+                database => $SQLITE_FILENAME);
 }
 
 sub connect_mysql
@@ -166,7 +171,7 @@
 {
         my $handle = shift;
         $handle->disconnect;
-        unlink File::Spec->catfile(File::Spec->tmpdir(), "sb-test.$$");
+        unlink $SQLITE_FILENAME;
 }
 
 sub disconnect_mysql


More information about the Jifty-commit mailing list