Merge 'pg_unqualified_schema' into 'trunk'
Peter Rabbitson [Tue, 18 Aug 2009 06:51:20 +0000 (06:51 +0000)]
r7248@Thesaurus (orig r7245):  rbuels | 2009-08-06 21:39:05 +0200
making topic branch for "currval undefined" problem when not qualifying tables with their schema names
r7249@Thesaurus (orig r7246):  rbuels | 2009-08-06 21:40:39 +0200
failing (crashing, really) test for this strange pg thing.  could not figure out a way to make a non-crashing test
r7250@Thesaurus (orig r7247):  rbuels | 2009-08-06 21:42:30 +0200
fix for pg non-schema-qualified thing, with a nice vague commit message.  performance should be the same as before, for the common (schema-qualified) case
r7251@Thesaurus (orig r7248):  rbuels | 2009-08-06 22:41:19 +0200
woops, pg search path fix needed support for quoted schema names in search paths
r7295@Thesaurus (orig r7292):  rbuels | 2009-08-10 20:45:50 +0200
added caching of pg search path in Pg storage object
r7296@Thesaurus (orig r7293):  rbuels | 2009-08-10 22:37:31 +0200
added test for empty table before non-schema-qualified pg sequence test in 72pg.t
r7299@Thesaurus (orig r7296):  rbuels | 2009-08-11 00:46:35 +0200
added blub to Changes for pg_unqualified_schema branch
r7300@Thesaurus (orig r7297):  rbuels | 2009-08-11 00:48:53 +0200
added me (rbuels) to contributors
r7328@Thesaurus (orig r7325):  rbuels | 2009-08-17 23:46:21 +0200
added POD section about schema support to DBIx::Class::Storage::Pg
r7329@Thesaurus (orig r7326):  rbuels | 2009-08-17 23:51:40 +0200
added more tests for multi-schema support in 72pg.t
r7334@Thesaurus (orig r7331):  ribasushi | 2009-08-18 08:49:03 +0200
Un-plan test and fix authorship

Changes
lib/DBIx/Class.pm
lib/DBIx/Class/Storage/DBI/Pg.pm
t/72pg.t

diff --git a/Changes b/Changes
index 8489383..36ae780 100644 (file)
--- a/Changes
+++ b/Changes
@@ -38,6 +38,8 @@ Revision history for DBIx::Class
         - Some fixes of multi-create corner cases
         - Multiple POD improvements
         - Added exception when resultset is called without an argument
+        - Improved support for non-schema-qualified tables under
+          Postgres (fixed last_insert_id sequence name auto-detection)
 
 0.08108 2009-07-05 23:15:00 (UTC)
         - Fixed the has_many prefetch with limit/group deficiency -
index 34ab70d..5c8d1ab 100644 (file)
@@ -312,6 +312,8 @@ quicksilver: Jules Bean
 
 rafl: Florian Ragwitz <rafl@debian.org>
 
+rbuels: Robert Buels <rmb32@cornell.edu>
+
 rdj: Ryan D Johnson <ryan@innerfence.com>
 
 ribasushi: Peter Rabbitson <rabbit+dbic@rabbit.us>
index a664808..9314396 100644 (file)
@@ -33,17 +33,54 @@ sub last_insert_id {
   $self->dbh_do('_dbh_last_insert_id', $seq);
 }
 
