Merge 'trunk' into 'filter_column'
Peter Rabbitson [Wed, 5 May 2010 09:52:30 +0000 (09:52 +0000)]
r9155@Thesaurus (orig r9142):  rabbit | 2010-04-14 15:41:51 +0200
Add forgotten changes
r9156@Thesaurus (orig r9143):  caelum | 2010-04-14 17:04:00 +0200
support $ENV{DBI_DSN} and $ENV{DBI_DRIVER} (patch from Possum)
r9157@Thesaurus (orig r9144):  rabbit | 2010-04-14 17:50:58 +0200
Fix exception message
r9190@Thesaurus (orig r9177):  caelum | 2010-04-15 01:41:26 +0200
datetime millisecond precision for MSSQL
r9200@Thesaurus (orig r9187):  ribasushi | 2010-04-18 23:06:29 +0200
Fix leftover tabs
r9201@Thesaurus (orig r9188):  castaway | 2010-04-20 08:06:26 +0200
Warn if a class found in ResultSet/ is not a subclass of ::ResultSet

r9203@Thesaurus (orig r9190):  rbuels | 2010-04-20 21:12:22 +0200
create_ddl_dir mkpaths its dir if necessary.  also, added storage/deploy.t as place to put deployment tests
r9204@Thesaurus (orig r9191):  rbuels | 2010-04-20 21:20:06 +0200
do not croak, rbuels!  jeez.
r9205@Thesaurus (orig r9192):  castaway | 2010-04-21 08:03:08 +0200
Added missing test file (oops)

r9213@Thesaurus (orig r9200):  rabbit | 2010-04-24 02:23:05 +0200
10% speed up on quoted statement generation
r9215@Thesaurus (orig r9202):  rabbit | 2010-04-24 02:27:47 +0200
Revert bogus commit
r9216@Thesaurus (orig r9203):  ribasushi | 2010-04-24 02:31:06 +0200
_quote is now properly handled in SQLA
r9217@Thesaurus (orig r9204):  caelum | 2010-04-24 02:32:58 +0200
add "IMPROVING PERFORMANCE" section to Cookbook
r9231@Thesaurus (orig r9218):  ribasushi | 2010-04-26 13:13:13 +0200
Bump CAG and SQLA dependencies
r9232@Thesaurus (orig r9219):  ribasushi | 2010-04-26 15:27:38 +0200
Bizarre fork failure
r9233@Thesaurus (orig r9220):  castaway | 2010-04-26 21:45:32 +0200
Add tests using select/as to sqlahacks

r9234@Thesaurus (orig r9221):  castaway | 2010-04-26 21:49:10 +0200
Add test for fetching related obj/col as well

r9245@Thesaurus (orig r9232):  abraxxa | 2010-04-27 15:58:56 +0200
fixed missing ' in update_or_create with key attr example

r9247@Thesaurus (orig r9234):  ribasushi | 2010-04-27 16:53:06 +0200
Better concurrency in test (parent blocks)
r9248@Thesaurus (orig r9235):  ribasushi | 2010-04-27 16:53:34 +0200
Reformat tests/comments a bit
r9249@Thesaurus (orig r9236):  ribasushi | 2010-04-27 18:40:10 +0200
Better comment
r9250@Thesaurus (orig r9237):  ribasushi | 2010-04-27 18:40:31 +0200
Rename test
r9251@Thesaurus (orig r9238):  ribasushi | 2010-04-27 19:11:45 +0200
Fix global destruction problems
r9271@Thesaurus (orig r9258):  ribasushi | 2010-04-28 11:10:00 +0200
Refactor SQLA/select interaction (in reality just cleanup)
r9272@Thesaurus (orig r9259):  caelum | 2010-04-28 11:20:08 +0200
update ::DBI::Replicated
r9273@Thesaurus (orig r9260):  caelum | 2010-04-28 12:20:01 +0200
add _verify_pid and _verify_tid to methods that croak in ::Replicated
r9274@Thesaurus (orig r9261):  ribasushi | 2010-04-28 14:39:02 +0200
Fix failing test and some warnings
r9288@Thesaurus (orig r9275):  rabbit | 2010-04-29 10:32:10 +0200
Allow limit syntax change in-flight without digging into internals
r9292@Thesaurus (orig r9279):  castaway | 2010-04-30 12:26:52 +0200
Argh.. committing missing test file for load_namespaces tests

