Really fix INSERT RETURNING - simply make it a flag on the storage and keep the machi...
[dbsrgits/DBIx-Class.git] / t / 72pg.t
index 5e6c467..ee6db29 100644 (file)
--- a/t/72pg.t
+++ b/t/72pg.t
@@ -19,20 +19,20 @@ Set \$ENV{DBICTEST_PG_DSN}, _USER and _PASS to run this test
 )
 EOM
 
-### load any test classes that are defined further down in the file
+### load any test classes that are defined further down in the file via BEGIN blocks
 
 our @test_classes; #< array that will be pushed into by test classes defined in this file
 DBICTest::Schema->load_classes( map {s/.+:://;$_} @test_classes ) if @test_classes;
 
 
-###  pre-connect tests
+###  pre-connect tests (keep each test separate as to make sure rebless() runs)
 {
   my $s = DBICTest::Schema->connect($dsn, $user, $pass);
 
   ok (!$s->storage->_dbh, 'definitely not connected');
 
   # Check that datetime_parser returns correctly before we explicitly connect.
- SKIP: {
+  SKIP: {
       eval { require DateTime::Format::Pg };
       skip "DateTime::Format::Pg required", 2 if $@;
 
@@ -43,16 +43,21 @@ DBICTest::Schema->load_classes( map {s/.+:://;$_} @test_classes ) if @test_class
       is( $parser, 'DateTime::Format::Pg', 'datetime_parser is as expected');
   }
 
-
+  ok (!$s->storage->_dbh, 'still not connected');
+}
+{
+  my $s = DBICTest::Schema->connect($dsn, $user, $pass);
   # make sure sqlt_type overrides work (::Storage::DBI::Pg does this)
+  ok (!$s->storage->_dbh, 'definitely not connected');
   is ($s->storage->sqlt_type, 'PostgreSQL', 'sqlt_type correct pre-connection');
+  ok (!$s->storage->_dbh, 'still not connected');
 }
 
 ### connect, create postgres-specific test schema
 
 my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
 
-drop_test_schema($schema, 'no warn');
+drop_test_schema($schema);
 create_test_schema($schema);
 
 ### begin main tests
@@ -63,6 +68,10 @@ create_test_schema($schema);
 run_apk_tests($schema); #< older set of auto-pk tests
 run_extended_apk_tests($schema); #< new extended set of auto-pk tests
 
+
+
+
+
 ### type_info tests
 
 my $test_type_info = {
@@ -117,9 +126,8 @@ BEGIN {
 
   use strict;
   use warnings;
-  use base 'DBIx::Class';
+  use base 'DBIx::Class::Core';
 
-  __PACKAGE__->load_components(qw/Core/);
   __PACKAGE__->table('dbic_t_schema.array_test');
   __PACKAGE__->add_columns(qw/id arrayfield/);
   __PACKAGE__->column_info_from_storage(1);
@@ -164,25 +172,14 @@ BEGIN {
 
   use strict;
   use warnings;
-  use base 'DBIx::Class';
+  use base 'DBIx::Class::Core';
 
-  __PACKAGE__->load_components(qw/Core/);
   __PACKAGE__->table('dbic_t_schema.casecheck');
-  __PACKAGE__->add_columns(qw/id name NAME uc_name storecolumn/);
+  __PACKAGE__->add_columns(qw/id name NAME uc_name/);
   __PACKAGE__->column_info_from_storage(1);
   __PACKAGE__->set_primary_key('id');
-
-  sub store_column {
-    my ($self, $name, $value) = @_;
-    $value = '#'.$value if($name eq "storecolumn");
-    $self->maybe::next::method($name, $value);
-  }
 }
 
-# store_column is called once for create() for non sequence columns
-ok(my $storecolumn = $schema->resultset('Casecheck')->create({'storecolumn' => 'a'}));
-is($storecolumn->storecolumn, '#a'); # was '##a'
-
 my $name_info = $schema->source('Casecheck')->column_info( 'name' );
 is( $name_info->{size}, 1, "Case sensitive matching info for 'name'" );
 
@@ -197,79 +194,61 @@ is( $uc_name_info->{size}, 3, "Case insensitive matching info for 'uc_name'" );
 
 ## Test SELECT ... FOR UPDATE
 
-my $HaveSysSigAction = eval "require Sys::SigAction" && !$@;
-if( $HaveSysSigAction ) {
-    Sys::SigAction->import( 'set_sig_handler' );
-}
 SKIP: {
-    skip "Sys::SigAction is not available", 3 unless $HaveSysSigAction;
-    # create a new schema
-    my $schema2 = DBICTest::Schema->connect($dsn, $user, $pass);
-    $schema2->source("Artist")->name("dbic_t_schema.artist");
-
-    $schema->txn_do( sub {
-        my $artist = $schema->resultset('Artist')->search(
-            {
-                artistid => 1
-            },
-            {
-                for => 'update'
-            }
-        )->first;
-        is($artist->artistid, 1, "select for update returns artistid = 1");
+    if(eval "require Sys::SigAction" && !$@) {
+        Sys::SigAction->import( 'set_sig_handler' );
+    }
+    else {
+      skip "Sys::SigAction is not available", 6;
+    }
 
-        my $artist_from_schema2;
-        my $error_ok = 0;
-        eval {
-            my $h = set_sig_handler( 'ALRM', sub { die "DBICTestTimeout" } );
-            alarm(2);
-            $artist_from_schema2 = $schema2->resultset('Artist')->find(1);
-            $artist_from_schema2->name('fooey');
-            $artist_from_schema2->update;
-            alarm(0);
-        };
-        if (my $e = $@) {
-            $error_ok = $e =~ /DBICTestTimeout/;
-        }
+    my ($timed_out, $artist2);
 
+    for my $t (
+      {
         # Make sure that an error was raised, and that the update failed
-        ok($error_ok, "update from second schema times out");
-        ok($artist_from_schema2->is_column_changed('name'), "'name' column is still dirty from second schema");
-    });
-}
-
-SKIP: {
-    skip "Sys::SigAction is not available", 3 unless $HaveSysSigAction;
-    # create a new schema
-    my $schema2 = DBICTest::Schema->connect($dsn, $user, $pass);
-    $schema2->source("Artist")->name("dbic_t_schema.artist");
-
-    $schema->txn_do( sub {
+        update_lock => 1,
+        test_sub => sub {
+          ok($timed_out, "update from second schema times out");
+          ok($artist2->is_column_changed('name'), "'name' column is still dirty from second schema");
+        },
+      },
+      {
+        # Make sure that an error was NOT raised, and that the update succeeded
+        update_lock => 0,
+        test_sub => sub {
+          ok(! $timed_out, "update from second schema DOES NOT timeout");
+          ok(! $artist2->is_column_changed('name'), "'name' column is NOT dirty from second schema");
+        },
+      },
+    ) {
+      # create a new schema
+      my $schema2 = DBICTest::Schema->connect($dsn, $user, $pass);
+      $schema2->source("Artist")->name("dbic_t_schema.artist");
+
+      $schema->txn_do( sub {
         my $artist = $schema->resultset('Artist')->search(
             {
                 artistid => 1
             },
+            $t->{update_lock} ? { for => 'update' } : {}
         )->first;
-        is($artist->artistid, 1, "select for update returns artistid = 1");
+        is($artist->artistid, 1, "select returns artistid = 1");
 
-        my $artist_from_schema2;
-        my $error_ok = 0;
+        $timed_out = 0;
         eval {
             my $h = set_sig_handler( 'ALRM', sub { die "DBICTestTimeout" } );
             alarm(2);
-            $artist_from_schema2 = $schema2->resultset('Artist')->find(1);
-            $artist_from_schema2->name('fooey');
-            $artist_from_schema2->update;
+            $artist2 = $schema2->resultset('Artist')->find(1);
+            $artist2->name('fooey');
+            $artist2->update;
             alarm(0);
         };
-        if (my $e = $@) {
-            $error_ok = $e =~ /DBICTestTimeout/;
-        }
+        $timed_out = $@ =~ /DBICTestTimeout/;
+      });
 
-        # Make sure that an error was NOT raised, and that the update succeeded
-        ok(! $error_ok, "update from second schema DOES NOT timeout");
-        ok(! $artist_from_schema2->is_column_changed('name'), "'name' column is NOT dirty from second schema");
-    });
+      $t->{test_sub}->();
+    }
 }
 
 
@@ -278,18 +257,52 @@ SKIP: {
 $schema->source("SequenceTest")->name("dbic_t_schema.sequence_test");
 for (1..5) {
     my $st = $schema->resultset('SequenceTest')->create({ name => 'foo' });
-    is($st->pkid1, $_, "Oracle Auto-PK without trigger: First primary key");
-    is($st->pkid2, $_ + 9, "Oracle Auto-PK without trigger: Second primary key");
-    is($st->nonpkid, $_ + 19, "Oracle Auto-PK without trigger: Non-primary key");
+    is($st->pkid1, $_, "Auto-PK for sequence without default: First primary key");
+    is($st->pkid2, $_ + 9, "Auto-PK for sequence without default: Second primary key");
+    is($st->nonpkid, $_ + 19, "Auto-PK for sequence without default: Non-primary key");
 }
 my $st = $schema->resultset('SequenceTest')->create({ name => 'foo', pkid1 => 55 });
-is($st->pkid1, 55, "Oracle Auto-PK without trigger: First primary key set manually");
+is($st->pkid1, 55, "Auto-PK for sequence without default: First primary key set manually");
+
+
+######## test non-serial auto-pk
+
+if ($schema->storage->can_insert_returning) {
+  $schema->source('TimestampPrimaryKey')->name('dbic_t_schema.timestamp_primary_key_test');
+  my $row = $schema->resultset('TimestampPrimaryKey')->create({});
+  ok $row->id;
+}
+
+######## test with_deferred_fk_checks
+
+$schema->source('CD')->name('dbic_t_schema.cd');
+$schema->source('Track')->name('dbic_t_schema.track');
+lives_ok {
+  $schema->storage->with_deferred_fk_checks(sub {
+    $schema->resultset('Track')->create({
+      trackid => 999, cd => 999, position => 1, title => 'deferred FK track'
+    });
+    $schema->resultset('CD')->create({
+      artist => 1, cdid => 999, year => '2003', title => 'deferred FK cd'
+    });
+  });
+} 'with_deferred_fk_checks code survived';
+
+is eval { $schema->resultset('Track')->find(999)->title }, 'deferred FK track',
+   'code in with_deferred_fk_checks worked'; 
+
+throws_ok {
+  $schema->resultset('Track')->create({
+    trackid => 1, cd => 9999, position => 1, title => 'Track1'
+  });
+} qr/constraint/i, 'with_deferred_fk_checks is off';
 
 done_testing;
 
 exit;
 
 END {
+    return unless $schema;
     drop_test_schema($schema);
     eapk_drop_all( $schema)
 };
@@ -316,6 +329,34 @@ EOS
 
       $dbh->do("CREATE SCHEMA dbic_t_schema");
       $dbh->do("CREATE TABLE dbic_t_schema.artist $std_artist_table");
+
+      $dbh->do(<<EOS);
+CREATE TABLE dbic_t_schema.timestamp_primary_key_test (
+  id timestamp default current_timestamp
+)
+EOS
+      $dbh->do(<<EOS);
+CREATE TABLE dbic_t_schema.cd (
+  cdid int PRIMARY KEY,
+  artist int,
+  title varchar(255),
+  year varchar(4),
+  genreid int,
+  single_track int
+)
+EOS
+      $dbh->do(<<EOS);
+CREATE TABLE dbic_t_schema.track (
+  trackid int,
+  cd int REFERENCES dbic_t_schema.cd(cdid) DEFERRABLE,
+  position int,
+  title varchar(255),
+  last_updated_on date,
+  last_updated_at date,
+  small_dt date
+)
+EOS
+
       $dbh->do(<<EOS);
 CREATE TABLE dbic_t_schema.sequence_test (
     pkid1 integer
@@ -334,7 +375,6 @@ CREATE TABLE dbic_t_schema.casecheck (
     , "name" VARCHAR(1)
     , "NAME" VARCHAR(2)
     , "UC_NAME" VARCHAR(3)
-    , "storecolumn" VARCHAR(10)
 )
 EOS
       $dbh->do(<<EOS);
@@ -379,7 +419,7 @@ EOS
 
 
 sub drop_test_schema {
-    my ( $schema, $no_warn ) = @_;
+    my ( $schema, $warn_exceptions ) = @_;
 
     $schema->storage->dbh_do(sub {
         my (undef,$dbh) = @_;
@@ -398,7 +438,7 @@ sub drop_test_schema {
                       'DROP SCHEMA dbic_t_schema_3 CASCADE',
                      ) {
             eval { $dbh->do ($stat) };
-            diag $@ if $@ && !$no_warn;
+            diag $@ if $@ && $warn_exceptions;
         }
     });
 }
@@ -467,48 +507,179 @@ sub apk_t_set {
 
 ######## EXTENDED AUTO-PK TESTS
 
+my @eapk_id_columns;
 BEGIN {
   package DBICTest::Schema::ExtAPK;
   push @main::test_classes, __PACKAGE__;
 
   use strict;
   use warnings;
-  use base 'DBIx::Class';
+  use base 'DBIx::Class::Core';
 
-  __PACKAGE__->load_components(qw/Core/);
-  __PACKAGE__->table('apk_t');
+  __PACKAGE__->table('apk');
 
+  @eapk_id_columns = qw( id1 id2 id3 id4 );
   __PACKAGE__->add_columns(
     map { $_ => { data_type => 'integer', is_auto_increment => 1 } }
-        qw( id1 id2 id3 id4 )
+       @eapk_id_columns
   );
 
-  __PACKAGE__->set_primary_key('id1');
+  __PACKAGE__->set_primary_key('id2'); #< note the SECOND column is
+                                       #the primary key
 }
 
-my @apk_schemas;
-BEGIN{ @apk_schemas = map "dbic_apk_$_", 0..5 }
+my @eapk_schemas;
+BEGIN{ @eapk_schemas = map "dbic_apk_$_", 0..5 }
+my %seqs; #< hash of schema.table.col => currval of its (DBIC) primary key sequence
 
 sub run_extended_apk_tests {
   my $schema = shift;
 
-  eapk_drop_all($schema,'no warn');
+  #save the search path and reset it at the end
+  my $search_path_save = eapk_get_search_path($schema);
+
+  eapk_drop_all($schema);
 
-  # make the test schemas
+  # make the test schemas and sequences
   $schema->storage->dbh_do(sub {
-    $_[1]->do("CREATE SCHEMA $_")
-        for @apk_schemas;
+    my ( undef, $dbh ) = @_;
+
+    $dbh->do("CREATE SCHEMA $_")
+        for @eapk_schemas;
+
+    $dbh->do("CREATE SEQUENCE $eapk_schemas[5].fooseq");
+    $dbh->do("SELECT setval('$eapk_schemas[5].fooseq',400)");
+    $seqs{"$eapk_schemas[1].apk.id2"} = 400;
+
+    $dbh->do("CREATE SEQUENCE $eapk_schemas[4].fooseq");
+    $dbh->do("SELECT setval('$eapk_schemas[4].fooseq',300)");
+    $seqs{"$eapk_schemas[3].apk.id2"} = 300;
+
+    $dbh->do("CREATE SEQUENCE $eapk_schemas[3].fooseq");
+    $dbh->do("SELECT setval('$eapk_schemas[3].fooseq',200)");
+    $seqs{"$eapk_schemas[4].apk.id2"} = 200;
+
+    $dbh->do("SET search_path = ".join ',', reverse @eapk_schemas );
   });
 
-  eapk_create($schema, with_search_path => [0,1]);
+  # clear our search_path cache
+  $schema->storage->{_pg_search_path} = undef;
+
+  eapk_create( $schema,
+               with_search_path => [0,1],
+             );
+  eapk_create( $schema,
+               with_search_path => [1,0,'public'],
+               nextval => "$eapk_schemas[5].fooseq",
+             );
+  eapk_create( $schema,
+               with_search_path => ['public',0,1],
+               qualify_table => 2,
+             );
+  eapk_create( $schema,
+               with_search_path => [3,1,0,'public'],
+               nextval => "$eapk_schemas[4].fooseq",
+             );
+  eapk_create( $schema,
+               with_search_path => [3,1,0,'public'],
+               nextval => "$eapk_schemas[3].fooseq",
+               qualify_table => 4,
+             );
+
+  eapk_poke( $schema );
+  eapk_poke( $schema, 0 );
+  eapk_poke( $schema, 2 );
+  eapk_poke( $schema, 4 );
+  eapk_poke( $schema, 1 );
+  eapk_poke( $schema, 0 );
+  eapk_poke( $schema, 1 );
+  eapk_poke( $schema );
+  eapk_poke( $schema, 4 );
+  eapk_poke( $schema, 3 );
+  eapk_poke( $schema, 1 );
+  eapk_poke( $schema, 2 );
+  eapk_poke( $schema, 0 );
+
+  # set our search path back
+  eapk_set_search_path( $schema, @$search_path_save );
+}
+
+# do a DBIC create on the apk table in the given schema number (which is an
+# index of @eapk_schemas)
+
+sub eapk_poke {
+  my ($s, $schema_num) = @_;
+
+  my $schema_name = defined $schema_num
+      ? $eapk_schemas[$schema_num]
+      : '';
 
-  #unqualified table, unqualified 
+  my $schema_name_actual = $schema_name || eapk_find_visible_schema($s);
+
+  $s->source('ExtAPK')->name($schema_name ? $schema_name.'.apk' : 'apk');
+  #< clear sequence name cache
+  $s->source('ExtAPK')->column_info($_)->{sequence} = undef
+      for @eapk_id_columns;
+
+  no warnings 'uninitialized';
   lives_ok {
-    $schema->resultset('ExtAPK')->create({});
-  } 'create in first schema does not die';
+    my $new;
+    for my $inc (1,2,3) {
+      $new = $schema->resultset('ExtAPK')->create({ id1 => 1});
+      my $proper_seqval = ++$seqs{"$schema_name_actual.apk.id2"};
+      is( $new->id2, $proper_seqval, "$schema_name_actual.apk.id2 correct inc $inc" )
+          or eapk_seq_diag($s,$schema_name);
+      $new->discard_changes;
+      is( $new->id1, 1 );
+      for my $id ('id3','id4') {
+        my $proper_seqval = ++$seqs{"$schema_name_actual.apk.$id"};
+        is( $new->$id, $proper_seqval, "$schema_name_actual.apk.$id correct inc $inc" )
+            or eapk_seq_diag($s,$schema_name);
+      }
+    }
+  } "create in schema '$schema_name' lives"
+      or eapk_seq_diag($s,$schema_name);
+}
 
+# print diagnostic info on which sequences were found in the ExtAPK
+# class
+sub eapk_seq_diag {
+    my $s = shift;
+    my $schema = shift || eapk_find_visible_schema($s);
+
+    diag "$schema.apk sequences: ",
+        join(', ',
+             map "$_:".($s->source('ExtAPK')->column_info($_)->{sequence} || '<none>'),
+             @eapk_id_columns
+            );
 }
 
+# get the postgres search path as an arrayref
+sub eapk_get_search_path {
+    my ( $s ) = @_;
+    # cache the search path as ['schema','schema',...] in the storage
+    # obj
+
+    return $s->storage->dbh_do(sub {
+        my (undef, $dbh) = @_;
+        my @search_path;
+        my ($sp_string) = $dbh->selectrow_array('SHOW search_path');
+        while ( $sp_string =~ s/("[^"]+"|[^,]+),?// ) {
+            unless( defined $1 and length $1 ) {
+                die "search path sanity check failed: '$1'";
+            }
+            push @search_path, $1;
+        }
+        \@search_path
+    });
+}
+sub eapk_set_search_path {
+    my ($s,@sp) = @_;
+    my $sp = join ',',@sp;
+    $s->storage->dbh_do( sub { $_[1]->do("SET search_path = $sp") } );
+}
+
+# create the apk table in the given schema, can set whether the table name is qualified, what the nextval is for the second ID
 sub eapk_create {
     my ($schema, %a) = @_;
 
@@ -519,17 +690,24 @@ sub eapk_create {
         if ( $a{with_search_path} ) {
             ($searchpath_save) = $dbh->selectrow_array('SHOW search_path');
 
-            my $search_path = join ',',@apk_schemas[@{$a{with_search_path}}];
+            my $search_path = join ',',map {/\D/ ? $_ : $eapk_schemas[$_]} @{$a{with_search_path}};
 
             $dbh->do("SET search_path = $search_path");
         }
 
-        my $schema = $a{qualify} ? "$a{qualify}." : '';
+        my $table_name = $a{qualify_table}
+            ? ($eapk_schemas[$a{qualify_table}] || die). ".apk"
+            : 'apk';
+        local $_[1]->{Warn} = 0;
+
+        my $id_def = $a{nextval}
+            ? "integer not null default nextval('$a{nextval}'::regclass)"
+            : 'serial';
         $dbh->do(<<EOS);
-CREATE TABLE apk_t (
-  id1 serial primary key
-  , id2 serial
-  , id3 serial
+CREATE TABLE $table_name (
+  id1 serial
+  , id2 $id_def
+  , id3 serial primary key
   , id4 serial
 )
 EOS
@@ -540,9 +718,8 @@ EOS
     });
 }
 
-
 sub eapk_drop_all {
-    my ( $schema, $no_warn ) = @_;
+    my ( $schema, $warn_exceptions ) = @_;
 
     $schema->storage->dbh_do(sub {
         my (undef,$dbh) = @_;
@@ -550,11 +727,27 @@ sub eapk_drop_all {
         local $dbh->{Warn} = 0;
 
         # drop the test schemas
-        for (@apk_schemas ) {
+        for (@eapk_schemas ) {
             eval{ $dbh->do("DROP SCHEMA $_ CASCADE") };
-            diag $@ if $@ && !$no_warn;
+            diag $@ if $@ && $warn_exceptions;
         }
 
 
     });
 }
+
+sub eapk_find_visible_schema {
+    my ($s) = @_;
+
+    my ($schema) =
+        $s->storage->dbh_do(sub {
+            $_[1]->selectrow_array(<<EOS);
+SELECT n.nspname
+FROM pg_catalog.pg_namespace n
+JOIN pg_catalog.pg_class c ON c.relnamespace = n.oid
+WHERE c.relname = 'apk'
+  AND pg_catalog.pg_table_is_visible(c.oid)
+EOS
+        });
+    return $schema;
+}