+sub _get_pg_search_path {
+    my ($self,$dbh) = @_;
+    # cache the search path as ['schema','schema',...] in the storage
+    # obj
+    $self->{_pg_search_path} ||= do {
+        my @search_path;
+        my ($sp_string) = $dbh->selectrow_array('SHOW search_path');
+        while( $sp_string =~ s/("[^"]+"|[^,]+),?// ) {
+            unless( defined $1 and length $1 ) {
+                $self->throw_exception("search path sanity check failed: '$1'")
+            }
+            push @search_path, $1;
+        }
+        \@search_path
+    };
+}
+
 sub _dbh_get_autoinc_seq {
   my ($self, $dbh, $schema, $table, @pri) = @_;
 
-  while (my $col = shift @pri) {
-    my $info = $dbh->column_info(undef,$schema,$table,$col)->fetchrow_hashref;
-    if(defined $info->{COLUMN_DEF} and
-       $info->{COLUMN_DEF} =~ /^nextval\(+'([^']+)'::(?:text|regclass)\)/) {
-      my $seq = $1;
-      # may need to strip quotes -- see if this works
-      return $seq =~ /\./ ? $seq : $info->{TABLE_SCHEM} . "." . $seq;
-    }
+  # get the list of postgres schemas to search.  if we have a schema
+  # specified, use that.  otherwise, use the search path
+  my @search_path;
+  if( defined $schema and length $schema ) {
+      @search_path = ( $schema );
+  } else {
+      @search_path = @{ $self->_get_pg_search_path($dbh) };
+  }
+
+  foreach my $search_schema (@search_path) {
+      foreach my $col (@pri) {
+          my $info = $dbh->column_info(undef,$search_schema,$table,$col)->fetchrow_hashref;
+          if($info) {
+              # if we get here, we have definitely found the right
+              # column.
+              if( defined $info->{COLUMN_DEF} and
+                  $info->{COLUMN_DEF}
+                    =~ /^nextval\(+'([^']+)'::(?:text|regclass)\)/i
+                ) {
+                  my $seq = $1;
+                  return $seq =~ /\./ ? $seq : $info->{TABLE_SCHEM} . "." . $seq;
+              } else {
+                  # we have found the column, but cannot figure out
+                  # the nextval seq
+                  return;
+              }
+          }
+      }
   }
   return;
 }
@@ -129,9 +166,26 @@ DBIx::Class::Storage::DBI::Pg - Automatic primary key class for PostgreSQL
 
 This class implements autoincrements for PostgreSQL.
 
+=head1 POSTGRESQL SCHEMA SUPPORT
+
+This supports multiple PostgreSQL schemas, with one caveat: for
+performance reasons, the schema search path is queried the first time it is
+needed and CACHED for subsequent uses.
+
+For this reason, you should do any necessary manipulation of the
+PostgreSQL search path BEFORE instantiating your schema object, or as
+part of the on_connect_do option to connect(), for example:
+
+   my $schema = My::Schema->connect
+                  ( $dsn,$user,$pass,
+                    { on_connect_do =>
+                        [ 'SET search_path TO myschema, foo, public' ],
+                    },
+                  );
+
 =head1 AUTHORS
 
-Marcus Ramberg <m.ramberg@cpan.org>
+See L<DBIx::Class/CONTRIBUTORS>
 
 =head1 LICENSE
 
index a5b5ac4..b53916b 100644 (file)
--- a/t/72pg.t
+++ b/t/72pg.t
@@ -1,5 +1,5 @@
 use strict;
-use warnings;  
+use warnings;
 
 use Test::More;
 use Test::Exception;
@@ -46,14 +46,11 @@ my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
 plan skip_all => 'Set $ENV{DBICTEST_PG_DSN}, _USER and _PASS to run this test '.
   '(note: This test drops and creates tables called \'artist\', \'casecheck\', \'array_test\' and \'sequence_test\''.
   ' as well as following sequences: \'pkid1_seq\', \'pkid2_seq\' and \'nonpkid_seq\''.
-  ' as well as following schemas: \'testschema\'!)'
+  ' as well as following schemas: \'testschema\',\'anothertestschema\'!)'
     unless ($dsn && $user);
 
-
-plan tests => 39;
-
 DBICTest::Schema->load_classes( 'Casecheck', 'ArrayTest' );
