Created
November 17, 2008 18:56
-
-
Save markpasc/25861 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Index: t/11-sql.t | |
=================================================================== | |
--- t/11-sql.t (.../tags/release-0.05) (revision 528) | |
+++ t/11-sql.t (.../trunk) (revision 528) | |
@@ -3,7 +3,7 @@ | |
use strict; | |
use Data::ObjectDriver::SQL; | |
-use Test::More tests => 58; | |
+use Test::More tests => 67; | |
my $stmt = ns(); | |
ok($stmt, 'Created SQL object'); | |
@@ -212,4 +212,48 @@ | |
LIMIT 2 | |
SQL | |
+# DISTINCT | |
+$stmt = ns(); | |
+$stmt->add_select(foo => 'foo'); | |
+$stmt->from([ qw(baz) ]); | |
+is($stmt->as_sql, "SELECT foo\nFROM baz\n", "DISTINCT is absent by default"); | |
+$stmt->distinct(1); | |
+is($stmt->as_sql, "SELECT DISTINCT foo\nFROM baz\n", "we can turn on DISTINCT"); | |
+ | |
+# index hint | |
+$stmt = ns(); | |
+$stmt->add_select(foo => 'foo'); | |
+$stmt->from([ qw(baz) ]); | |
+is($stmt->as_sql, "SELECT foo\nFROM baz\n", "index hint is absent by default"); | |
+$stmt->add_index_hint('baz' => { type => 'USE', list => ['index_hint']}); | |
+is($stmt->as_sql, "SELECT foo\nFROM baz USE INDEX (index_hint)\n", "we can turn on USE INDEX"); | |
+ | |
+# index hint with joins | |
+$stmt->joins([]); | |
+$stmt->from([]); | |
+$stmt->add_join(baz => { type => 'inner', table => 'baz', | |
+ condition => 'baz.baz_id = foo.baz_id' }); | |
+is($stmt->as_sql, "SELECT foo\nFROM baz USE INDEX (index_hint) INNER JOIN baz ON baz.baz_id = foo.baz_id\n", 'USE INDEX with JOIN'); | |
+$stmt->from([]); | |
+$stmt->joins([]); | |
+$stmt->add_join(baz => [ | |
+ { type => 'inner', table => 'baz b1', | |
+ condition => 'baz.baz_id = b1.baz_id AND b1.quux_id = 1' }, | |
+ { type => 'left', table => 'baz b2', | |
+ condition => 'baz.baz_id = b2.baz_id AND b2.quux_id = 2' }, | |
+ ]); | |
+is($stmt->as_sql, "SELECT foo\nFROM baz USE INDEX (index_hint) INNER JOIN baz b1 ON baz.baz_id = b1.baz_id AND b1.quux_id = 1 LEFT JOIN baz b2 ON baz.baz_id = b2.baz_id AND b2.quux_id = 2\n", 'USE INDEX with JOINs'); | |
+ | |
+$stmt = ns(); | |
+$stmt->add_select(foo => 'foo'); | |
+$stmt->from([ qw(baz) ]); | |
+$stmt->comment("mycomment"); | |
+is($stmt->as_sql, "SELECT foo\nFROM baz\n-- mycomment"); | |
+ | |
+$stmt->comment("\nbad\n\nmycomment"); | |
+is($stmt->as_sql, "SELECT foo\nFROM baz\n-- bad", "correctly untainted"); | |
+ | |
+$stmt->comment("G\\G"); | |
+is($stmt->as_sql, "SELECT foo\nFROM baz\n-- G", "correctly untainted"); | |
+ | |
sub ns { Data::ObjectDriver::SQL->new } | |
Index: t/09-resultset.t | |
=================================================================== | |
--- t/09-resultset.t (.../tags/release-0.05) (revision 0) | |
+++ t/09-resultset.t (.../trunk) (revision 528) | |
@@ -0,0 +1,149 @@ | |
+# $Id: 01-col-inheritance.t 989 2005-09-23 19:58:01Z btrott $ | |
+ | |
+use strict; | |
+ | |
+use lib 't/lib'; | |
+ | |
+require 't/lib/db-common.pl'; | |
+ | |
+$Data::ObjectDriver::DEBUG = 0; | |
+use Test::More; | |
+unless (eval { require DBD::SQLite }) { | |
+ plan skip_all => 'Tests require DBD::SQLite'; | |
+} | |
+plan tests => 47; | |
+ | |
+setup_dbs({ | |
+ global => [ qw( wines ) ], | |
+}); | |
+ | |
+use Wine; | |
+use Storable; | |
+ | |
+my $wine = Wine->new; | |
+$wine->name("Saumur Champigny, Le Grand Clos 2001"); | |
+$wine->rating(4); | |
+ | |
+## generate some binary data (SQL_BLOB / MEDIUMBLOB) | |
+my $glouglou = { tanin => "beaucoup", caudalies => "4" }; | |
+$wine->binchar("xxx\0yyy"); | |
+$wine->content(Storable::nfreeze($glouglou)); | |
+ok($wine->save, 'Object saved successfully'); | |
+ | |
+my $iter; | |
+ | |
+$iter = Data::ObjectDriver::Iterator->new(sub {}); | |
+my $wine_id = $wine->id; | |
+undef $wine; | |
+$wine = Wine->lookup($wine_id); | |
+ | |
+ok $wine; | |
+is_deeply Storable::thaw($wine->content), $glouglou; | |
+SKIP: { | |
+ skip "Please upgrade to DBD::SQLite 1.11", 1 | |
+ if $DBD::SQLite::VERSION < 1.11; | |
+ is $wine->binchar, "xxx\0yyy"; | |
+}; | |
+ | |
+Wine->bulk_insert(['name', 'rating'], [['Caymus', 4], ['Thunderbird', 1], ['Stags Leap', 3]]); | |
+ | |
+ | |
+{ | |
+ my $result = Wine->result({}); | |
+ | |
+ my $objs = $result->slice(0, 100); | |
+ is @$objs, 4; | |
+ | |
+ my $rs = $result->slice(0, 2); | |
+ is @$rs, 3; | |
+ for my $r (@$rs) { | |
+ isa_ok $r, 'Wine'; | |
+ } | |
+} | |
+ | |
+$wine = undef; | |
+my ($result) = Wine->result({name => 'Caymus'}); | |
+ok! $result->is_finished; | |
+$wine = $result->next; | |
+ok $wine, 'Found Caymus'; | |
+is $wine->name, 'Caymus'; | |
+ok ! $result->next; #sets is_finished() | |
+ok $result->is_finished; | |
+ | |
+# testing iterator | |
+my ($iterator) = $result->iterator([$wine]); | |
+ok(! $iterator->is_finished ); | |
+$wine = $iterator->next; | |
+ok $wine, 'Found Caymus'; | |
+is $wine->name, 'Caymus'; | |
+ok( ! $iterator->next ); | |
+ok( $iterator->is_finished ); | |
+ | |
+# testing bug in iterator, adding a limit where there was one before shouldn't invalidate results | |
+($iterator) = $result->iterator([$wine]); | |
+$iterator->add_limit(1); | |
+ok(! $iterator->is_finished ); | |
+$wine = $iterator->next; | |
+ok $wine, 'Found Caymus'; | |
+is $wine->name, 'Caymus'; | |
+ok ! $iterator->next; | |
+ok $iterator->is_finished; | |
+ | |
+ | |
+($result) = Wine->result({}, { sort => 'name', direction => 'ascend' }); | |
+($iterator) = $result->iterator( [ $result->next, $result->next ] ); | |
+$iterator->add_limit(1); | |
+ok! $iterator->is_finished ; | |
+$wine = $iterator->next; | |
+ok $wine, 'Found Caymus'; | |
+is $wine->name, 'Caymus'; | |
+ok ! $iterator->next; | |
+ok $iterator->is_finished; | |
+ | |
+ | |
+# raising the limit should trigger a new search | |
+($result) = Wine->result({}, { sort => 'name', direction => 'ascend' }); | |
+($iterator) = $result->iterator( [ $result->next, $result->next ] ); | |
+$iterator->add_limit(9999); | |
+ok! $iterator->is_finished; | |
+$wine = $iterator->next; | |
+ok $wine, 'Found Caymus'; | |
+is $wine->name, 'Caymus'; | |
+ok $iterator->next, 'more to go'; | |
+ok ! $iterator->is_finished, "we're not finished"; | |
+ | |
+ | |
+# testing limit in args | |
+($result) = Wine->result({}, { limit => 2, sort => 'name', direction => 'ascend' }); | |
+ok! $result->is_finished ; | |
+$wine = $result->next; | |
+is $wine->name, 'Caymus'; | |
+$wine = $result->next; | |
+is $wine->name, 'Saumur Champigny, Le Grand Clos 2001'; | |
+ok ! $result->next; | |
+ok $result->is_finished; | |
+ | |
+# raising the limit should trigger a new search | |
+($result) = Wine->result({}, { limit => 2, sort => 'name', direction => 'ascend' }); | |
+$result->add_limit(3); | |
+is $result->next->name, 'Caymus'; | |
+is $result->next->name, 'Saumur Champigny, Le Grand Clos 2001'; | |
+is $result->next->name, 'Stags Leap'; | |
+ | |
+# test slice again with _results_loaded | |
+$result->rewind; | |
+{ | |
+ my $rs = $result->slice(0, 2); | |
+ for my $r (@$rs) { | |
+ isa_ok $r, 'Wine'; | |
+ } | |
+ | |
+ my $objs; | |
+ $objs = $result->slice(0, 100); | |
+ is @$objs, 3; | |
+ | |
+ $objs = $result->slice(5, 10); | |
+ is @$objs, 0; | |
+} | |
+ | |
+teardown_dbs(qw( global )); | |
Index: t/34-both.t | |
=================================================================== | |
--- t/34-both.t (.../tags/release-0.05) (revision 528) | |
+++ t/34-both.t (.../trunk) (revision 528) | |
@@ -18,7 +18,7 @@ | |
} | |
} | |
-plan tests => 46; | |
+plan tests => 86; | |
use Recipe; | |
use Ingredient; | |
@@ -32,6 +32,7 @@ | |
## Install some deflate/inflate in the Cache driver. | |
{ | |
no warnings 'once'; | |
+ no warnings 'redefine'; | |
*Data::ObjectDriver::Driver::Cache::Cache::deflate = sub { | |
$_[1]->deflate; | |
}; | |
@@ -114,15 +115,24 @@ | |
ok !$i4->{__cached}; | |
is $i4->name, 'Flour'; | |
+## verify it's in the cache | |
+my $key = $i4->driver->cache_key(ref($i4), $i4->primary_key); | |
+my $data = $i4->driver->get_from_cache($key); | |
+ok $data; | |
+is $data->{columns}{id}, $i3->id, "it's in the cache"; | |
## Delete it from the cache, so that the next test is actually accurate. | |
-my $driver = Ingredient->driver; | |
-$driver->remove_from_cache($driver->cache_key('Ingredient', $i4->primary_key)); | |
+$i4->uncache_object; | |
+ok ! $i4->driver->get_from_cache($key), "It's been purged from the cache"; | |
## Now look up the ingredients again. Milk and Eggs should already be cached, | |
## and doing the search should now cache Flour. | |
@is = Ingredient->search({ recipe_id => $recipe->recipe_id }); | |
is scalar(@is), 3; | |
+## this is still working if we add a comment | |
+@is = Ingredient->search({ recipe_id => $recipe->recipe_id }, { comment => "mytest" }); | |
+is scalar(@is), 3; | |
+ | |
## Flour should now be cached. | |
$i4 = Ingredient->lookup([ $recipe->recipe_id, $i3->id ]); | |
ok $i4->{__cached}; | |
@@ -176,4 +186,6 @@ | |
ok $replaced->{__cached}; | |
is $replaced->title, 'Cup Cake'; | |
+require 't/txn-common.pl'; | |
+ | |
teardown_dbs(qw( global cluster1 cluster2 )); | |
Index: t/04-clone.t | |
=================================================================== | |
--- t/04-clone.t (.../tags/release-0.05) (revision 528) | |
+++ t/04-clone.t (.../trunk) (revision 528) | |
@@ -18,7 +18,7 @@ | |
} | |
} | |
-plan tests => 26; | |
+plan tests => 29; | |
use Wine; | |
use Recipe; | |
@@ -68,6 +68,10 @@ | |
ok !defined $clone->id, 'Basic clone has no id'; | |
ok $clone->save, 'Basic clone could be saved'; | |
+ is $clone->name, 'Cul de Veau à la Sauge'; | |
+ is $clone->is_changed('name'), '', "This is documentation ;-)"; | |
+ $clone->refresh; | |
+ is $clone->name, 'Cul de Veau à la Sauge'; | |
ok defined $clone->id, 'Basic clone has an id after saving'; | |
isnt $w->id, $clone->id, q(Basic clone's id differs from original's id); | |
} | |
Index: t/35-multiplexed.t | |
=================================================================== | |
--- t/35-multiplexed.t (.../tags/release-0.05) (revision 528) | |
+++ t/35-multiplexed.t (.../trunk) (revision 528) | |
@@ -11,7 +11,7 @@ | |
unless (eval { require DBD::SQLite }) { | |
plan skip_all => 'Tests require DBD::SQLite'; | |
} | |
-plan tests => 26; | |
+plan tests => 42; | |
setup_dbs({ | |
global1 => [ qw( ingredient2recipe ) ], | |
@@ -60,6 +60,54 @@ | |
is $ok, 1, "Record is removed from $driver backend database"; | |
} | |
+## check transactions | |
+$obj = Ingredient2Recipe->new; | |
+$obj->ingredient_id(10); | |
+$obj->recipe_id(50); | |
+$obj->insert; | |
+ | |
+Data::ObjectDriver::BaseObject->begin_work(); | |
+$obj->value1("will be rolled back"); | |
+$obj->update; | |
+Data::ObjectDriver::BaseObject->rollback(); | |
+$obj->refresh; | |
+is $obj->value1, undef, "properly rolled back"; | |
+_check_object($obj); | |
+ | |
+Data::ObjectDriver::BaseObject->begin_work(); | |
+$obj->value1("commit"); | |
+$obj->update; | |
+Data::ObjectDriver::BaseObject->commit(); | |
+$obj->refresh; | |
+is $obj->value1, "commit", "yay"; | |
+_check_object($obj); | |
+ | |
+## if something goes wrong writing the second partition we roll back | |
+## the first one | |
+## set up a trap: | |
+my $second_driver = Ingredient2Recipe->driver->drivers->[-1]; | |
+my $dbh = $second_driver->dbh; | |
+my $sth = $dbh->prepare("insert into ingredient2recipe (ingredient_id, recipe_id, value1) values (199, 199, 'tada')"); | |
+$sth->execute; | |
+$sth->finish; | |
+ | |
+Data::ObjectDriver::BaseObject->begin_work(); | |
+$obj = Ingredient2Recipe->new; | |
+$obj->ingredient_id(199); | |
+$obj->recipe_id(199); | |
+$obj->value1("test"); | |
+eval { $obj->insert;}; | |
+ok $@, "rollback"; | |
+if ($@) { | |
+ Data::ObjectDriver::BaseObject->rollback(); | |
+} | |
+else { | |
+ Data::ObjectDriver::BaseObject->commit(); | |
+} | |
+# since on_lookup use the first driver this should be undef | |
+my $void = Ingredient2Recipe->lookup(199); | |
+is $void, undef, "rolled back"; | |
+ | |
## Object remove() | |
$obj = Ingredient2Recipe->new; | |
$obj->ingredient_id(4); | |
Index: t/schemas/ingredient2recipe.sql | |
=================================================================== | |
--- t/schemas/ingredient2recipe.sql (.../tags/release-0.05) (revision 528) | |
+++ t/schemas/ingredient2recipe.sql (.../trunk) (revision 528) | |
@@ -1,5 +1,6 @@ | |
CREATE TABLE ingredient2recipe ( | |
ingredient_id INTEGER NOT NULL, | |
recipe_id INTEGER NOT NULL, | |
+ value1 VARCHAR(255), | |
PRIMARY KEY (recipe_id, ingredient_id) | |
) | |
Index: t/05-deflate.t | |
=================================================================== | |
--- t/05-deflate.t (.../tags/release-0.05) (revision 528) | |
+++ t/05-deflate.t (.../trunk) (revision 528) | |
@@ -42,6 +42,7 @@ | |
## Install some deflate/inflate in the Cache driver. | |
{ | |
no warnings 'once'; | |
+ no warnings 'redefine'; | |
*Data::ObjectDriver::Driver::Cache::Cache::deflate = sub { | |
$_[1]->deflate; | |
}; | |
Index: t/31-cached.t | |
=================================================================== | |
--- t/31-cached.t (.../tags/release-0.05) (revision 528) | |
+++ t/31-cached.t (.../trunk) (revision 528) | |
@@ -16,7 +16,7 @@ | |
plan skip_all => 'Tests require Cache::Memory'; | |
} | |
} | |
-plan tests => 62; | |
+plan tests => 100; | |
setup_dbs({ | |
global => [ qw( recipes ingredients ) ], | |
@@ -151,6 +151,23 @@ | |
is($ingredient->remove, 1, 'Ingredient removed successfully'); | |
is($ingredient2->remove, 1, 'Ingredient removed successfully'); | |
+ | |
+## demonstration that we have a problem with caching and transaction | |
+{ | |
+ # ingredient3 should already be hot in the cache anyway | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ $ingredient3->quantity(300); # originally was 100 | |
+ $ingredient3->save; | |
+ | |
+ my $same = Ingredient->lookup($ingredient3->primary_key); | |
+ is $same->quantity, 300; | |
+ | |
+ Data::ObjectDriver::BaseObject->rollback; | |
+ | |
+ $same = Ingredient->lookup($ingredient3->primary_key); | |
+ is $same->quantity, 100; | |
+} | |
+ | |
# let's remove ingredient3 with Class methods | |
eval { | |
Ingredient->remove({ name => 'Chocolate Chips' }, { nofetch => 1 }); | |
@@ -160,8 +177,9 @@ | |
is(Ingredient->remove({ name => 'Chocolate Chips' }), 1, "Removed with class method"); | |
ok(! Ingredient->lookup(1), "really deleted"); | |
- | |
is($recipe->remove, 1, 'Recipe removed successfully'); | |
is($recipe2->remove, 1, 'Recipe removed successfully'); | |
+require 't/txn-common.pl'; | |
+ | |
teardown_dbs(qw( global )); | |
Index: t/10-resultset-peek.t | |
=================================================================== | |
--- t/10-resultset-peek.t (.../tags/release-0.05) (revision 0) | |
+++ t/10-resultset-peek.t (.../trunk) (revision 528) | |
@@ -0,0 +1,150 @@ | |
+# $Id: 01-col-inheritance.t 989 2005-09-23 19:58:01Z btrott $ | |
+ | |
+# this is about the same test as t/09-resultset.t, but with lots of peek_next'ing | |
+# going on, to test that new method | |
+ | |
+use strict; | |
+ | |
+use lib 't/lib'; | |
+ | |
+require 't/lib/db-common.pl'; | |
+ | |
+$Data::ObjectDriver::DEBUG = 0; | |
+use Test::More; | |
+unless (eval { require DBD::SQLite }) { | |
+ plan skip_all => 'Tests require DBD::SQLite'; | |
+} | |
+plan tests => 65; | |
+ | |
+setup_dbs({ | |
+ global => [ qw( wines ) ], | |
+}); | |
+ | |
+use Wine; | |
+use Storable; | |
+ | |
+my $wine = Wine->new; | |
+$wine->name("Saumur Champigny, Le Grand Clos 2001"); | |
+$wine->rating(4); | |
+ | |
+## generate some binary data (SQL_BLOB / MEDIUMBLOB) | |
+my $glouglou = { tanin => "beaucoup", caudalies => "4" }; | |
+$wine->binchar("xxx\0yyy"); | |
+$wine->content(Storable::nfreeze($glouglou)); | |
+ok($wine->save, 'Object saved successfully'); | |
+ | |
+my $iter; | |
+ | |
+$iter = Data::ObjectDriver::Iterator->new(sub {}); | |
+my $wine_id = $wine->id; | |
+undef $wine; | |
+$wine = Wine->lookup($wine_id); | |
+ | |
+ok $wine; | |
+is_deeply Storable::thaw($wine->content), $glouglou; | |
+SKIP: { | |
+ skip "Please upgrade to DBD::SQLite 1.11", 1 | |
+ if $DBD::SQLite::VERSION < 1.11; | |
+ is $wine->binchar, "xxx\0yyy"; | |
+}; | |
+ | |
+Wine->bulk_insert(['name', 'rating'], [['Caymus', 4], ['Thunderbird', 1], ['Stags Leap', 3]]); | |
+ | |
+$wine = undef; | |
+my ($result) = Wine->result({name => 'Caymus'}); | |
+is $result->peek_next->name, 'Caymus', 'before we start, peek_next says the first one is Caymus'; | |
+ok! $result->is_finished; | |
+$wine = $result->next; | |
+ok $wine, 'Found Caymus'; | |
+is $wine->name, 'Caymus'; | |
+ok ! $result->peek_next, "we're at the end of the set"; | |
+ok ! $result->next; #sets is_finished() | |
+ok ! $result->peek_next, "we're *still* at the end of the set"; | |
+ok $result->is_finished; | |
+ | |
+# testing iterator | |
+my ($iterator) = $result->iterator([$wine]); | |
+is $iterator->peek_next->name, 'Caymus', 'before we start, peek_next says the first one is Caymus'; | |
+ok(! $iterator->is_finished ); | |
+$wine = $iterator->next; | |
+ok $wine, 'Found Caymus'; | |
+is $wine->name, 'Caymus'; | |
+ok ! $iterator->peek_next, "we're at the end of the set"; | |
+ok( ! $iterator->next ); | |
+ok ! $iterator->peek_next, "we're *still* at the end of the set"; | |
+ok( $iterator->is_finished ); | |
+ | |
+# testing bug in iterator, adding a limit where there was one before shouldn't invalidate results | |
+($iterator) = $result->iterator([$wine]); | |
+is $iterator->peek_next->name, 'Caymus', 'before we start, peek_next says the first one is Caymus'; | |
+$iterator->add_limit(1); | |
+is $iterator->peek_next->name, 'Caymus', 'after adding limit, peek_next says the first one is Caymus'; | |
+ok(! $iterator->is_finished ); | |
+$wine = $iterator->next; | |
+ok $wine, 'Found Caymus'; | |
+is $wine->name, 'Caymus'; | |
+ok ! $iterator->peek_next, "we're at the end of the set"; | |
+ok ! $iterator->next; | |
+ok ! $iterator->peek_next, "we're *still* at the end of the set"; | |
+ok $iterator->is_finished; | |
+ | |
+ | |
+($result) = Wine->result({}, { sort => 'name', direction => 'ascend' }); | |
+($iterator) = $result->iterator( [ $result->next, $result->next ] ); | |
+is $iterator->peek_next->name, 'Caymus', 'before we start, peek_next says the first one is Caymus'; | |
+$iterator->add_limit(1); | |
+is $iterator->peek_next->name, 'Caymus', 'after adding limit, peek_next says the first one is Caymus'; | |
+ok! $iterator->is_finished ; | |
+$wine = $iterator->next; | |
+ok $wine, 'Found Caymus'; | |
+is $wine->name, 'Caymus'; | |
+ok ! $iterator->peek_next, "we're at the end of the set"; | |
+ok ! $iterator->next; | |
+ok ! $iterator->peek_next, "we're *still* at the end of the set"; | |
+ok $iterator->is_finished; | |
+ | |
+ | |
+# raising the limit should trigger a new search | |
+($result) = Wine->result({}, { sort => 'name', direction => 'ascend' }); | |
+($iterator) = $result->iterator( [ $result->next, $result->next ] ); | |
+is $iterator->peek_next->name, 'Caymus', 'before we start, peek_next says the first one is Caymus'; | |
+$iterator->add_limit(9999); | |
+is $iterator->peek_next->name, 'Caymus', 'after adding limit, peek_next says the first one is Caymus'; | |
+ok! $iterator->is_finished; | |
+$wine = $iterator->next; | |
+ok $wine, 'Found Caymus'; | |
+is $wine->name, 'Caymus'; | |
+ok $iterator->peek_next, "more to go"; | |
+ok $iterator->next, 'more to go'; | |
+ok ! $iterator->peek_next, "that was the last one, there are no more"; | |
+ok ! $iterator->is_finished, "we're not finished"; | |
+ok ! $iterator->next; #sets is_finished() | |
+ok ! $iterator->peek_next, "that was the last one, there are no more"; | |
+ok $iterator->is_finished, "now we are finished"; | |
+ | |
+ | |
+# testing limit in args | |
+($result) = Wine->result({}, { limit => 2, sort => 'name', direction => 'ascend' }); | |
+is $result->peek_next->name, 'Caymus', 'before we start, peek_next says the first one is Caymus'; | |
+ok! $result->is_finished ; | |
+$wine = $result->next; | |
+is $wine->name, 'Caymus'; | |
+is $result->peek_next->name, 'Saumur Champigny, Le Grand Clos 2001', 'the next one will be Saumur'; | |
+$wine = $result->next; | |
+is $wine->name, 'Saumur Champigny, Le Grand Clos 2001'; | |
+ok ! $result->peek_next, "Saumur was the last one"; | |
+ok ! $result->next; | |
+ok $result->is_finished; | |
+ok ! $result->peek_next, "Saumur was really the last one"; | |
+ | |
+# raising the limit should trigger a new search | |
+($result) = Wine->result({}, { limit => 2, sort => 'name', direction => 'ascend' }); | |
+$result->add_limit(3); | |
+is $result->next->name, 'Caymus'; | |
+is $result->peek_next->name, 'Saumur Champigny, Le Grand Clos 2001', 'the next one will be Saumur'; | |
+is $result->next->name, 'Saumur Champigny, Le Grand Clos 2001'; | |
+is $result->peek_next->name, 'Stags Leap', 'the next one will be Stags Leap'; | |
+is $result->next->name, 'Stags Leap'; | |
+ok ! $result->peek_next, "Stags Leap was the last one"; | |
+ | |
+teardown_dbs(qw( global )); | |
Index: t/98-perl_critic.t | |
=================================================================== | |
--- t/98-perl_critic.t (.../tags/release-0.05) (revision 528) | |
+++ t/98-perl_critic.t (.../trunk) (revision 528) | |
@@ -1,6 +1,9 @@ | |
use Test::More; | |
-eval 'use Test::Perl::Critic'; | |
+eval { | |
+ require Test::Perl::Critic; | |
+ Test::Perl::Critic->import( -exclude => ['ProhibitNoStrict'] ); | |
+}; | |
plan skip_all => 'Test::Perl::Critic required to criticise code' if $@; | |
all_critic_ok(); | |
Index: t/02-basic.t | |
=================================================================== | |
--- t/02-basic.t (.../tags/release-0.05) (revision 528) | |
+++ t/02-basic.t (.../trunk) (revision 528) | |
@@ -19,7 +19,7 @@ | |
} | |
} | |
-plan tests => 58; | |
+plan tests => 67; | |
use Wine; | |
use Recipe; | |
@@ -196,6 +196,7 @@ | |
# emulate a driver which doesn't support REPLACE INTO | |
{ | |
+ no warnings 'redefine'; | |
local *Data::ObjectDriver::Driver::DBD::SQLite::can_replace = sub { 0 }; | |
$r->title('replaced'); | |
$r->recipe_id("lamer"); | |
@@ -244,5 +245,35 @@ | |
is (Wine->remove({}, { nofetch => 1 }), '0E0', 'removing all bad wine'); | |
} | |
-#teardown_dbs(qw( global )); | |
+# different utilities | |
+{ | |
+ my $w1 = Wine->new; | |
+ $w1->name("Chateau la pompe"); | |
+ $w1->insert; | |
+ my $w3 = Wine->new; | |
+ $w3->name("different"); | |
+ $w3->insert; | |
+ | |
+ my $w2 = Wine->lookup($w1->id); | |
+ ok $w1->is_same($w1); | |
+ ok $w2->is_same($w1); | |
+ ok $w1->is_same($w2); | |
+ ok !$w1->is_same($w3); | |
+ ok !$w3->is_same($w2); | |
+ | |
+ like $w1->pk_str, qr/\d+/; | |
+} | |
+ | |
+# Test the new flag for persistent store insertion | |
+{ | |
+ my $w = Wine->new(name => 'flag test', rating=> 4); | |
+ ok !$w->object_is_stored, "this object needs to be saved!"; | |
+ $w->save; | |
+ ok $w->object_is_stored, "this object is no saved"; | |
+ my $w2 = Wine->lookup( $w->id ); | |
+ ok $w2->object_is_stored, "an object fetched from the database is by definition NOT ephemeral"; | |
+} | |
+ | |
+teardown_dbs(qw( global )); | |
+ | |
Index: t/32-partitioned.t | |
=================================================================== | |
--- t/32-partitioned.t (.../tags/release-0.05) (revision 528) | |
+++ t/32-partitioned.t (.../trunk) (revision 528) | |
@@ -10,7 +10,7 @@ | |
unless (eval { require DBD::SQLite }) { | |
plan skip_all => 'Tests require DBD::SQLite'; | |
} | |
-plan tests => 52; | |
+plan tests => 88; | |
setup_dbs({ | |
global => [ qw( recipes ) ], | |
@@ -48,6 +48,7 @@ | |
ok(!$iter->(), 'Iterator gave us only one recipe'); | |
is(ref $tmp, 'Recipe', 'Iterator gave us a recipe'); | |
is($tmp->title, 'My Banana Milkshake', 'Title is My Banana Milkshake'); | |
+$iter->end(); | |
my $ingredient = Ingredient->new; | |
$ingredient->recipe_id($recipe->recipe_id); | |
@@ -72,6 +73,7 @@ | |
ok(!$iter->(), 'Iterator gave us only one ingredient'); | |
is(ref $tmp, 'Ingredient', 'Iterator gave us an ingredient'); | |
is($tmp->name, 'Vanilla Ice Cream', 'Name is Vanilla Ice Cream'); | |
+$iter->end(); | |
my $ingredient2 = Ingredient->new; | |
$ingredient2->recipe_id($recipe->recipe_id); | |
@@ -121,4 +123,6 @@ | |
is $recipe->remove, 1, 'Recipe removed successfully'; | |
is $recipe2->remove, 1, 'Recipe removed successfully'; | |
+require 't/txn-common.pl'; | |
+ | |
teardown_dbs(qw( global cluster1 cluster2 )); | |
Index: t/lib/partitioned/Recipe.pm | |
=================================================================== | |
--- t/lib/partitioned/Recipe.pm (.../tags/release-0.05) (revision 528) | |
+++ t/lib/partitioned/Recipe.pm (.../trunk) (revision 528) | |
@@ -12,16 +12,22 @@ | |
primary_key => 'recipe_id', | |
driver => Data::ObjectDriver::Driver::DBI->new( | |
dsn => 'dbi:SQLite:dbname=global.db', | |
+ reuse_dbh => 1, | |
), | |
}); | |
+my %drivers; | |
__PACKAGE__->has_partitions( | |
number => 2, | |
get_driver => sub { | |
- return Data::ObjectDriver::Driver::DBI->new( | |
- dsn => 'dbi:SQLite:dbname=cluster' . shift() . '.db', | |
- @_, | |
- ), | |
+ my $cluster = shift; | |
+ my $driver = $drivers{$cluster} ||= | |
+ Data::ObjectDriver::Driver::DBI->new( | |
+ dsn => 'dbi:SQLite:dbname=cluster' . $cluster . '.db', | |
+ reuse_dbh => 1, | |
+ @_, | |
+ ); | |
+ return $driver; | |
}, | |
); | |
Index: t/lib/cached/Recipe.pm | |
=================================================================== | |
--- t/lib/cached/Recipe.pm (.../tags/release-0.05) (revision 528) | |
+++ t/lib/cached/Recipe.pm (.../trunk) (revision 528) | |
@@ -12,6 +12,7 @@ | |
primary_key => 'recipe_id', | |
driver => Data::ObjectDriver::Driver::DBI->new( | |
dsn => 'dbi:SQLite:dbname=global.db', | |
+ reuse_dbh => 1, | |
), | |
}); | |
Index: t/lib/cached/Ingredient.pm | |
=================================================================== | |
--- t/lib/cached/Ingredient.pm (.../tags/release-0.05) (revision 528) | |
+++ t/lib/cached/Ingredient.pm (.../trunk) (revision 528) | |
@@ -6,8 +6,7 @@ | |
use Carp (); | |
use Data::ObjectDriver::Driver::DBI; | |
-use Data::ObjectDriver::Driver::Cache::Cache; | |
-use Cache::Memory; | |
+use Data::ObjectDriver::Driver::Cache::RAM; | |
our %IDs; | |
@@ -15,11 +14,11 @@ | |
columns => [ 'id', 'recipe_id', 'name', 'quantity' ], | |
datasource => 'ingredients', | |
primary_key => [ 'recipe_id', 'id' ], | |
- driver => Data::ObjectDriver::Driver::Cache::Cache->new( | |
- cache => Cache::Memory->new, | |
+ driver => Data::ObjectDriver::Driver::Cache::RAM->new( | |
fallback => Data::ObjectDriver::Driver::DBI->new( | |
dsn => 'dbi:SQLite:dbname=global.db', | |
pk_generator => \&generate_pk, | |
+ reuse_dbh => 1, | |
), | |
pk_generator => \&generate_pk, | |
), | |
Index: t/lib/both/Recipe.pm | |
=================================================================== | |
--- t/lib/both/Recipe.pm (.../tags/release-0.05) (revision 528) | |
+++ t/lib/both/Recipe.pm (.../trunk) (revision 528) | |
@@ -16,17 +16,23 @@ | |
cache => Cache::Memory->new, | |
fallback => Data::ObjectDriver::Driver::DBI->new( | |
dsn => 'dbi:SQLite:dbname=global.db', | |
+ reuse_dbh => 1, | |
), | |
), | |
}); | |
+my %drivers; | |
__PACKAGE__->has_partitions( | |
number => 2, | |
get_driver => sub { | |
- return Data::ObjectDriver::Driver::DBI->new( | |
- dsn => 'dbi:SQLite:dbname=cluster' . shift() . '.db', | |
- @_, | |
- ), | |
+ my $cluster = shift; | |
+ my $driver = $drivers{$cluster} ||= | |
+ Data::ObjectDriver::Driver::DBI->new( | |
+ dsn => 'dbi:SQLite:dbname=cluster' . $cluster . '.db', | |
+ reuse_dbh => 1, | |
+ @_, | |
+ ); | |
+ return $driver; | |
}, | |
); | |
Index: t/lib/multiplexed/Ingredient2Recipe.pm | |
=================================================================== | |
--- t/lib/multiplexed/Ingredient2Recipe.pm (.../tags/release-0.05) (revision 528) | |
+++ t/lib/multiplexed/Ingredient2Recipe.pm (.../trunk) (revision 528) | |
@@ -16,7 +16,7 @@ | |
); | |
__PACKAGE__->install_properties({ | |
- columns => [ 'recipe_id', 'ingredient_id' ], | |
+ columns => [ 'recipe_id', 'ingredient_id', "value1" ], | |
datasource => 'ingredient2recipe', | |
primary_key => 'recipe_id', ## should match lookup XXX could we auto generate it ? | |
driver => Data::ObjectDriver::Driver::Multiplexer->new( | |
Index: t/txn-common.pl | |
=================================================================== | |
--- t/txn-common.pl (.../tags/release-0.05) (revision 0) | |
+++ t/txn-common.pl (.../trunk) (revision 528) | |
@@ -0,0 +1,139 @@ | |
+# $Id: db-common.pl 58 2006-05-04 00:04:10Z sky $ | |
+ | |
+use strict; | |
+use Test::More; | |
+ | |
+diag "executing common tests"; | |
+use Data::ObjectDriver::BaseObject; | |
+ | |
+## testing basic rollback | |
+{ | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ | |
+ my $recipe = Recipe->new; | |
+ $recipe->title('gratin dauphinois'); | |
+ ok($recipe->save, 'Object saved successfully'); | |
+ ok(my $recipe_id = $recipe->recipe_id, 'Recipe has an ID'); | |
+ is($recipe->title, 'gratin dauphinois', 'Title is set'); | |
+ | |
+ my $ingredient = Ingredient->new; | |
+ $ingredient->recipe_id($recipe->recipe_id); | |
+ $ingredient->name('cheese'); | |
+ $ingredient->quantity(10); | |
+ ok($ingredient->save, 'Ingredient saved successfully'); | |
+ ok(my $ingredient_pk = $ingredient->primary_key, 'Ingredient has an ID'); | |
+ ok($ingredient->id, 'ID is defined'); | |
+ is($ingredient->name, 'cheese', 'got a name for the ingredient'); | |
+ | |
+ #use YAML; warn Dump (Data::ObjectDriver::BaseObject->txn_debug); | |
+ Data::ObjectDriver::BaseObject->rollback; | |
+ | |
+ ## check that we don't have a trace of all the good stuff we cooked | |
+ is(Recipe->lookup($recipe_id), undef, "no trace of object"); | |
+ is(eval { Ingredient->lookup($ingredient_pk) }, undef, "no trace of object"); | |
+ is(Recipe->lookup_multi([ $recipe_id ])->[0], undef); | |
+} | |
+ | |
+## testing basic commit | |
+{ | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ | |
+ my $recipe = Recipe->new; | |
+ $recipe->title('gratin dauphinois'); | |
+ ok($recipe->save, 'Object saved successfully'); | |
+ ok(my $recipe_id = $recipe->recipe_id, 'Recipe has an ID'); | |
+ is($recipe->title, 'gratin dauphinois', 'Title is set'); | |
+ | |
+ my $ingredient = Ingredient->new; | |
+ $ingredient->recipe_id($recipe->recipe_id); | |
+ $ingredient->name('cheese'); | |
+ $ingredient->quantity(10); | |
+ ok($ingredient->save, 'Ingredient saved successfully'); | |
+ ok(my $ingredient_pk = $ingredient->primary_key, 'Ingredient has an ID'); | |
+ ok($ingredient->id, 'ID is defined'); | |
+ is($ingredient->name, 'cheese', 'got a name for the ingredient'); | |
+ | |
+ Data::ObjectDriver::BaseObject->commit; | |
+ | |
+ ## check that we don't have a trace of all the good stuff we cooked | |
+ ok(Recipe->lookup($recipe_id), "still here"); | |
+ ok(Ingredient->lookup($ingredient_pk), "still here"); | |
+ ok defined Recipe->lookup_multi([ $recipe_id ])->[0]; | |
+ | |
+ ## and now test a rollback of a remove | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ $ingredient->remove; | |
+ Data::ObjectDriver::BaseObject->rollback; | |
+ ok(Ingredient->lookup($ingredient_pk), "still here"); | |
+ | |
+ ## finally let's delete it | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ $ingredient->remove; | |
+ Data::ObjectDriver::BaseObject->commit; | |
+ ok(! Ingredient->lookup($ingredient_pk), "finally deleted"); | |
+} | |
+ | |
+## nested transactions | |
+{ | |
+ ## if there is no transaction active this will just warn | |
+ is( Data::ObjectDriver::BaseObject->txn_active, 0); | |
+ diag "will warn"; | |
+ Data::ObjectDriver::BaseObject->commit; | |
+ is( Data::ObjectDriver::BaseObject->txn_active, 0); | |
+ | |
+ ## do a commit in the end | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ is( Data::ObjectDriver::BaseObject->txn_active, 1); | |
+ | |
+ my $recipe = Recipe->new; | |
+ $recipe->title('lasagnes'); | |
+ ok($recipe->save, 'Object saved successfully'); | |
+ diag $recipe->recipe_id; | |
+ | |
+ diag "will warn"; | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ is( Data::ObjectDriver::BaseObject->txn_active, 3); | |
+ | |
+ | |
+ my $ingredient = Ingredient->new; | |
+ $ingredient->recipe_id($recipe->recipe_id); | |
+ $ingredient->name("pasta"); | |
+ ok $ingredient->insert; | |
+ | |
+ Data::ObjectDriver::BaseObject->rollback; | |
+ Data::ObjectDriver::BaseObject->commit; | |
+ Data::ObjectDriver::BaseObject->commit; | |
+ is( Data::ObjectDriver::BaseObject->txn_active, 0); | |
+ | |
+ $recipe = Recipe->lookup($recipe->primary_key); | |
+ $ingredient = Ingredient->lookup($ingredient->primary_key); | |
+ ok $recipe, "got committed"; | |
+ ok $ingredient, "got committed"; | |
+ is $ingredient->name, "pasta"; | |
+ | |
+ ## now test the same thing with a rollback in the end | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ | |
+ $recipe = Recipe->new; | |
+ $recipe->title('lasagnes'); | |
+ ok($recipe->save, 'Object saved successfully'); | |
+ | |
+ diag "will warn"; | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ | |
+ $ingredient = Ingredient->new; | |
+ $ingredient->recipe_id($recipe->recipe_id); | |
+ $ingredient->name("more layers"); | |
+ ok $ingredient->insert; | |
+ | |
+ Data::ObjectDriver::BaseObject->commit; | |
+ Data::ObjectDriver::BaseObject->rollback; | |
+ | |
+ $recipe = Recipe->lookup($recipe->primary_key); | |
+ $ingredient = eval { Ingredient->lookup($ingredient->primary_key) }; | |
+ ok ! $recipe, "rollback"; | |
+ ok ! $ingredient, "rollback"; | |
+} | |
+ | |
+1; | |
Property changes on: t/txn-common.pl | |
___________________________________________________________________ | |
Name: svn:executable | |
+ * | |
Index: t/12-windows.t | |
=================================================================== | |
--- t/12-windows.t (.../tags/release-0.05) (revision 0) | |
+++ t/12-windows.t (.../trunk) (revision 528) | |
@@ -0,0 +1,104 @@ | |
+# $Id$ | |
+ | |
+use strict; | |
+ | |
+use Data::Dumper; | |
+use lib 't/lib'; | |
+use lib 't/lib/cached'; | |
+ | |
+require 't/lib/db-common.pl'; | |
+ | |
+use Test::More; | |
+use Test::Exception; | |
+use Scalar::Util; | |
+BEGIN { | |
+ unless (eval { require DBD::SQLite }) { | |
+ plan skip_all => 'Tests require DBD::SQLite'; | |
+ } | |
+ unless (eval { require Cache::Memory }) { | |
+ plan skip_all => 'Tests require Cache::Memory'; | |
+ } | |
+} | |
+ | |
+plan tests => 19; | |
+ | |
+use Recipe; | |
+use Ingredient; | |
+ | |
+setup_dbs({ | |
+ global => [ qw( recipes ingredients ) ], | |
+}); | |
+ | |
+my $r = Recipe->new; | |
+$r->title("Spaghetti"); | |
+$r->save; | |
+ | |
+my $i = Ingredient->new; | |
+$i->name("Oregano"); | |
+$i->recipe_id($r->recipe_id); | |
+ok( $i->save, "Saved first ingredient" ); | |
+ | |
+$i = Ingredient->new; | |
+$i->name("Salt"); | |
+$i->recipe_id($r->recipe_id); | |
+ok( $i->save, "Saved second ingredient" ); | |
+ | |
+$i = Ingredient->new; | |
+$i->name("Onion"); | |
+$i->recipe_id($r->recipe_id); | |
+ok( $i->save, "Saved third ingredient" ); | |
+ | |
+my $load_count = 0; | |
+my $trigger = sub { $load_count++ }; | |
+Ingredient->add_trigger( 'post_load', $trigger ); | |
+ | |
+$load_count = 0; | |
+Ingredient->driver->clear_cache; | |
+my $iter = Ingredient->search(); | |
+$iter->end; | |
+is( $load_count, 3, "Default behavior: load all objects with plain search method" ); | |
+ | |
+$load_count = 0; | |
+Ingredient->driver->clear_cache; | |
+$iter = Ingredient->search( undef, { window_size => 1 }); | |
+$i = $iter->(); | |
+$iter->end; | |
+is( $load_count, 1, "1 ingredient loaded when window size = 1" ); | |
+ | |
+$load_count = 0; | |
+Ingredient->driver->clear_cache; | |
+$iter = Ingredient->search( undef, { window_size => 2 }); | |
+$i = $iter->(); | |
+$iter->end; | |
+is( $load_count, 2, "2 ingredients loaded" ); | |
+ | |
+$load_count = 0; | |
+Ingredient->driver->clear_cache; | |
+$iter = Ingredient->search( undef, { window_size => 1, sort => "name", direction => "asc" }); | |
+my $i1 = $iter->(); | |
+ok($i1, "First row from windowed select returned"); | |
+is( $i1->name, "Onion", "Name is 'Onion'" ); | |
+my $i2 = $iter->(); | |
+ok( $i2, "Second row from windowed select returned"); | |
+is( $i2->name, "Oregano", "Name is 'Oregano'" ); | |
+ok( $iter->(), "Third row from windowed select returned" ); | |
+ok( ! $iter->(), "No more rows, which is okay" ); | |
+is( $load_count, 3, "3 objects loaded"); | |
+$iter->end; | |
+ | |
+$load_count = 0; | |
+Ingredient->driver->clear_cache; | |
+$iter = Ingredient->search( undef, { window_size => 5, limit => 2, sort => "name", direction => "asc" }); | |
+$i1 = $iter->(); | |
+ok($i1, "First row from windowed select returned"); | |
+is( $i1->name, "Onion", "Name is 'Onion'" ); | |
+$i2 = $iter->(); | |
+ok( $i2, "Second row from windowed select returned"); | |
+is( $i2->name, "Oregano", "Name is 'Oregano'" ); | |
+ok( !$iter->(), "No third row; limit argument respected" ); | |
+is( $load_count, 2, "2 objects loaded; limit argument respected"); | |
+$iter->end; | |
+ | |
+teardown_dbs(qw( global )); | |
+ | |
+print Dumper( Data::ObjectDriver->profiler->query_log ) if $ENV{DOD_PROFILE}; | |
Property changes on: t/12-windows.t | |
___________________________________________________________________ | |
Name: svn:keywords | |
+ Id Revision | |
Index: lib/Data/ObjectDriver/ResultSet.pm | |
=================================================================== | |
--- lib/Data/ObjectDriver/ResultSet.pm (.../tags/release-0.05) (revision 528) | |
+++ lib/Data/ObjectDriver/ResultSet.pm (.../trunk) (revision 528) | |
@@ -7,6 +7,7 @@ | |
use strict; | |
use base qw( Class::Accessor::Fast ); | |
+use List::Util qw(min); | |
## Public/_Private Accessors | |
@@ -91,6 +92,7 @@ | |
my $cur_terms = $self->_terms || {}; | |
my $filter_terms = $self->_filter_terms || {}; | |
foreach my $k (keys %$terms) { | |
+ $self->_results_loaded(0) unless $cur_terms->{$k}; | |
$cur_terms->{$k} = $terms->{$k}; | |
$filter_terms->{$k} = 1 if $self->_results_loaded; | |
} | |
@@ -107,10 +109,12 @@ | |
foreach my $k (keys %$args) { | |
my $val = $args->{$k}; | |
- # If we get a limit arg that is bigger than our existing limit, then | |
+ # If we get a limit arg that is bigger than our existing limit (and | |
+ # we *have* an existing limit), then | |
# make sure we force a requery. Same for any filter arguments. | |
# Same for offset arg that is smaller than existing one. | |
- if ((($k eq 'limit') and (($cur_args->{'limit'}||0) < $val)) or | |
+ if ((($k eq 'limit') and | |
+ ( exists $cur_args->{'limit'} && defined $cur_args->{'limit'} && ($cur_args->{'limit'}||0) < $val)) or | |
(($k eq 'offset') and (($cur_args->{'offset'}||0) > $val)) or | |
($k eq 'filters')) { | |
$self->_results_loaded(0); | |
@@ -136,7 +140,9 @@ | |
if ref $term_names ne 'ARRAY'; | |
foreach my $n (@$term_names) { | |
- delete $terms->{$n}; | |
+ if (delete $terms->{$n}) { | |
+ $self->_results_loaded(0); | |
+ } | |
} | |
} | |
@@ -211,6 +217,23 @@ | |
} | |
} | |
+# look at next() without incrementing the cursor | |
+# like if you just want to see what's coming down the road at you | |
+sub peek_next { | |
+ my $self = shift; | |
+ | |
+ return if $self->is_finished; | |
+ | |
+ # Load the results and return an object | |
+ my $results = $self->_load_results; | |
+ | |
+ my $obj = $results->[$self->_cursor + 1]; | |
+ | |
+ return $obj; | |
+} | |
+ | |
+ | |
+ | |
sub prev { | |
my $self = shift; | |
@@ -239,13 +262,14 @@ | |
sub slice { | |
my $self = shift; | |
my ($start, $end) = @_; | |
- my $limit = $end - $start; | |
# Do we already have results? | |
if ($self->_results) { | |
- return @{ $self->_results }[$start, $end]; | |
+ return [ @{ $self->_results }[$start..min($self->count-1, $end)] ]; | |
} | |
+ my $limit = $end - $start + 1; | |
+ | |
$self->add_offset($start); | |
$self->add_limit($limit); | |
@@ -254,6 +278,21 @@ | |
return $r; | |
} | |
+sub all { | |
+ my $self = shift; | |
+ | |
+ return unless $self->count; | |
+ | |
+ my @obj; | |
+ push @obj, $self->first; | |
+ while (my $obj = $self->next) { | |
+ push @obj, $obj; | |
+ } | |
+ | |
+ $self->rewind; | |
+ return @obj; | |
+} | |
+ | |
sub count { | |
my $self = shift; | |
@@ -415,6 +454,12 @@ | |
return \@r; | |
} | |
+sub rewind { | |
+ my $self = shift; | |
+ $self->is_finished(0); | |
+ $self->_cursor(-1); | |
+ return $self; | |
+} | |
1; | |
__END__ | |
@@ -739,6 +784,39 @@ | |
$obj = $res->next; | |
+=head2 peek_next | |
+ | |
+Retrieve the next item in the resultset WITHOUT advancing the cursor. | |
+ | |
+Arguments: | |
+ | |
+=over 4 | |
+ | |
+=item I<none> | |
+ | |
+=back | |
+ | |
+; Return value | |
+: The next object or undef if past the end of the result set | |
+ | |
+; Notes | |
+: Calling this method will force a DB query. All subsequent calls to I<curr> will return this object | |
+ | |
+; Example | |
+ | |
+ while ($bottle = $res->next){ | |
+ | |
+ if ($bottle->type eq 'Bud Light' | |
+ && $res->peek_next->type eq 'Chimay'){ | |
+ | |
+ $bottle->pass; #don't spoil my palate | |
+ | |
+ }else{ | |
+ $bottle->drink; | |
+ } | |
+ } | |
+ | |
+ | |
=head2 prev | |
Retrieve the previous item in the result set | |
@@ -859,6 +937,10 @@ | |
Set this and you'll see $Data::ObjectDriver::DEBUG output when | |
I go to get the results. | |
+=head2 rewind | |
+ | |
+Move back to the start of the iterator for this instance of results of a query. | |
+ | |
=head2 first | |
Returns the first object in the result set. | |
Index: lib/Data/ObjectDriver/SQL.pm | |
=================================================================== | |
--- lib/Data/ObjectDriver/SQL.pm (.../tags/release-0.05) (revision 528) | |
+++ lib/Data/ObjectDriver/SQL.pm (.../trunk) (revision 528) | |
@@ -6,12 +6,18 @@ | |
use base qw( Class::Accessor::Fast ); | |
-__PACKAGE__->mk_accessors(qw( select select_map select_map_reverse from joins where bind limit offset group order having where_values column_mutator )); | |
+__PACKAGE__->mk_accessors(qw( | |
+ select distinct select_map select_map_reverse | |
+ from joins where bind limit offset group order | |
+ having where_values column_mutator index_hint | |
+ comment | |
+)); | |
sub new { | |
my $class = shift; | |
my $stmt = $class->SUPER::new(@_); | |
$stmt->select([]); | |
+ $stmt->distinct(0); | |
$stmt->select_map({}); | |
$stmt->select_map_reverse({}); | |
$stmt->bind([]); | |
@@ -20,6 +26,7 @@ | |
$stmt->where_values({}); | |
$stmt->having([]); | |
$stmt->joins([]); | |
+ $stmt->index_hint({}); | |
$stmt; | |
} | |
@@ -41,22 +48,34 @@ | |
}; | |
} | |
+sub add_index_hint { | |
+ my $stmt = shift; | |
+ my($table, $hint) = @_; | |
+ $stmt->index_hint->{$table} = { | |
+ type => $hint->{type} || 'USE', | |
+ list => ref($hint->{list}) eq 'ARRAY' ? $hint->{list} : [ $hint->{list} ], | |
+ }; | |
+} | |
+ | |
sub as_sql { | |
my $stmt = shift; | |
my $sql = ''; | |
if (@{ $stmt->select }) { | |
$sql .= 'SELECT '; | |
+ $sql .= 'DISTINCT ' if $stmt->distinct; | |
$sql .= join(', ', map { | |
my $alias = $stmt->select_map->{$_}; | |
$alias && /(?:^|\.)\Q$alias\E$/ ? $_ : "$_ $alias"; | |
} @{ $stmt->select }) . "\n"; | |
} | |
$sql .= 'FROM '; | |
+ | |
## Add any explicit JOIN statements before the non-joined tables. | |
if ($stmt->joins && @{ $stmt->joins }) { | |
my $initial_table_written = 0; | |
for my $j (@{ $stmt->joins }) { | |
my($table, $joins) = map { $j->{$_} } qw( table joins ); | |
+ $table = $stmt->_add_index_hint($table); ## index hint handling | |
$sql .= $table unless $initial_table_written++; | |
for my $join (@{ $j->{joins} }) { | |
$sql .= ' ' . | |
@@ -66,7 +85,12 @@ | |
} | |
$sql .= ', ' if @{ $stmt->from }; | |
} | |
- $sql .= join(', ', @{ $stmt->from }) . "\n"; | |
+ | |
+ if ($stmt->from && @{ $stmt->from }) { | |
+ $sql .= join ', ', map { $stmt->_add_index_hint($_) } @{ $stmt->from }; | |
+ } | |
+ | |
+ $sql .= "\n"; | |
$sql .= $stmt->as_sql_where; | |
$sql .= $stmt->as_aggregate('group'); | |
@@ -74,7 +98,11 @@ | |
$sql .= $stmt->as_aggregate('order'); | |
$sql .= $stmt->as_limit; | |
- $sql; | |
+ my $comment = $stmt->comment; | |
+ if ($comment && $comment =~ /([ 0-9a-zA-Z.:;()_#&,]+)/) { | |
+ $sql .= "-- $1" if $1; | |
+ } | |
+ return $sql; | |
} | |
sub as_limit { | |
@@ -233,6 +261,19 @@ | |
($term, \@bind, $col); | |
} | |
+sub _add_index_hint { | |
+ my $stmt = shift; | |
+ my ($tbl_name) = @_; | |
+ my $hint = $stmt->index_hint->{$tbl_name}; | |
+ return $tbl_name unless $hint && ref($hint) eq 'HASH'; | |
+ if ($hint->{list} && @{ $hint->{list} }) { | |
+ return $tbl_name . ' ' . uc($hint->{type} || 'USE') . ' INDEX (' . | |
+ join (',', @{ $hint->{list} }) . | |
+ ')'; | |
+ } | |
+ return $tbl_name; | |
+} | |
+ | |
1; | |
__END__ | |
@@ -276,6 +317,10 @@ | |
The database columns to select in a C<SELECT> query. | |
+=head2 C<distinct> (boolean) | |
+ | |
+Whether the C<SELECT> query should return DISTINCT rows only. | |
+ | |
=head2 C<select_map> (hashref) | |
The map of database column names to object fields in a C<SELECT> query. Use | |
@@ -401,6 +446,10 @@ | |
Note you can set a single ordering field, or use an arrayref containing | |
multiple ordering fields. | |
+=head2 C<$sql-E<gt>comment([ $comment ])> | |
+ | |
+Returns or sets a simple comment to the SQL statement | |
+ | |
=head1 USAGE | |
=head2 C<Data::ObjectDriver::SQL-E<gt>new()> | |
@@ -421,6 +470,10 @@ | |
C<JOIN> table references for the statement. The structure for the set of joins | |
are as described for the C<joins> attribute member above. | |
+=head2 C<$sql-E<gt>add_index_hint($table, $index)> | |
+ | |
+Specifies a particular index to use for a particular table. | |
+ | |
=head2 C<$sql-E<gt>add_where($column, $value)> | |
Adds a condition on the value of the database column C<$column> to the | |
@@ -518,6 +571,23 @@ | |
HAVING> clause. The expression compares C<$column> using C<$value>, which can | |
be any of the structures described above for the C<add_where()> method. | |
+=head2 C<$sql-E<gt>add_index_hint($table, \@hints)> | |
+ | |
+Addes the index hint into a C<SELECT> query. The structure for the set of | |
+C<\@hints> are arrayref of hashrefs containing these members: | |
+ | |
+=over 4 | |
+ | |
+=item * C<type> (scalar) | |
+ | |
+The name of the type. "USE", "IGNORE or "FORCE". | |
+ | |
+=item * C<list> (arrayref) | |
+ | |
+The list of name of indexes which to use. | |
+ | |
+=back | |
+ | |
=head2 C<$sql-E<gt>as_sql()> | |
Returns the SQL fully representing the SQL statement C<$sql>. | |
Index: lib/Data/ObjectDriver/Driver/DBD.pm | |
=================================================================== | |
--- lib/Data/ObjectDriver/Driver/DBD.pm (.../tags/release-0.05) (revision 528) | |
+++ lib/Data/ObjectDriver/Driver/DBD.pm (.../trunk) (revision 528) | |
@@ -10,6 +10,7 @@ | |
my($name) = @_; | |
die "No Driver" unless $name; | |
my $subclass = join '::', $class, $name; | |
+ no strict 'refs'; | |
unless (defined ${"${subclass}::"}) { | |
eval "use $subclass"; ## no critic | |
die $@ if $@; | |
@@ -40,6 +41,9 @@ | |
sub sql_class { 'Data::ObjectDriver::SQL' } | |
+# Some drivers have problems with prepared caches | |
+sub force_no_prepared_cache { 0 }; | |
+ | |
1; | |
__END__ | |
Index: lib/Data/ObjectDriver/Driver/Partition.pm | |
=================================================================== | |
--- lib/Data/ObjectDriver/Driver/Partition.pm (.../tags/release-0.05) (revision 528) | |
+++ lib/Data/ObjectDriver/Driver/Partition.pm (.../trunk) (revision 528) | |
@@ -3,6 +3,7 @@ | |
package Data::ObjectDriver::Driver::Partition; | |
use strict; | |
use warnings; | |
+use Carp(); | |
use base qw( Data::ObjectDriver Class::Accessor::Fast ); | |
@@ -13,18 +14,21 @@ | |
$driver->SUPER::init(@_); | |
my %param = @_; | |
$driver->get_driver($param{get_driver}); | |
+ $driver->{__working_drivers} = []; | |
$driver; | |
} | |
sub lookup { | |
my $driver = shift; | |
my($class, $id) = @_; | |
+ return unless $id; | |
$driver->get_driver->($id)->lookup($class, $id); | |
} | |
sub lookup_multi { | |
my $driver = shift; | |
my($class, $ids) = @_; | |
+ return [] unless @$ids; | |
$driver->get_driver->($ids->[0])->lookup_multi($class, $ids); | |
} | |
@@ -55,9 +59,56 @@ | |
} else { | |
$d = $driver->get_driver->(@rest); | |
} | |
+ | |
+ if ( $driver->txn_active ) { | |
+ $driver->add_working_driver($d); | |
+ } | |
$d->$meth($obj, @rest); | |
} | |
+sub add_working_driver { | |
+ my $driver = shift; | |
+ my $part_driver = shift; | |
+ if (! $part_driver->txn_active) { | |
+ $part_driver->begin_work; | |
+ push @{$driver->{__working_drivers}}, $part_driver; | |
+ } | |
+} | |
+ | |
+sub commit { | |
+ my $driver = shift; | |
+ | |
+ ## if the driver has its own internal txn_active flag | |
+ ## off, we don't bother ending. Maybe we already did | |
+ return unless $driver->txn_active; | |
+ | |
+ $driver->SUPER::commit(@_); | |
+ _end_txn($driver, 'commit', @_); | |
+} | |
+ | |
+sub rollback { | |
+ my $driver = shift; | |
+ | |
+ ## if the driver has its own internal txn_active flag | |
+ ## off, we don't bother ending. Maybe we already did | |
+ return unless $driver->txn_active; | |
+ | |
+ $driver->SUPER::rollback(@_); | |
+ _end_txn($driver, 'rollback', @_); | |
+} | |
+ | |
+sub _end_txn { | |
+ my ($driver, $method) = @_; | |
+ | |
+ my $wd = $driver->{__working_drivers}; | |
+ $driver->{__working_drivers} = []; | |
+ | |
+ for my $part_driver (@{ $wd || [] }) { | |
+ $part_driver->$method; | |
+ } | |
+} | |
+ | |
+ | |
1; | |
__END__ | |
Index: lib/Data/ObjectDriver/Driver/Multiplexer.pm | |
=================================================================== | |
--- lib/Data/ObjectDriver/Driver/Multiplexer.pm (.../tags/release-0.05) (revision 528) | |
+++ lib/Data/ObjectDriver/Driver/Multiplexer.pm (.../trunk) (revision 528) | |
@@ -93,9 +93,10 @@ | |
my($meth, $obj, @args) = @_; | |
my $orig_obj = Storable::dclone($obj); | |
my $ret; | |
+ | |
## We want to be sure to have the initial and final state of the object | |
## strictly identical as if we made only one call on $obj | |
- ## (Perhaps it's a bit overkill ? playing with 'changed_cols' may suffice) | |
+ ## (Perhaps it's a bit overkill ? playing with 'changed_cols' may do the trick) | |
for my $sub_driver (@{ $driver->drivers }) { | |
$obj = Storable::dclone($orig_obj); | |
$ret = $sub_driver->$meth($obj, @args); | |
@@ -103,15 +104,33 @@ | |
return $ret; | |
} | |
-## Nobody should ask a dbh for us directly, if someone does, this | |
-## is probably to change handler properties (transaction). So | |
-## we assume that only the on_lookup is important (I said it was experimental..) | |
-sub get_dbh { | |
+sub begin_work { | |
my $driver = shift; | |
- my $subdriver = $driver->on_lookup; | |
- return $subdriver->get_dbh(@_); | |
+ $driver->SUPER::begin_work(@_); | |
+ for my $sub_driver (@{ $driver->drivers }) { | |
+ $sub_driver->begin_work; | |
+ } | |
} | |
+sub commit { | |
+ my $driver = shift; | |
+ $driver->SUPER::commit(@_); | |
+ $driver->_end_txn('commit', @_); | |
+} | |
+ | |
+sub rollback { | |
+ my $driver = shift; | |
+ $driver->SUPER::rollback(@_); | |
+ $driver->_end_txn('rollback', @_); | |
+} | |
+ | |
+sub _end_txn { | |
+ my ($driver, $method) = @_; | |
+ for my $sub_driver (@{ $driver->drivers }) { | |
+ $sub_driver->$method; | |
+ } | |
+} | |
+ | |
1; | |
__END__ | |
Index: lib/Data/ObjectDriver/Driver/BaseCache.pm | |
=================================================================== | |
--- lib/Data/ObjectDriver/Driver/BaseCache.pm (.../tags/release-0.05) (revision 528) | |
+++ lib/Data/ObjectDriver/Driver/BaseCache.pm (.../trunk) (revision 528) | |
@@ -9,7 +9,7 @@ | |
use Carp (); | |
-__PACKAGE__->mk_accessors(qw( cache fallback )); | |
+__PACKAGE__->mk_accessors(qw( cache fallback txn_buffer)); | |
__PACKAGE__->mk_classdata(qw( Disabled )); | |
sub deflate { $_[1] } | |
@@ -29,9 +29,45 @@ | |
or Carp::croak("cache is required"); | |
$driver->fallback($param{fallback}) | |
or Carp::croak("fallback is required"); | |
+ $driver->txn_buffer([]); | |
$driver; | |
} | |
+sub begin_work { | |
+ my $driver = shift; | |
+ my $rv = $driver->fallback->begin_work(@_); | |
+ $driver->SUPER::begin_work(@_); | |
+ return $rv; | |
+} | |
+ | |
+sub commit { | |
+ my $driver = shift; | |
+ return unless $driver->txn_active; | |
+ | |
+ my $rv = $driver->fallback->commit(@_); | |
+ | |
+ $driver->debug(sprintf("%14s", "COMMIT(" . scalar(@{$driver->txn_buffer}) . ")") . ": driver=$driver"); | |
+ while (my $cb = shift @{$driver->txn_buffer}) { | |
+ $cb->(); | |
+ } | |
+ $driver->SUPER::commit(@_); | |
+ | |
+ return $rv; | |
+} | |
+ | |
+sub rollback { | |
+ my $driver = shift; | |
+ return unless $driver->txn_active; | |
+ my $rv = $driver->fallback->rollback(@_); | |
+ | |
+ $driver->debug(sprintf("%14s", "ROLLBACK(" . scalar(@{$driver->txn_buffer}) . ")") . ": driver=$driver"); | |
+ $driver->txn_buffer([]); | |
+ | |
+ $driver->SUPER::rollback(@_); | |
+ | |
+ return $rv; | |
+} | |
+ | |
sub cache_object { | |
my $driver = shift; | |
my($obj) = @_; | |
@@ -40,10 +76,12 @@ | |
## If it's already cached in this layer, assume it's already cached in | |
## all layers below this, as well. | |
unless (exists $obj->{__cached} && $obj->{__cached}{ref $driver}) { | |
- $driver->add_to_cache( | |
+ $driver->modify_cache(sub { | |
+ $driver->add_to_cache( | |
$driver->cache_key(ref($obj), $obj->primary_key), | |
$driver->deflate($obj) | |
); | |
+ }); | |
$driver->fallback->cache_object($obj); | |
} | |
} | |
@@ -53,7 +91,7 @@ | |
my($class, $id) = @_; | |
return unless defined $id; | |
return $driver->fallback->lookup($class, $id) | |
- if $driver->Disabled; | |
+ if $driver->Disabled or $driver->txn_active; | |
my $key = $driver->cache_key($class, $id); | |
my $obj = $driver->get_from_cache($key); | |
if ($obj) { | |
@@ -83,7 +121,7 @@ | |
my $driver = shift; | |
my($class, $ids) = @_; | |
return $driver->fallback->lookup_multi($class, $ids) | |
- if $driver->Disabled; | |
+ if $driver->Disabled or $driver->txn_active; | |
my %id2key = map { $_ => $driver->cache_key($class, $_) } grep { defined } @$ids; | |
my $got = $driver->get_multi_from_cache(values %id2key); | |
@@ -149,14 +187,35 @@ | |
local $args->{fetchonly} = $class->primary_key_tuple; | |
## Disable triggers for this load. We don't want the post_load trigger | |
## being called twice. | |
- $args->{no_triggers} = 1; | |
+ local $args->{no_triggers} = 1; | |
my @objs = $driver->fallback->search($class, $terms, $args); | |
- ## Load all of the objects using a lookup_multi, which is fast from | |
- ## cache. | |
- my $objs = $driver->lookup_multi($class, [ map { $_->primary_key } @objs ]); | |
+ my $windowed = (!wantarray) && $args->{window_size}; | |
- $driver->list_or_iterator($objs); | |
+ if ( $windowed ) { | |
+ my @window; | |
+ my $window_size = $args->{window_size}; | |
+ my $iter = sub { | |
+ my $d = $driver; | |
+ while ( (!@window) && @objs ) { | |
+ my $objs = $driver->lookup_multi( | |
+ $class, | |
+ [ map { $_->primary_key } | |
+ splice( @objs, 0, $window_size ) ] | |
+ ); | |
+ # A small possibility exists that we may fetch | |
+ # some IDs here that no longer exist; grep these out | |
+ @window = grep { defined $_ } @$objs if $objs; | |
+ } | |
+ return @window ? shift @window : undef; | |
+ }; | |
+ return Data::ObjectDriver::Iterator->new($iter, sub { @objs = (); @window = () }); | |
+ } else { | |
+ ## Load all of the objects using a lookup_multi, which is fast from | |
+ ## cache. | |
+ my $objs = $driver->lookup_multi($class, [ map { $_->primary_key } @objs ]); | |
+ return $driver->list_or_iterator($objs); | |
+ } | |
} | |
sub update { | |
@@ -166,7 +225,9 @@ | |
if $driver->Disabled; | |
my $ret = $driver->fallback->update($obj); | |
my $key = $driver->cache_key(ref($obj), $obj->primary_key); | |
- $driver->update_cache($key, $driver->deflate($obj)); | |
+ $driver->modify_cache(sub { | |
+ $driver->update_cache($key, $driver->deflate($obj)); | |
+ }); | |
return $ret; | |
} | |
@@ -181,7 +242,9 @@ | |
my $ret = $driver->fallback->replace($obj); | |
if ($has_pk) { | |
my $key = $driver->cache_key(ref($obj), $obj->primary_key); | |
- $driver->update_cache($key, $driver->deflate($obj)); | |
+ $driver->modify_cache(sub { | |
+ $driver->update_cache($key, $driver->deflate($obj)); | |
+ }); | |
} | |
return $ret; | |
} | |
@@ -199,11 +262,22 @@ | |
Carp::croak("nofetch option isn't compatible with a cache driver"); | |
} | |
if (ref $obj) { | |
- $driver->remove_from_cache($driver->cache_key(ref($obj), $obj->primary_key)); | |
+ $driver->uncache_object($obj); | |
} | |
$driver->fallback->remove(@_); | |
} | |
+sub uncache_object { | |
+ my $driver = shift; | |
+ my($obj) = @_; | |
+ my $key = $driver->cache_key(ref($obj), $obj->primary_key); | |
+ return $driver->modify_cache(sub { | |
+ delete $obj->{__cached}; | |
+ $driver->remove_from_cache($key); | |
+ $driver->fallback->uncache_object($obj); | |
+ }); | |
+} | |
+ | |
sub cache_key { | |
my $driver = shift; | |
my($class, $id) = @_; | |
@@ -217,6 +291,18 @@ | |
return $key; | |
} | |
+# if we're operating within a transaction then we need to buffer CRUD | |
+# and only commit to the cache upon commit | |
+sub modify_cache { | |
+ my ($driver, $cb) = @_; | |
+ | |
+ unless ($driver->txn_active) { | |
+ return $cb->(); | |
+ } | |
+ $driver->debug(sprintf("%14s", "BUFFER(1)") . ": driver=$driver"); | |
+ push @{$driver->txn_buffer} => $cb; | |
+} | |
+ | |
sub DESTROY { } | |
sub AUTOLOAD { | |
Index: lib/Data/ObjectDriver/Driver/DBD/SQLite.pm | |
=================================================================== | |
--- lib/Data/ObjectDriver/Driver/DBD/SQLite.pm (.../tags/release-0.05) (revision 528) | |
+++ lib/Data/ObjectDriver/Driver/DBD/SQLite.pm (.../trunk) (revision 528) | |
@@ -56,7 +56,10 @@ | |
return 1; | |
} | |
+# TODO this should check the version | |
+sub force_no_prepared_cache { 1 }; | |
+ | |
1; | |
=pod | |
Index: lib/Data/ObjectDriver/Driver/DBI.pm | |
=================================================================== | |
--- lib/Data/ObjectDriver/Driver/DBI.pm (.../tags/release-0.05) (revision 528) | |
+++ lib/Data/ObjectDriver/Driver/DBI.pm (.../trunk) (revision 528) | |
@@ -13,8 +13,10 @@ | |
use Data::ObjectDriver::Driver::DBD; | |
use Data::ObjectDriver::Iterator; | |
-__PACKAGE__->mk_accessors(qw( dsn username password connect_options dbh get_dbh dbd prefix )); | |
+__PACKAGE__->mk_accessors(qw( dsn username password connect_options dbh get_dbh dbd prefix reuse_dbh force_no_prepared_cache)); | |
+our $FORCE_NO_PREPARED_CACHE = 0; | |
+ | |
sub init { | |
my $driver = shift; | |
my %param = @_; | |
@@ -45,21 +47,45 @@ | |
} | |
} | |
+# Some versions of SQLite require the undefing to finalise properly | |
+sub _close_sth { | |
+ my $sth = shift; | |
+ $sth->finish; | |
+ undef $sth; | |
+} | |
+ | |
+# Some versions of SQLite have problems with prepared caching due to finalisation order | |
+sub _prepare_cached { | |
+ my $driver = shift; | |
+ my $dbh = shift; | |
+ my $sql = shift; | |
+ return ($FORCE_NO_PREPARED_CACHE || $driver->force_no_prepared_cache || $driver->dbd->force_no_prepared_cache)? $dbh->prepare($sql) : $dbh->prepare_cached($sql); | |
+} | |
+ | |
+my %Handles; | |
sub init_db { | |
my $driver = shift; | |
my $dbh; | |
- eval { | |
- $dbh = DBI->connect($driver->dsn, $driver->username, $driver->password, | |
- { RaiseError => 1, PrintError => 0, AutoCommit => 1, | |
- %{$driver->connect_options || {}} }) | |
- or Carp::croak("Connection error: " . $DBI::errstr); | |
- }; | |
- if ($@) { | |
- Carp::croak($@); | |
+ if ($driver->reuse_dbh) { | |
+ $dbh = $Handles{$driver->dsn}; | |
} | |
+ unless ($dbh) { | |
+ eval { | |
+ $dbh = DBI->connect($driver->dsn, $driver->username, $driver->password, | |
+ { RaiseError => 1, PrintError => 0, AutoCommit => 1, | |
+ %{$driver->connect_options || {}} }) | |
+ or Carp::croak("Connection error: " . $DBI::errstr); | |
+ }; | |
+ if ($@) { | |
+ Carp::croak($@); | |
+ } | |
+ } | |
+ if ($driver->reuse_dbh) { | |
+ $Handles{$driver->dsn} = $dbh; | |
+ } | |
$driver->dbd->init_dbh($dbh); | |
$driver->{__dbh_init_by_driver} = 1; | |
- $dbh; | |
+ return $dbh; | |
} | |
sub rw_handle { | |
@@ -88,7 +114,7 @@ | |
my $rec = {}; | |
my $sth = $driver->fetch($rec, $obj, $terms, $args); | |
$sth->fetch; | |
- $sth->finish; | |
+ _close_sth($sth); | |
$driver->end_query($sth); | |
return $rec; | |
} | |
@@ -98,8 +124,8 @@ | |
my($rec, $class, $orig_terms, $orig_args) = @_; | |
## Use (shallow) duplicates so the pre_search trigger can modify them. | |
- my $terms = defined $orig_terms ? ( ref $orig_terms eq 'ARRAY' ? [ @$orig_terms ] : { %$orig_terms } ) : undef; | |
- my $args = defined $orig_args ? { %$orig_args } : undef; | |
+ my $terms = defined $orig_terms ? ( ref $orig_terms eq 'ARRAY' ? [ @$orig_terms ] : { %$orig_terms } ) : {}; | |
+ my $args = defined $orig_args ? { %$orig_args } : {}; | |
$class->call_trigger('pre_search', $terms, $args); | |
my $stmt = $driver->prepare_statement($class, $terms, $args); | |
@@ -114,7 +140,7 @@ | |
$sql .= "\nFOR UPDATE" if $orig_args->{for_update}; | |
my $dbh = $driver->r_handle($class->properties->{db}); | |
$driver->start_query($sql, $stmt->{bind}); | |
- my $sth = $orig_args->{no_cached_prepare} ? $dbh->prepare($sql) : $dbh->prepare_cached($sql); | |
+ my $sth = $orig_args->{no_cached_prepare} ? $dbh->prepare($sql) : $driver->_prepare_cached($dbh, $sql); | |
$sth->execute(@{ $stmt->{bind} }); | |
$sth->bind_columns(undef, @bind); | |
@@ -142,7 +168,7 @@ | |
my $d = $driver; | |
unless ($sth->fetch) { | |
- $sth->finish; | |
+ _close_sth($sth); | |
$driver->end_query($sth); | |
return; | |
} | |
@@ -151,12 +177,10 @@ | |
$obj->set_values_internal($rec); | |
## Don't need a duplicate as there's no previous version in memory | |
## to preserve. | |
+ $obj->{__is_stored} = 1; | |
$obj->call_trigger('post_load') unless $args->{no_triggers}; | |
$obj; | |
}; | |
- my $iterator = Data::ObjectDriver::Iterator->new( | |
- $iter, sub { $sth->finish; $driver->end_query($sth) }, | |
- ); | |
if (wantarray) { | |
my @objs = (); | |
@@ -166,6 +190,9 @@ | |
} | |
return @objs; | |
} else { | |
+ my $iterator = Data::ObjectDriver::Iterator->new( | |
+ $iter, sub { _close_sth($sth); $driver->end_query($sth) }, | |
+ ); | |
return $iterator; | |
} | |
return; | |
@@ -186,7 +213,7 @@ | |
return [] unless @$ids; | |
my @got; | |
## If it's a single-column PK, assume it's in one partition, and | |
- ## use an OR search. | |
+ ## use an OR search. FIXME: can we instead check for partitioning? | |
unless (ref($ids->[0])) { | |
my $terms = $class->primary_key_to_terms([ $ids ]); | |
my @sqlgot = $driver->search($class, $terms, { is_pk => 1 }); | |
@@ -194,7 +221,7 @@ | |
@got = map { defined $_ ? $hgot{$_} : undef } @$ids; | |
} else { | |
for my $id (@$ids) { | |
- push @got, $class->driver->lookup($class, $id); | |
+ push @got, eval{ $class->driver->lookup($class, $id) }; | |
} | |
} | |
\@got; | |
@@ -206,16 +233,16 @@ | |
my $dbh = $driver->r_handle; | |
$driver->start_query($sql, $bind); | |
- my $sth = $dbh->prepare_cached($sql); | |
+ my $sth = $driver->_prepare_cached($dbh, $sql); | |
$sth->execute(@$bind); | |
$sth->bind_columns(undef, \my($val)); | |
unless ($sth->fetch) { | |
- $sth->finish; | |
+ _close_sth($sth); | |
$driver->end_query($sth); | |
return; | |
} | |
- $sth->finish; | |
+ _close_sth($sth); | |
$driver->end_query($sth); | |
return $val; | |
@@ -237,6 +264,7 @@ | |
my $terms = $obj->primary_key_to_terms; | |
my $class = ref $obj; | |
+ $terms ||= {}; | |
$class->call_trigger('pre_search', $terms); | |
my $tbl = $driver->table_for($obj); | |
@@ -245,10 +273,10 @@ | |
$sql .= $stmt->as_sql_where; | |
my $dbh = $driver->r_handle($obj->properties->{db}); | |
$driver->start_query($sql, $stmt->{bind}); | |
- my $sth = $dbh->prepare_cached($sql); | |
+ my $sth = $driver->_prepare_cached($dbh, $sql); | |
$sth->execute(@{ $stmt->{bind} }); | |
my $exists = $sth->fetch; | |
- $sth->finish; | |
+ _close_sth($sth); | |
$driver->end_query($sth); | |
return $exists; | |
@@ -257,8 +285,9 @@ | |
sub replace { | |
my $driver = shift; | |
if ($driver->dbd->can_replace) { | |
- $driver->_insert_or_replace(@_, { replace => 1 }); | |
- } else { | |
+ return $driver->_insert_or_replace(@_, { replace => 1 }); | |
+ } | |
+ if (! $driver->txn_active) { | |
$driver->begin_work; | |
eval { | |
$driver->remove(@_); | |
@@ -269,7 +298,10 @@ | |
Carp::croak("REPLACE transaction error $driver: $@"); | |
} | |
$driver->commit; | |
+ return; | |
} | |
+ $driver->remove(@_); | |
+ $driver->insert(@_); | |
} | |
sub insert { | |
@@ -321,7 +353,7 @@ | |
'VALUES (' . join(', ', ('?') x @$cols) . ')' . "\n"; | |
my $dbh = $driver->rw_handle($obj->properties->{db}); | |
$driver->start_query($sql, $obj->{column_values}); | |
- my $sth = $dbh->prepare_cached($sql); | |
+ my $sth = $driver->_prepare_cached($dbh, $sql); | |
my $i = 1; | |
my $col_defs = $obj->properties->{column_defs}; | |
for my $col (@$cols) { | |
@@ -330,8 +362,9 @@ | |
my $attr = $dbd->bind_param_attributes($type, $obj, $col); | |
$sth->bind_param($i++, $val, $attr); | |
} | |
- $sth->execute; | |
- $sth->finish; | |
+ eval { $sth->execute }; | |
+ die "Failed to execute $sql with ".join(", ",@$cols).": $@" if $@; | |
+ _close_sth($sth); | |
$driver->end_query($sth); | |
## Now, if we didn't have an object ID, we need to grab the | |
@@ -349,12 +382,14 @@ | |
$obj->call_trigger('post_save', $orig_obj); | |
$obj->call_trigger('post_insert', $orig_obj); | |
+ $orig_obj->{__is_stored} = 1; | |
$orig_obj->{changed_cols} = {}; | |
1; | |
} | |
sub update { | |
my $driver = shift; | |
+ | |
my($orig_obj, $terms) = @_; | |
## Use a duplicate so the pre_save trigger can modify it. | |
@@ -387,7 +422,7 @@ | |
my $dbh = $driver->rw_handle($obj->properties->{db}); | |
$driver->start_query($sql, $obj->{column_values}); | |
- my $sth = $dbh->prepare_cached($sql); | |
+ my $sth = $driver->_prepare_cached($dbh, $sql); | |
my $i = 1; | |
my $col_defs = $obj->properties->{column_defs}; | |
for my $col (@changed_cols) { | |
@@ -403,7 +438,7 @@ | |
} | |
my $rows = $sth->execute; | |
- $sth->finish; | |
+ _close_sth($sth); | |
$driver->end_query($sth); | |
$obj->call_trigger('post_save', $orig_obj); | |
@@ -448,13 +483,14 @@ | |
$sql .= $stmt->as_sql_where; | |
my $dbh = $driver->rw_handle($obj->properties->{db}); | |
$driver->start_query($sql, $stmt->{bind}); | |
- my $sth = $dbh->prepare_cached($sql); | |
+ my $sth = $driver->_prepare_cached($dbh, $sql); | |
my $result = $sth->execute(@{ $stmt->{bind} }); | |
- $sth->finish; | |
+ _close_sth($sth); | |
$driver->end_query($sth); | |
$obj->call_trigger('post_remove', $orig_obj); | |
+ $orig_obj->{__is_stored} = 1; | |
return $result; | |
} | |
@@ -482,9 +518,9 @@ | |
my $dbh = $driver->rw_handle($class->properties->{db}); | |
$driver->start_query($sql, $stmt->{bind}); | |
- my $sth = $dbh->prepare_cached($sql); | |
+ my $sth = $driver->_prepare_cached($dbh, $sql); | |
my $result = $sth->execute(@{ $stmt->{bind} }); | |
- $sth->finish; | |
+ _close_sth($sth); | |
$driver->end_query($sth); | |
return $result; | |
} | |
@@ -523,19 +559,29 @@ | |
sub begin_work { | |
my $driver = shift; | |
+ | |
+ return if $driver->txn_active; | |
+ | |
my $dbh = $driver->dbh; | |
+ | |
unless ($dbh) { | |
$driver->{__delete_dbh_after_txn} = 1; | |
$dbh = $driver->rw_handle; | |
$driver->dbh($dbh); | |
} | |
- eval { | |
- $dbh->begin_work; | |
- }; | |
- if ($@) { | |
- $driver->rollback; | |
- Carp::croak("Begin work failed for driver $driver: $@"); | |
+ | |
+ if ($dbh->{AutoCommit}) { | |
+ eval { | |
+ $dbh->begin_work; | |
+ }; | |
+ if (my $err = $@) { | |
+ $driver->rollback; | |
+ Carp::croak("Begin work failed for driver $driver: $err"); | |
+ } | |
} | |
+ ## if for some reason AutoCommit was 0 but txn_active was false, | |
+ ## then we set it to true now | |
+ $driver->txn_active(1); | |
} | |
sub commit { shift->_end_txn('commit') } | |
@@ -544,11 +590,21 @@ | |
sub _end_txn { | |
my $driver = shift; | |
my($action) = @_; | |
- my $dbh = $driver->dbh | |
- or Carp::croak("$action called without a stored handle--begin_work?"); | |
- eval { $dbh->$action() }; | |
- if ($@) { | |
- Carp::croak("$action failed for driver $driver: $@"); | |
+ | |
+ ## if the driver has its own internal txn_active flag | |
+ ## off, we don't bother ending. Maybe we already did | |
+ if ($driver->txn_active) { | |
+ $driver->txn_active(0); | |
+ | |
+ my $dbh = $driver->dbh | |
+ or Carp::croak("$action called without a stored handle--begin_work?"); | |
+ | |
+ unless ($dbh->{AutoCommit}) { | |
+ eval { $dbh->$action() }; | |
+ if ($@) { | |
+ Carp::croak("$action failed for driver $driver: $@"); | |
+ } | |
+ } | |
} | |
if ($driver->{__delete_dbh_after_txn}) { | |
$driver->dbh(undef); | |
@@ -629,8 +685,9 @@ | |
$stmt->order(\@order); | |
} | |
} | |
- $stmt->limit($args->{limit}) if $args->{limit}; | |
- $stmt->offset($args->{offset}) if $args->{offset}; | |
+ $stmt->limit( $args->{limit} ) if $args->{limit}; | |
+ $stmt->offset( $args->{offset} ) if $args->{offset}; | |
+ $stmt->comment( $args->{comment} ) if $args->{comment}; | |
if (my $terms = $args->{having}) { | |
for my $col (keys %$terms) { | |
Index: lib/Data/ObjectDriver/BaseObject.pm | |
=================================================================== | |
--- lib/Data/ObjectDriver/BaseObject.pm (.../tags/release-0.05) (revision 528) | |
+++ lib/Data/ObjectDriver/BaseObject.pm (.../trunk) (revision 528) | |
@@ -5,7 +5,7 @@ | |
use warnings; | |
our $HasWeaken; | |
-eval "use Scalar::Util qw(weaken)"; | |
+eval q{ use Scalar::Util qw(weaken) }; ## no critic | |
$HasWeaken = !$@; | |
use Carp (); | |
@@ -16,6 +16,10 @@ | |
use Data::ObjectDriver::ResultSet; | |
+## Global Transaction variables | |
+our @WorkingDrivers; | |
+our $TransactionLevel = 0; | |
+ | |
sub install_properties { | |
my $class = shift; | |
my($props) = @_; | |
@@ -269,6 +273,27 @@ | |
\%terms; | |
} | |
+sub is_same { | |
+ my($obj, $other) = @_; | |
+ | |
+ my @a; | |
+ for my $o ($obj, $other) { | |
+ push @a, [ map { $o->$_() } @{ $o->primary_key_tuple }]; | |
+ } | |
+ return is_same_array( @a ); | |
+} | |
+ | |
+sub object_is_stored { | |
+ my $obj = shift; | |
+ return $obj->{__is_stored} ? 1 : 0; | |
+} | |
+sub pk_str { | |
+ my ($obj) = @_; | |
+ my $pk = $obj->primary_key; | |
+ return $pk unless ref ($pk) eq 'ARRAY'; | |
+ return join (":", @$pk); | |
+} | |
+ | |
sub has_primary_key { | |
my $obj = shift; | |
return unless @{$obj->primary_key_tuple}; | |
@@ -300,7 +325,7 @@ | |
my $values = shift; | |
for my $col (keys %$values) { | |
unless ( $obj->has_column($col) ) { | |
- Carp::croak("You tried to set inexistent column $col to value $values->{$col} on " . ref($obj)); | |
+ Carp::croak("You tried to set non-existent column $col to value $values->{$col} on " . ref($obj)); | |
} | |
$obj->$col($values->{$col}); | |
} | |
@@ -468,24 +493,43 @@ | |
my $class = shift; | |
my($terms, $args) = @_; | |
my $driver = $class->driver; | |
- my @objs = $driver->search($class, $terms, $args); | |
+ if (wantarray) { | |
+ my @objs = $driver->search($class, $terms, $args); | |
- ## Don't attempt to cache objects where the caller specified fetchonly, | |
- ## because they won't be complete. | |
- ## Also skip this step if we don't get any objects back from the search | |
- if (!$args->{fetchonly} || !@objs) { | |
- for my $obj (@objs) { | |
- $driver->cache_object($obj) if $obj; | |
+ ## Don't attempt to cache objects where the caller specified fetchonly, | |
+ ## because they won't be complete. | |
+ ## Also skip this step if we don't get any objects back from the search | |
+ if (!$args->{fetchonly} || !@objs) { | |
+ for my $obj (@objs) { | |
+ $driver->cache_object($obj) if $obj; | |
+ } | |
} | |
+ return @objs; | |
+ } else { | |
+ my $iter = $driver->search($class, $terms, $args); | |
+ return $iter if $args->{fetchonly}; | |
+ | |
+ my $caching_iter = sub { | |
+ my $d = $driver; | |
+ | |
+ my $o = $iter->(); | |
+ unless ($o) { | |
+ $iter->end; | |
+ return; | |
+ } | |
+ $driver->cache_object($o); | |
+ return $o; | |
+ }; | |
+ return Data::ObjectDriver::Iterator->new($caching_iter, sub { $iter->end }); | |
} | |
- $driver->list_or_iterator(\@objs); | |
} | |
-sub remove { shift->_proxy('remove', @_) } | |
-sub update { shift->_proxy('update', @_) } | |
-sub insert { shift->_proxy('insert', @_) } | |
-sub replace { shift->_proxy('replace', @_) } | |
-sub fetch_data { shift->_proxy('fetch_data', @_) } | |
+sub remove { shift->_proxy( 'remove', @_ ) } | |
+sub update { shift->_proxy( 'update', @_ ) } | |
+sub insert { shift->_proxy( 'insert', @_ ) } | |
+sub replace { shift->_proxy( 'replace', @_ ) } | |
+sub fetch_data { shift->_proxy( 'fetch_data', @_ ) } | |
+sub uncache_object { shift->_proxy( 'uncache_object', @_ ) } | |
sub refresh { | |
my $obj = shift; | |
@@ -496,12 +540,72 @@ | |
return 1; | |
} | |
+## NOTE: I wonder if it could be useful to BaseObject superclass | |
+## to override the global transaction flag. If so, I'd add methods | |
+## to manipulate this flag and the working drivers. -- Yann | |
sub _proxy { | |
my $obj = shift; | |
my($meth, @args) = @_; | |
- $obj->driver->$meth($obj, @args); | |
+ my $driver = $obj->driver; | |
+ ## faster than $obj->txn_active && ! $driver->txn_active but see note. | |
+ if ($TransactionLevel && ! $driver->txn_active) { | |
+ $driver->begin_work; | |
+ push @WorkingDrivers, $driver; | |
+ } | |
+ $driver->$meth($obj, @args); | |
} | |
+sub txn_active { $TransactionLevel } | |
+ | |
+sub begin_work { | |
+ my $class = shift; | |
+ if ($TransactionLevel > 0) { | |
+ warn __PACKAGE__ . ": one ore more transaction already active: $TransactionLevel"; | |
+ } | |
+ $TransactionLevel++; | |
+} | |
+ | |
+sub commit { | |
+ my $class = shift; | |
+ $class->_end_txn('commit'); | |
+} | |
+ | |
+sub rollback { | |
+ my $class = shift; | |
+ $class->_end_txn('rollback'); | |
+} | |
+ | |
+sub _end_txn { | |
+ my $class = shift; | |
+ my $meth = shift; | |
+ | |
+ ## Ignore nested transactions | |
+ if ($TransactionLevel > 1) { | |
+ $TransactionLevel--; | |
+ return; | |
+ } | |
+ | |
+ if (! $TransactionLevel) { | |
+ warn __PACKAGE__ . ": no transaction active, ignored $meth"; | |
+ return; | |
+ } | |
+ my @wd = @WorkingDrivers; | |
+ $TransactionLevel--; | |
+ @WorkingDrivers = (); | |
+ | |
+ for my $driver (@wd) { | |
+ $driver->$meth; | |
+ } | |
+} | |
+ | |
+sub txn_debug { | |
+ my $class = shift; | |
+ return { | |
+ txn => $TransactionLevel, | |
+ drivers => \@WorkingDrivers, | |
+ }; | |
+} | |
+ | |
sub deflate { { columns => shift->column_values } } | |
sub inflate { | |
@@ -930,10 +1034,31 @@ | |
Returns the I<names> of the primary key fields of C<Class> objects. | |
+=head2 C<$obj-E<gt>is_same($other_obj)> | |
+ | |
+Do a primary key check on C<$obj> and $<other_obj> and returns true only if they | |
+are identical. | |
+ | |
+=head2 C<$obj-E<gt>object_is_stored()> | |
+ | |
+Returns true if the object hasn't been stored in the database yet. | |
+This is particularily useful in triggers where you can then determine | |
+if the object is being INSERTED or just UPDATED. | |
+ | |
+=head2 C<$obj-E<gt>pk_str()> | |
+ | |
+returns the primay key has a printable string. | |
+ | |
=head2 C<$obj-E<gt>has_primary_key()> | |
Returns whether the given object has values for all of its primary key fields. | |
+=head2 C<$obj-E<gt>uncache_object()> | |
+ | |
+If you use a Cache driver, returned object will be automatically cached as a result | |
+of common retrieve operations. In some rare cases you may want the cache to be cleared | |
+explicitely, and this method provides you with a way to do it. | |
+ | |
=head2 C<$obj-E<gt>primary_key_to_terms([$id])> | |
Returns C<$obj>'s primary key as a hashref of values keyed on column names, | |
@@ -1053,6 +1178,41 @@ | |
object in the class I<Class>. That is, undoes the operation C<$deflated = | |
$obj-E<gt>deflate()> by returning a new object equivalent to C<$obj>. | |
+=head1 TRANSACTION SUPPORT AND METHODS | |
+ | |
+=head2 Introduction | |
+ | |
+When dealing with the methods on this class, the transactions are global, | |
+i.e: applied to all drivers. You can still enable transactions per driver | |
+if you directly use the driver API. | |
+ | |
+=head2 C<Class-E<gt>begin_work> | |
+ | |
+This enable transactions globally for all drivers until the next L<rollback> | |
+or L<commit> call on the class. | |
+ | |
+If begin_work is called while a transaction is still active (nested transaction) | |
+then the two transactions are merged. So inner transactions are ignored and | |
+a warning will be emitted. | |
+ | |
+=head2 C<Class-E<gt>rollback> | |
+ | |
+This rollbacks all the transactions since the last begin work, and exits | |
+from the active transaction state. | |
+ | |
+=head2 C<Class-E<gt>commit> | |
+ | |
+Commits the transactions, and exits from the active transaction state. | |
+ | |
+=head2 C<Class-E<gt>txn_debug> | |
+ | |
+Just return the value of the global flag and the current working drivers | |
+in a hashref. | |
+ | |
+=head2 C<Class-E<gt>txn_active> | |
+ | |
+Returns true if a transaction is already active. | |
+ | |
=head1 DIAGNOSTICS | |
=over 4 | |
Index: lib/Data/ObjectDriver.pm | |
=================================================================== | |
--- lib/Data/ObjectDriver.pm (.../tags/release-0.05) (revision 528) | |
+++ lib/Data/ObjectDriver.pm (.../trunk) (revision 528) | |
@@ -8,7 +8,7 @@ | |
use base qw( Class::Accessor::Fast ); | |
use Data::ObjectDriver::Iterator; | |
-__PACKAGE__->mk_accessors(qw( pk_generator )); | |
+__PACKAGE__->mk_accessors(qw( pk_generator txn_active )); | |
our $VERSION = '0.05'; | |
our $DEBUG = $ENV{DOD_DEBUG} || 0; | |
@@ -38,6 +38,7 @@ | |
my $driver = shift; | |
my %param = @_; | |
$driver->pk_generator($param{pk_generator}); | |
+ $driver->txn_active(0); | |
$driver; | |
} | |
@@ -56,6 +57,29 @@ | |
sub end_query { } | |
+sub begin_work { | |
+ my $driver = shift; | |
+ $driver->txn_active(1); | |
+ $driver->debug(sprintf("%14s", "BEGIN_WORK") . ": driver=$driver"); | |
+} | |
+ | |
+sub commit { | |
+ my $driver = shift; | |
+ _end_txn($driver, 'commit'); | |
+} | |
+ | |
+sub rollback { | |
+ my $driver = shift; | |
+ _end_txn($driver, 'rollback'); | |
+} | |
+ | |
+sub _end_txn { | |
+ my $driver = shift; | |
+ my $method = shift; | |
+ $driver->txn_active(0); | |
+ $driver->debug(sprintf("%14s", uc($method)) . ": driver=$driver"); | |
+} | |
+ | |
sub debug { | |
my $driver = shift; | |
return unless $DEBUG; | |
@@ -106,6 +130,7 @@ | |
} | |
sub cache_object { } | |
+sub uncache_object { } | |
1; | |
__END__ | |
@@ -396,6 +421,23 @@ | |
If set to a true value, the I<SELECT> statement generated will include a | |
I<FOR UPDATE> clause. | |
+=item * comment | |
+ | |
+A sql comment to watermark the SQL query. | |
+ | |
+=item * window_size | |
+ | |
+Used when requesting an iterator for the search method and selecting | |
+a large result set or a result set of unknown size. In such a case, | |
+no LIMIT clause is assigned, which can load all available objects into | |
+memory. Specifying C<window_size> will load objects in manageable chunks. | |
+This will also cause any caching driver to be bypassed for issuing | |
+the search itself. Objects are still placed into the cache upon load. | |
+ | |
+This attribute is ignored when the search method is invoked in an array | |
+context, or if a C<limit> attribute is also specified that is smaller than | |
+the C<window_size>. | |
+ | |
=back | |
=head2 Class->search(\@terms [, \%options ]) | |
@@ -571,6 +613,89 @@ | |
Then see the documentation for I<Data::ObjectDriver::Profiler> to see the | |
methods on that class. | |
+ | |
+=head1 TRANSACTIONS | |
+ | |
+ | |
+Transactions are supported by Data::ObjectDriver's default drivers. So each | |
+Driver is capable to deal with transactional state independently. Additionally | |
+<Data::ObjectDriver::BaseObject> class know how to turn transactions switch on | |
+for all objects. | |
+ | |
+In the case of a global transaction all drivers used during this time are put | |
+in a transactional state until the end of the transaction. | |
+ | |
+=head2 Example | |
+ | |
+ ## start a transaction | |
+ Data::ObjectDriver::BaseObject->begin_work; | |
+ | |
+ $recipe = Recipe->new; | |
+ $recipe->title('lasagnes'); | |
+ $recipe->save; | |
+ | |
+ my $ingredient = Ingredient->new; | |
+ $ingredient->recipe_id($recipe->recipe_id); | |
+ $ingredient->name("more layers"); | |
+ $ingredient->insert; | |
+ $ingredient->remove; | |
+ | |
+ if ($you_are_sure) { | |
+ Data::ObjectDriver::BaseObject->commit; | |
+ } | |
+ else { | |
+ ## erase all trace of the above | |
+ Data::ObjectDriver::BaseObject->rollback; | |
+ } | |
+ | |
+=head2 Driver implementation | |
+ | |
+Drivers have to implement the following methods: | |
+ | |
+=over 4 | |
+ | |
+=item * begin_work to initialize a transaction | |
+ | |
+=item * rollback | |
+ | |
+=item * commmit | |
+ | |
+=back | |
+ | |
+=head2 Nested transactions | |
+ | |
+Are not supported and will result in warnings and the inner transactions | |
+to be ignored. Be sure to B<end> each transaction and not to let et long | |
+running transaction open (i.e you should execute a rollback or commit for | |
+each open begin_work). | |
+ | |
+=head2 Transactions and DBI | |
+ | |
+In order to make transactions work properly you have to make sure that | |
+the C<$dbh> for each DBI drivers are shared among drivers using the same | |
+database (basically dsn). | |
+ | |
+One way of doing that is to define a get_dbh() subref in each DBI driver | |
+to return the same dbh if the dsn and attributes of the connection are | |
+identical. | |
+ | |
+The other way is to use the new configuration flag on the DBI driver that | |
+has been added specifically for this purpose: C<reuse_dbh>. | |
+ | |
+ ## example coming from the test suite | |
+ __PACKAGE__->install_properties({ | |
+ columns => [ 'recipe_id', 'partition_id', 'title' ], | |
+ datasource => 'recipes', | |
+ primary_key => 'recipe_id', | |
+ driver => Data::ObjectDriver::Driver::Cache::Cache->new( | |
+ cache => Cache::Memory->new, | |
+ fallback => Data::ObjectDriver::Driver::DBI->new( | |
+ dsn => 'dbi:SQLite:dbname=global.db', | |
+ reuse_dbh => 1, ## be sure that the corresponding dbh is shared | |
+ ), | |
+ ), | |
+ }); | |
+ | |
=head1 EXAMPLES | |
=head2 A Partitioned, Caching Driver | |
Index: Changes | |
=================================================================== | |
--- Changes (.../tags/release-0.05) (revision 528) | |
+++ Changes (.../trunk) (revision 528) | |
@@ -2,6 +2,31 @@ | |
Revision history for Data::ObjectDriver | |
+0.06 | |
+ - Added peek_next() method to ResultSet, q.v. | |
+ - Localized creation of D::OD::Iterator object. Thanks to Hirotaka Ogawa | |
+ for the patch. | |
+ - Fixed compilation error with Perl 5.10. Thanks to smpeters for the patch. | |
+ - Added a new $object->uncache_object as a mirror of cache_object(), which | |
+ purge one object from the cache layer, for the cases where you want a | |
+ manual control over it. | |
+ - Added a "distinct" method to D::OD::SQL that forces the DISTINCT keyword | |
+ in the generated SQL statement. Thanks to John Berthels for the patch. | |
+ - Added a "window_size" argument for the search() method of the caching | |
+ layer to constrain the number of objects loaded from the database for | |
+ large or unbounded searches. | |
+ - Added a "comment" argument to search parameter allowing the SQL | |
+ queries to be watermarked with SQL comments. | |
+ - Added a "object_is_stored" method on DOD objects, which returns true until | |
+ the object has been saved in the persistent store. | |
+ - Added a "pk_str" method on base objects has a nice shortcut for printing | |
+ the primary key of an object. | |
+ - Added a "reuse_dbh" option to D::OD::D::DBI, if enabled it caches and reuses | |
+ $dbh using the dsn as the key. | |
+ - Exposed the transaction mechanism built in the drivers at the object levels: | |
+ D::OD::BO->begin_work now starts a global transaction across all drivers | |
+ ending with a rollback or a commit on the same class. | |
+ | |
0.05 2008.02.24 | |
- Added a new Data::ObjectDriver::ResultSet abstraction for building | |
result sets with lazy-loading of the actual results. This allows for | |
Index: MANIFEST.SKIP | |
=================================================================== | |
--- MANIFEST.SKIP (.../tags/release-0.05) (revision 528) | |
+++ MANIFEST.SKIP (.../trunk) (revision 528) | |
@@ -12,3 +12,4 @@ | |
\.tar\.gz$ | |
\.svn | |
\.shipit$ | |
+t/9\d.+\.t$ | |
Index: README | |
=================================================================== | |
--- README (.../tags/release-0.05) (revision 528) | |
+++ README (.../trunk) (revision 528) | |
@@ -27,4 +27,4 @@ | |
% make install | |
-Six Apart / [email protected] | |
+Six Apart / [email protected] | |
\ No newline at end of file |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment