Some test suite corrections ahead of next commits
[dbsrgits/DBIx-Class.git] / t / 72pg.t
index 542b915..9d37930 100644 (file)
--- a/t/72pg.t
+++ b/t/72pg.t
+BEGIN { do "./t/lib/ANFANG.pm" or die ( $@ || $! ) }
+use DBIx::Class::Optional::Dependencies -skip_all_without => 'test_rdbms_pg';
+
 use strict;
 use warnings;
 
 use Test::More;
 use Test::Exception;
-use lib qw(t/lib);
+use Test::Warn;
+use Config;
 use DBICTest;
-
+use SQL::Abstract 'is_literal_value';
+use DBIx::Class::_Util qw( is_exception set_subname );
 
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
 
-plan skip_all => <<EOM unless $dsn && $user;
-Set \$ENV{DBICTEST_PG_DSN}, _USER and _PASS to run this test
-( NOTE: This test drops and creates tables called 'artist', 'cd',
-'timestamp_primary_key_test', 'track', 'casecheck', 'array_test' and
-'sequence_test' as well as following sequences: 'pkid1_seq', 'pkid2_seq' and
-'nonpkid_seq''. as well as following schemas: 'dbic_t_schema',
-'dbic_t_schema_2', 'dbic_t_schema_3', 'dbic_t_schema_4', and 'dbic_t_schema_5')
-EOM
-
 ### 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 (keep each test separate as to make sure rebless() runs)
+  {
+    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');
+  }
+
+# test LIMIT support
+{
+  my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
+  drop_test_schema($schema);
+  create_test_schema($schema);
+  for (1..6) {
+    $schema->resultset('Artist')->create({ name => 'Artist ' . $_ });
+  }
+  my $it = $schema->resultset('Artist')->search( {},
+    { rows => 3,
+      offset => 2,
+      order_by => 'artistid' }
+  );
+  is( $it->count, 3, "LIMIT count ok" );  # ask for 3 rows out of 6 artists
+  is( $it->next->name, "Artist 3", "iterator->next ok" );
+  $it->next;
+  $it->next;
+  $it->next;
+  is( $it->next, undef, "next past end of resultset ok" );
+
+  # Limit with select-lock
+  lives_ok {
+    $schema->txn_do (sub {
+      isa_ok (
+        $schema->resultset('Artist')->find({artistid => 1}, {for => 'update', rows => 1}),
+        'DBICTest::Schema::Artist',
+      );
+    });
+  } 'Limited FOR UPDATE select works';
+}
+
+# check if we indeed do support stuff
 my $test_server_supports_insert_returning = do {
-  my $s = DBICTest::Schema->connect($dsn, $user, $pass);
-  $s->storage->_determine_driver;
-  $s->storage->_supports_insert_returning;
+
+  my $si = DBICTest::Schema->connect($dsn, $user, $pass)->storage->_server_info;
+  die "Unparseable Pg server version: $si->{dbms_version}\n"
+    unless $si->{normalized_dbms_version};
+
+  $si->{normalized_dbms_version} < 8.002 ? 0 : 1;
 };
+is (
+  DBICTest::Schema->connect($dsn, $user, $pass)->storage->_use_insert_returning,
+  $test_server_supports_insert_returning,
+  'insert returning capability guessed correctly'
+);
 
 my $schema;
-
 for my $use_insert_returning ($test_server_supports_insert_returning
   ? (0,1)
   : (0)
 ) {
-  no warnings qw/redefine once/;
-  local *DBIx::Class::Storage::DBI::Pg::_supports_insert_returning = sub {
-    $use_insert_returning
-  };
-
-###  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: {
-        eval { require DateTime::Format::Pg };
-        skip "DateTime::Format::Pg required", 2 if $@;
+  # doing it here instead of the actual class to keep the main thing under dfs
+  # and thus keep catching false positives (so far none, but one never knows)
+  mro::set_mro("DBICTest::Schema", "c3");
 
-        my $store = ref $s->storage;
-        is($store, 'DBIx::Class::Storage::DBI', 'Started with generic storage');
+  my $old_connection = DBICTest::Schema->can('connection');
 
-        my $parser = $s->storage->datetime_parser;
-        is( $parser, 'DateTime::Format::Pg', 'datetime_parser is as expected');
-    }
+  no warnings qw/once redefine/;
+  local *DBICTest::Schema::connection = set_subname 'DBICTest::Schema::connection' => sub {
+    my $s = shift->$old_connection(@_);
+    $s->storage->_use_insert_returning ($use_insert_returning);
+    $s;
+  };
 