-my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
+my $schema = DBICTest::Schema->connect($dsn, $user, $pass,);
 
 # Check that datetime_parser returns correctly before we explicitly connect.
 SKIP: {
@@ -74,14 +71,28 @@ $schema->source("SequenceTest")->name("testschema.sequence_test");
     local $SIG{__WARN__} = sub {};
     _cleanup ($dbh);
 
+    my $artist_table_def = <<EOS;
+(
+  artistid serial PRIMARY KEY
+  , name VARCHAR(100)
+  , rank INTEGER NOT NULL DEFAULT '13'
+  , charfield CHAR(10)
+  , arrayfield INTEGER[]
+)
+EOS
     $dbh->do("CREATE SCHEMA testschema;");
-    $dbh->do("CREATE TABLE testschema.artist (artistid serial PRIMARY KEY, name VARCHAR(100), rank INTEGER NOT NULL DEFAULT '13', charfield CHAR(10), arrayfield INTEGER[]);");
+    $dbh->do("CREATE TABLE testschema.artist $artist_table_def;");
     $dbh->do("CREATE TABLE testschema.sequence_test (pkid1 integer, pkid2 integer, nonpkid integer, name VARCHAR(100), CONSTRAINT pk PRIMARY KEY(pkid1, pkid2));");
     $dbh->do("CREATE SEQUENCE pkid1_seq START 1 MAXVALUE 999999 MINVALUE 0");
     $dbh->do("CREATE SEQUENCE pkid2_seq START 10 MAXVALUE 999999 MINVALUE 0");
     $dbh->do("CREATE SEQUENCE nonpkid_seq START 20 MAXVALUE 999999 MINVALUE 0");
     ok ( $dbh->do('CREATE TABLE testschema.casecheck (id serial PRIMARY KEY, "name" VARCHAR(1), "NAME" VARCHAR(2), "UC_NAME" VARCHAR(3), "storecolumn" VARCHAR(10));'), 'Creation of casecheck table');
     ok ( $dbh->do('CREATE TABLE testschema.array_test (id serial PRIMARY KEY, arrayfield INTEGER[]);'), 'Creation of array_test table');
+    $dbh->do("CREATE SCHEMA anothertestschema;");
+    $dbh->do("CREATE TABLE anothertestschema.artist $artist_table_def;");
+    $dbh->do("CREATE SCHEMA yetanothertestschema;");
+    $dbh->do("CREATE TABLE yetanothertestschema.artist $artist_table_def;");
+    $dbh->do('set search_path=testschema,public');
 }
 
 # store_column is called once for create() for non sequence columns
@@ -94,13 +105,42 @@ is($storecolumn->storecolumn, '#a'); # was '##a'
 # This is in Core now, but it's here just to test that it doesn't break
 $schema->class('Artist')->load_components('PK::Auto');
 
+cmp_ok( $schema->resultset('Artist')->count, '==', 0, 'this should start with an empty artist table');
+
+{ # test that auto-pk also works with the defined search path by
+  # un-schema-qualifying the table name
+  my $artist_name_save = $schema->source("Artist")->name;
+  $schema->source("Artist")->name("artist");
+
+  my $unq_new;
+  lives_ok {
+      $unq_new = $schema->resultset('Artist')->create({ name => 'baz' });
+  } 'insert into unqualified, shadowed table succeeds';
+
+  is($unq_new && $unq_new->artistid, 1, "and got correct artistid");
+
+  #test with anothertestschema
+  $schema->source('Artist')->name('anothertestschema.artist');
+  my $another_new = $schema->resultset('Artist')->create({ name => 'ribasushi'});
+  is( $another_new->artistid,1, 'got correct artistid for yetanotherschema');
+
+  #test with yetanothertestschema
+  $schema->source('Artist')->name('yetanothertestschema.artist');
+  my $yetanother_new = $schema->resultset('Artist')->create({ name => 'ribasushi'});
+  is( $yetanother_new->artistid,1, 'got correct artistid for yetanotherschema');
+  is( $yetanother_new->artistid,1, 'got correct artistid for yetanotherschema');
+
+  $schema->source("Artist")->name($artist_name_save);
+}
+
 my $new = $schema->resultset('Artist')->create({ name => 'foo' });
 
-is($new->artistid, 1, "Auto-PK worked");
+is($new->artistid, 2, "Auto-PK worked");
 
 $new = $schema->resultset('Artist')->create({ name => 'bar' });
 
-is($new->artistid, 2, "Auto-PK worked");
+is($new->artistid, 3, "Auto-PK worked");
+
 
 my $test_type_info = {
     'artistid' => {
@@ -280,9 +320,15 @@ sub _cleanup {
     'DROP SEQUENCE pkid2_seq',
     'DROP SEQUENCE nonpkid_seq',
     'DROP SCHEMA testschema',
+    'DROP TABLE anothertestschema.artist',
+    'DROP SCHEMA anothertestschema',
+    'DROP TABLE yetanothertestschema.artist',
+    'DROP SCHEMA yetanothertestschema',
   ) {
     eval { $dbh->do ($stat) };
   }
 }
 
+done_testing;
+
 END { _cleanup($dbh) }