r9295@Thesaurus (orig r9282):  rabbit | 2010-05-01 11:06:21 +0200
The final version of the test
r9309@Thesaurus (orig r9296):  rabbit | 2010-05-04 09:44:51 +0200
Test for RT#56257
r9310@Thesaurus (orig r9297):  rabbit | 2010-05-04 10:00:11 +0200
Refactor count handling, make count-resultset attribute lists inclusive rather than exclusive (side effect - solves RT#56257
r9318@Thesaurus (orig r9305):  rabbit | 2010-05-05 11:49:51 +0200
 r9296@Thesaurus (orig r9283):  ribasushi | 2010-05-01 11:51:15 +0200
 Branch to clean up various limit dialects
 r9297@Thesaurus (orig r9284):  rabbit | 2010-05-01 11:55:04 +0200
 Preliminary version
 r9301@Thesaurus (orig r9288):  rabbit | 2010-05-03 18:31:24 +0200
 Fix incorrect comparison
 r9302@Thesaurus (orig r9289):  rabbit | 2010-05-03 18:32:36 +0200
 Do not add TOP prefixes to queries already containing it
 r9303@Thesaurus (orig r9290):  rabbit | 2010-05-03 18:33:15 +0200
 Add an as selector to a prefetch subquery to aid the subselecting-limit analyzer
 r9304@Thesaurus (orig r9291):  rabbit | 2010-05-03 18:34:49 +0200
 Rewrite mssql test to verify both types of limit dialects with and without quoting, rewrite the RNO, Top and RowNum dialects to rely on a factored out column re-aliaser
 r9305@Thesaurus (orig r9292):  rabbit | 2010-05-03 21:06:01 +0200
 Fix Top tests, make extra col selector order consistent
 r9307@Thesaurus (orig r9294):  ribasushi | 2010-05-04 00:50:35 +0200
 Fix test warning
 r9308@Thesaurus (orig r9295):  ribasushi | 2010-05-04 01:04:32 +0200
 Some databases (db2) do not like leading __s - use a different weird identifier for extra selector names
 r9313@Thesaurus (orig r9300):  rabbit | 2010-05-05 11:08:33 +0200
 Rename test
 r9314@Thesaurus (orig r9301):  rabbit | 2010-05-05 11:11:32 +0200
 If there was no offset, there is no sense in reordering
 r9315@Thesaurus (orig r9302):  rabbit | 2010-05-05 11:12:19 +0200
 Split and fix oracle tests
 r9317@Thesaurus (orig r9304):  rabbit | 2010-05-05 11:49:33 +0200
 Changes

Changes
lib/DBIx/Class/FilterColumn.pm [new file with mode: 0644]
lib/DBIx/Class/InflateColumn.pm
lib/DBIx/Class/Row.pm
t/row/filter_column.t [new file with mode: 0644]

diff --git a/Changes b/Changes
index c05b16f..1058ac5 100644 (file)
--- a/Changes
+++ b/Changes
@@ -5,6 +5,7 @@ Revision history for DBIx::Class
         - ::Storage::DBI now correctly preserves a parent $dbh from
           terminating children, even during interpreter-global
           out-of-order destruction
+        - Add DBIx::Class::FilterColumn for non-ref filtering
         - InflateColumn::DateTime support for MSSQL via DBD::Sybase
         - Millisecond precision support for MSSQL datetimes for
           InflateColumn::DateTime
diff --git a/lib/DBIx/Class/FilterColumn.pm b/lib/DBIx/Class/FilterColumn.pm
new file mode 100644 (file)
index 0000000..9ae5007
--- /dev/null
@@ -0,0 +1,164 @@
+package DBIx::Class::FilterColumn;
+use strict;
+use warnings;
+
+use base qw/DBIx::Class::Row/;
+
+sub filter_column {
+  my ($self, $col, $attrs) = @_;
+
+  $self->throw_exception("No such column $col to filter")
+    unless $self->has_column($col);
+
+  $self->throw_exception("filter_column needs attr hashref")
+    unless ref $attrs eq 'HASH';
+
+  $self->column_info($col)->{_filter_info} = $attrs;
+  my $acc = $self->column_info($col)->{accessor};
+  $self->mk_group_accessors(filtered_column => [ (defined $acc ? $acc : $col), $col]);
+  return 1;
+}
+
+sub _column_from_storage {
+  my ($self, $col, $value) = @_;
+
+  return $value unless defined $value;
+
+  my $info = $self->column_info($col)
+    or $self->throw_exception("No column info for $col");
+
+  return $value unless exists $info->{_filter_info};
+
+  my $filter = $info->{_filter_info}{filter_from_storage};
+  $self->throw_exception("No inflator for $col") unless defined $filter;
+
+  return $self->$filter($value);
+}
+
+sub _column_to_storage {
+  my ($self, $col, $value) = @_;
+
+  my $info = $self->column_info($col) or
+    $self->throw_exception("No column info for $col");
+
+  return $value unless exists $info->{_filter_info};
+
+  my $unfilter = $info->{_filter_info}{filter_to_storage};
+  $self->throw_exception("No unfilter for $col") unless defined $unfilter;
+  return $self->$unfilter($value);
+}
+
+sub get_filtered_column {
+  my ($self, $col) = @_;
+
+  $self->throw_exception("$col is not a filtered column")
+    unless exists $self->column_info($col)->{_filter_info};
+
+  return $self->{_filtered_column}{$col}
+    if exists $self->{_filtered_column}{$col};
+
+  my $val = $self->get_column($col);
+
+  return $self->{_filtered_column}{$col} = $self->_column_from_storage($col, $val);
+}
+
+sub set_filtered_column {
+  my ($self, $col, $filtered) = @_;
+
+  $self->set_column($col, $self->_column_to_storage($col, $filtered));
+
+  delete $self->{_filtered_column}{$col};
+
+  return $filtered;
+}
+
+sub update {
+  my ($self, $attrs, @rest) = @_;
+  foreach my $key (keys %{$attrs||{}}) {
+    if ($self->has_column($key) &&
+          exists $self->column_info($key)->{_filter_info}) {
+      my $val = delete $attrs->{$key};
+      $self->set_filtered_column($key, $val);
+      $attrs->{$key} = $self->_column_to_storage($key, $val)
+    }
+  }
+  return $self->next::method($attrs, @rest);
+}
+
+sub new {
+  my ($class, $attrs, @rest) = @_;
+  my $source = $attrs->{-result_source}
+    or $class->throw_exception('Sourceless rows are not supported with DBIx::Class::FilterColumn');
+
+  my $obj = $class->next::method($attrs, @rest);
+  foreach my $key (keys %{$attrs||{}}) {
+    if ($obj->has_column($key) &&
+          exists $obj->column_info($key)->{_filter_info} ) {
+      my $val = delete $attrs->{$key};
+      $obj->set_filtered_column($key, $val);
+    }
+  }
+  return $obj;
+}
+
+1;
+
+=head1 NAME
+
+DBIx::Class::FilterColumn - Automatically convert column data
+
+=head1 SYNOPSIS
+
+ # In your result classes
+ __PACKAGE__->filter_column( money => {
+     filter_to_storage => 'to_pennies',
+     filter_from_storage => 'from_pennies',
+ });
+
+ sub to_pennies   { $_[1] * 100 }
+
+ sub from_pennies { $_[1] / 100 }
+
+ 1;
+
+=head1 DESCRIPTION
+
+This component is meant to be a more powerful, but less DWIM-y,
+L<DBIx::Class::InflateColumn>.  One of the major issues with said component is
+that it B<only> works with references.  Generally speaking anything that can
+be done with L<DBIx::Class::InflateColumn> can be done with this component.
+
+=head1 METHODS
+
+=head2 filter_column
+
+ __PACKAGE__->filter_column( colname => {
+     filter_from_storage => 'method',
+     filter_to_storage   => 'method',
+ })
+
+This is the method that you need to call to set up a filtered column.  It takes
+exactly two arguments; the first being the column name the second being a
+C<HashRef> with C<filter_from_storage> and C<filter_to_storage> having
+something that can be called as a method.  The method will be called with
+the value of the column as the first non-C<$self> argument.
+
+=head2 get_filtered_column
+
+ $obj->get_filtered_column('colname')
+
+Returns the filtered value of the column
+
+=head2 set_filtered_column
+
+ $obj->set_filtered_column(colname => 'new_value')
+
+Sets the filtered value of the column
+
+=head2 update
+
+Just overridden to filter changed columns through this component
+
+=head2 new
+
+Just overridden to filter columns through this component
index f5c2f8f..5d9c19d 100644 (file)
@@ -37,7 +37,7 @@ deal with, to allow such settings as C< \'year + 1'> and C< \'DEFAULT' >
 to work.
 
 If you want to filter plain scalar values and replace them with
-something else, contribute a filtering component.
+something else, see L<DBIx::Class::FilterColumn>.
 
 =head1 METHODS
 
@@ -146,9 +146,9 @@ sub set_inflated_column {
   $self->set_column($col, $self->_deflated_column($col, $inflated));
 #  if (blessed $inflated) {
   if (ref $inflated && ref($inflated) ne 'SCALAR') {
-    $self->{_inflated_column}{$col} = $inflated; 
+    $self->{_inflated_column}{$col} = $inflated;
   } else {
-    delete $self->{_inflated_column}{$col};      
+    delete $self->{_inflated_column}{$col};
   }
   return $inflated;
 }
index 06f850b..4ce8153 100644 (file)
@@ -314,7 +314,7 @@ sub insert {
 
       MULTICREATE_DEBUG and warn "MC $self pre-reconstructing $relname $rel_obj\n";
 
-      my $them = { %{$rel_obj->{_relationship_data} || {} }, $rel_obj->get_inflated_columns };
+      my $them = { %{$rel_obj->{_relationship_data} || {} }, $rel_obj->get_columns };
       my $existing;
 
       # if there are no keys - nothing to search for
diff --git a/t/row/filter_column.t b/t/row/filter_column.t
new file mode 100644 (file)
index 0000000..6441e33
--- /dev/null
@@ -0,0 +1,69 @@
+use strict;
+use warnings;
+
+use Test::More;
+use lib qw(t/lib);
+use DBICTest;
+
+my $schema = DBICTest->init_schema();
+DBICTest::Schema::Artist->load_components('FilterColumn');
+DBICTest::Schema::Artist->filter_column(rank => {
+  filter_from_storage => sub { $_[1] * 2 },
+  filter_to_storage   => sub { $_[1] / 2 },
+});
+Class::C3->reinitialize();
+
+my $artist = $schema->resultset('Artist')->create( { rank => 20 } );
+
+# this should be using the cursor directly, no inflation/processing of any sort
+my ($raw_db_rank) = $schema->resultset('Artist')
+                             ->search ($artist->ident_condition)
+                               ->get_column('rank')
+                                ->_resultset
+                                 ->cursor
+                                  ->next;
+
+is ($raw_db_rank, 10, 'INSERT: correctly unfiltered on insertion');
+
+for my $reloaded (0, 1) {
+  my $test = $reloaded ? 'reloaded' : 'stored';
+  $artist->discard_changes if $reloaded;
+
+  is( $artist->rank , 20, "got $test filtered rank" );
+}
+
+$artist->update;
+$artist->discard_changes;
+is( $artist->rank , 20, "got filtered rank" );
+
+$artist->update ({ rank => 40 });
+($raw_db_rank) = $schema->resultset('Artist')
+                             ->search ($artist->ident_condition)
+                               ->get_column('rank')
+                                ->_resultset
+                                 ->cursor
+                                  ->next;
+is ($raw_db_rank, 20, 'UPDATE: correctly unflitered on update');
+
+$artist->discard_changes;
+$artist->rank(40);
+ok( !$artist->is_column_changed('rank'), 'column is not dirty after setting the same value' );
+
+MC: {
+   my $cd = $schema->resultset('CD')->create({
+      artist => { rank => 20 },
+      title => 'fun time city!',
+      year => 'forevertime',
+   });
+   ($raw_db_rank) = $schema->resultset('Artist')
+                                ->search ($cd->artist->ident_condition)
+                                  ->get_column('rank')
+                                   ->_resultset
+                                    ->cursor
+                                     ->next;
+
+   is $raw_db_rank, 10, 'artist rank gets correctly unfiltered w/ MC';
+   is $cd->artist->rank, 20, 'artist rank gets correctly filtered w/ MC';
+}
+
+done_testing;