-    ok (!$s->storage->_dbh, 'still not connected');
-  }
+### test capability override
   {
     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');
+
+    ok (
+      ! ($s->storage->_use_insert_returning xor $use_insert_returning),
+      'insert returning capability set correctly',
+    );
+    ok (!$s->storage->_dbh, 'still not connected (capability override works)');
   }
 
 ### connect, create postgres-specific test schema
@@ -83,6 +120,16 @@ for my $use_insert_returning ($test_server_supports_insert_returning
   run_apk_tests($schema); #< older set of auto-pk tests
   run_extended_apk_tests($schema); #< new extended set of auto-pk tests
 
+
+######## test the pg-specific syntax from https://rt.cpan.org/Ticket/Display.html?id=99503
+  lives_ok {
+    is(
+      $schema->resultset('Artist')->search({ artistid => { -in => \ '(select 4) union (select 5)' } })->count,
+      2,
+      'Two expected artists found on subselect union within IN',
+    );
+  };
+
 ### type_info tests
 
   my $test_type_info = {
@@ -120,14 +167,19 @@ for my $use_insert_returning ($test_server_supports_insert_returning
 
   my $type_info = $schema->storage->columns_info_for('dbic_t_schema.artist');
   my $artistid_defval = delete $type_info->{artistid}->{default_value};
-  like($artistid_defval,
-       qr/^nextval\('([^\.]*\.){0,1}artist_artistid_seq'::(?:text|regclass)\)/,
-       'columns_info_for - sequence matches Pg get_autoinc_seq expectations');
-  is_deeply($type_info, $test_type_info,
-            'columns_info_for - column data types');
-
 
+  # The curor info is too radically different from what is in the column_info
+  # call - just punt it (DBD::SQLite tests the codepath plenty enough)
+  unless (DBIx::Class::_ENV_::STRESSTEST_COLUMN_INFO_UNAWARE_STORAGE) {
+    like(
+      $artistid_defval,
+      qr/^nextval\('([^\.]*\.){0,1}artist_artistid_seq'::(?:text|regclass)\)/,
+      'columns_info_for - sequence matches Pg get_autoinc_seq expectations'
+    );
 
+    is_deeply($type_info, $test_type_info,
+            'columns_info_for - column data types');
+  }
 
 ####### Array tests
 
@@ -137,7 +189,7 @@ for my $use_insert_returning ($test_server_supports_insert_returning
 
     use strict;
     use warnings;
-    use base 'DBIx::Class::Core';
+    use base 'DBICTest::BaseResult';
 
     __PACKAGE__->table('dbic_t_schema.array_test');
     __PACKAGE__->add_columns(qw/id arrayfield/);
@@ -148,32 +200,141 @@ for my $use_insert_returning ($test_server_supports_insert_returning
   SKIP: {
     skip "Need DBD::Pg 2.9.2 or newer for array tests", 4 if $DBD::Pg::VERSION < 2.009002;
 
+    my $arr_rs = $schema->resultset('ArrayTest');
+
     lives_ok {
-      $schema->resultset('ArrayTest')->create({
+      $arr_rs->create({
         arrayfield => [1, 2],
       });
     } 'inserting arrayref as pg array data';
 
     lives_ok {
-      $schema->resultset('ArrayTest')->update({
+      $arr_rs->update({
         arrayfield => [3, 4],
       });
     } 'updating arrayref as pg array data';
 
-    $schema->resultset('ArrayTest')->create({
+    $arr_rs->create({
       arrayfield => [5, 6],
     });
 
-    my $count;
     lives_ok {
-      $count = $schema->resultset('ArrayTest')->search({
-        arrayfield => \[ '= ?' => [arrayfield => [3, 4]] ],   #Todo anything less ugly than this?
-      })->count;
-    } 'comparing arrayref to pg array data does not blow up';
-    is($count, 1, 'comparing arrayref to pg array data gives correct result');
-  }
+      $schema->populate('ArrayTest', [
+        [ qw/arrayfield/ ],
+        [ [0,0]          ],
+      ]);
+    } 'inserting arrayref using void ctx populate';
+
+    # Search using arrays
+    lives_ok {
+      is_deeply (
+        $arr_rs->search({ arrayfield => { -value => [3,4] } })->first->arrayfield,
+        [3,4],
+        'Array value matches'
+      );
+    } 'searching by arrayref';
+
+    lives_ok {
+      is_deeply (
+        $arr_rs->search({ arrayfield => { '=' => { -value => [3,4] }} })->first->arrayfield,
+        [3,4],
+        'Array value matches explicit equal'
+      );
+    } 'searching by arrayref (explicit equal sign)';
+
+    lives_ok {
+      is_deeply (
+        $arr_rs->search({ arrayfield => { '>' => { -value => [3,1] }} })->first->arrayfield,
+        [3,4],
+        'Array value matches greater than'
+      );
+    } 'searching by arrayref (greater than)';
+
+    lives_ok {
+      is (
+        $arr_rs->search({ arrayfield => { '>' => { -value => [3,7] }} })->count,
+        1,
+        'Greater than search found [5,6]',
+      );
+    } 'searching by arrayref (greater than)';
+
+    # Find using arrays
+    lives_ok {
+      is_deeply (
+        $arr_rs->find({ arrayfield => { -value => [3,4] } })->arrayfield,
+        [3,4],
+        'Array value matches implicit equal'
+      );
+    } 'find by arrayref';
+
+    lives_ok {
+      is_deeply (
+        $arr_rs->find({ arrayfield => { '=' => { -value => [3,4] }} })->arrayfield,
+        [3,4],
+        'Array value matches explicit equal'
+      );
+    } 'find by arrayref (equal)';
+
+    # test inferred condition for creation
+    for my $cond (
+      { -value => [3,4] },
+      \[ '= ?' => [3, 4] ],
+    ) {
+      local $TODO = 'No introspection of complex literal conditions :('
+        if is_literal_value $cond;
+
+
+      my $arr_rs_cond = $arr_rs->search({ arrayfield => $cond });
+
+      my $row = $arr_rs_cond->create({});
+      is_deeply ($row->arrayfield, [3,4], 'Array value taken from $rs condition');
+      $row->discard_changes;
+      is_deeply ($row->arrayfield, [3,4], 'Array value made it to storage');
+    }
 
+    my $arr = [ 1..10 ];
+    # exercise the creation-logic even more (akin to t/100populate.t)
+    for my $insert_value (
+      $arr,
+      { -value => $arr },
+      \[ '?', $arr ],
+    ) {
+      $arr_rs->delete;
+
+      my @objs = (
+        $arr_rs->create({ arrayfield => $insert_value }),
+        $arr_rs->populate([ { arrayfield => $insert_value } ]),
+        $arr_rs->populate([ ['arrayfield'], [ $insert_value ] ]),
+      );
+
+      my $loose_obj = $arr_rs->new({ arrayfield => $insert_value });
+
+      unless (is_literal_value $insert_value) {
+        is_deeply( $_->arrayfield, $arr, 'array value preserved during set_columns' )
+          for ($loose_obj, @objs)
+      }
 
+      push @objs, $loose_obj->insert;
+
+      $_->discard_changes for @objs;
+      is_deeply( $_->arrayfield, $arr, 'array value correct after discard_changes' )
+        for (@objs);
+
+      # insert couple more in void ctx
+      $arr_rs->populate([ { arrayfield => $insert_value } ]);
+      $arr_rs->populate([ ['arrayfield'], [ $insert_value ] ]);
+
+      # should have a total of 6 now, all pristine
+      my @retrieved_objs = $arr_rs->search({
+        arrayfield => ref $insert_value eq 'ARRAY'
+          ? { -value => $insert_value }
+          : { '=' => $insert_value }
+      })->all;
+      is scalar @retrieved_objs, 6, 'Correct count of inserted rows';
+      is_deeply( $_->arrayfield, $arr, 'array value correct after storage retrieval' )
+        for (@retrieved_objs);
+    }
+  }
 
 ########## Case check
 
@@ -201,17 +362,17 @@ for my $use_insert_returning ($test_server_supports_insert_returning
   is( $uc_name_info->{size}, 3, "Case insensitive matching info for 'uc_name'" );
 
 
-
+## Test ResultSet->update
+my $artist = $schema->resultset('Artist')->first;
+my $cds = $artist->cds_unordered->search({
+    year => { '!=' => 2010 }
+}, { prefetch => 'liner_notes' });
+lives_ok { $cds->update({ year => '2010' }) } 'Update on prefetched rs';
 
 ## Test SELECT ... FOR UPDATE
-
   SKIP: {
-      if(eval "require Sys::SigAction" && !$@) {
-          Sys::SigAction->import( 'set_sig_handler' );
-      }
-      else {
-        skip "Sys::SigAction is not available", 6;
-      }
+      skip "Your system does not support unsafe signals (d_sigaction) - unable to run deadlock test", 1
+        unless eval { $Config{d_sigaction} and require POSIX };
 
       my ($timed_out, $artist2);
 
@@ -238,24 +399,46 @@ for my $use_insert_returning ($test_server_supports_insert_returning
         $schema2->source("Artist")->name("dbic_t_schema.artist");
 
         $schema->txn_do( sub {
-          my $artist = $schema->resultset('Artist')->search(
+          my $rs = $schema->resultset('Artist')->search(
               {
                   artistid => 1
               },
               $t->{update_lock} ? { for => 'update' } : {}
-          )->first;
+          );
+          ok ($rs->count, 'Count works');
+
+          my $artist = $rs->next;
           is($artist->artistid, 1, "select returns artistid = 1");
 
           $timed_out = 0;
+
           eval {
-              my $h = set_sig_handler( 'ALRM', sub { die "DBICTestTimeout" } );
-              alarm(2);
+              # can not use %SIG assignment directly - we need sigaction below
+              # localization to a block still works however
+              local $SIG{ALRM};
+
+              POSIX::sigaction( POSIX::SIGALRM() => POSIX::SigAction->new(
+                sub { die "DBICTestTimeout" },
+              ));
+
               $artist2 = $schema2->resultset('Artist')->find(1);
               $artist2->name('fooey');
+
+              # FIXME - this needs to go away in lieu of a non-retrying runner
+              # ( i.e. after solving RT#47005 )
+              local *DBIx::Class::Storage::DBI::_ping = sub { 1 }, DBIx::Class::_ENV_::OLD_MRO && Class::C3->reinitialize()
+                if DBIx::Class::_Util::modver_gt_or_eq( 'DBD::Pg' => '3.5.0' );
+
+              alarm(1);
               $artist2->update;
-              alarm(0);
           };
-          $timed_out = $@ =~ /DBICTestTimeout/;
+
+          alarm(0);
+
+          if (is_exception($@)) {
+            $timed_out = $@ =~ /DBICTestTimeout/
+              or die $@;
+          }
         });
 
         $t->{test_sub}->();
@@ -278,7 +461,7 @@ for my $use_insert_returning ($test_server_supports_insert_returning
 
 ######## test non-serial auto-pk
 
-  if ($schema->storage->_supports_insert_returning) {
+  if ($schema->storage->_use_insert_returning) {
     $schema->source('TimestampPrimaryKey')->name('dbic_t_schema.timestamp_primary_key_test');
     my $row = $schema->resultset('TimestampPrimaryKey')->create({});
     ok $row->id;
@@ -300,13 +483,30 @@ for my $use_insert_returning ($test_server_supports_insert_returning
   } 'with_deferred_fk_checks code survived';
 
   is eval { $schema->resultset('Track')->find(999)->title }, 'deferred FK track',
-     'code in with_deferred_fk_checks worked'; 
+     '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';
+  } qr/violates foreign key constraint/i, 'with_deferred_fk_checks is off outside of TXN';
+
+  # rerun the same under with_deferred_fk_checks
+  # it is expected to fail, hence the eval
+  # but it also should not warn
+  warnings_like {
+    eval {
+      $schema->storage->with_deferred_fk_checks(sub {
+        $schema->resultset('Track')->create({
+          trackid => 1, cd => 9999, position => 1, title => 'Track1'
+        });
+      } )
+    };
+
+    like $@, qr/violates foreign key constraint/i,
+      "Still expected exception on deferred failure at commit time";
+
+  } [], 'No warnings on deferred rollback';
 }
 
 done_testing;
@@ -314,7 +514,8 @@ done_testing;
 END {
     return unless $schema;
     drop_test_schema($schema);
-    eapk_drop_all( $schema)
+    eapk_drop_all($schema);
+    undef $schema;
 };
 
 
@@ -362,8 +563,7 @@ CREATE TABLE dbic_t_schema.track (
   position int,
   title varchar(255),
   last_updated_on date,
-  last_updated_at date,
-  small_dt date
+  last_updated_at date
 )
 EOS
 
@@ -438,12 +638,12 @@ sub drop_test_schema {
 
         for my $stat (
                       'DROP SCHEMA dbic_t_schema_5 CASCADE',
-                      'DROP SEQUENCE public.artist_artistid_seq',
+                      'DROP SEQUENCE public.artist_artistid_seq CASCADE',
                       'DROP SCHEMA dbic_t_schema_4 CASCADE',
                       'DROP SCHEMA dbic_t_schema CASCADE',
-                      'DROP SEQUENCE pkid1_seq',
-                      'DROP SEQUENCE pkid2_seq',
-                      'DROP SEQUENCE nonpkid_seq',
+                      'DROP SEQUENCE pkid1_seq CASCADE',
+                      'DROP SEQUENCE pkid2_seq CASCADE',
+                      'DROP SEQUENCE nonpkid_seq CASCADE',
                       'DROP SCHEMA dbic_t_schema_2 CASCADE',
                       'DROP SCHEMA dbic_t_schema_3 CASCADE',
                      ) {