sub last_insert_id {
my ($self,$source,$col) = @_;
- my $seq = ($source->column_info($col)->{sequence} ||= $self->get_autoinc_seq($source,$col));
- $self->throw_exception("could not fetch primary key for " . $source->name . ", could not "
- . "get autoinc sequence for $col (check that table and column specifications are correct "
- . "and in the correct case)") unless defined $seq;
+ my $seq = ( $source->column_info($col)->{sequence} ||= $self->dbh_do('_dbh_get_autoinc_seq', $source, $col) )
+ or $self->throw_exception( "could not determine sequence for "
+ . $source->name
+ . ".$col, please consider adding a "
+ . "schema-qualified sequence to its column info"
+ );
$self->_dbh_last_insert_id ($self->_dbh, $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) = @_;
-
- # 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;
-}
-
-sub get_autoinc_seq {
- my ($self,$source,$col) = @_;
-
- my @pri = $source->primary_columns;
+ my ($self, $dbh, $source, $col) = @_;
my $schema;
my $table = $source->name;
- if (ref $table eq 'SCALAR') {
- $table = $$table;
+ # deref table name if it needs it
+ $table = $$table
+ if ref $table eq 'SCALAR';
+
+ # parse out schema name if present
+ if( $table =~ /^(.+)\.(.+)$/ ) {
+ ( $schema, $table ) = ( $1, $2 );
}
- elsif ($table =~ /^(.+)\.(.+)$/) {
- ($schema, $table) = ($1, $2);
+
+ # use DBD::Pg to fetch the column info if it is recent enough to
+ # work. otherwise, use custom SQL
+ my $seq_expr = $DBD::Pg::VERSION >= 2.015001
+ ? eval{ $dbh->column_info(undef,$schema,$table,$col)->fetchrow_hashref->{COLUMN_DEF} }
+ : $self->_dbh_get_column_default( $dbh, $schema, $table, $col );
+
+ # if no default value is set on the column, or if we can't parse the
+ # default value as a sequence, throw.
+ unless ( defined $seq_expr and $seq_expr =~ /^nextval\(+'([^']+)'::(?:text|regclass)\)/i ){
+ $seq_expr = '' unless defined $seq_expr;
+ $schema = "$schema." if defined $schema && length $schema;
+ $self->throw_exception( "no sequence found for $schema$table.$col, check table definition, "
+ . "or explicitly set the 'sequence' for this column in the "
+ . $source->source_name
+ . " class"
+ );
}
- $self->dbh_do('_dbh_get_autoinc_seq', $schema, $table, @pri);
+ return $1;
+}
+
+# custom method for fetching column default, since column_info has a
+# bug with older versions of DBD::Pg
+sub _dbh_get_column_default {
+ my ( $self, $dbh, $schema, $table, $col ) = @_;
+
+ # Build and execute a query into the pg_catalog to find the Pg
+ # expression for the default value for this column in this table.
+ # If the table name is schema-qualified, query using that specific
+ # schema name.
+
+ # Otherwise, find the table in the standard Postgres way, using the
+ # search path. This is done with the pg_catalog.pg_table_is_visible
+ # function, which returns true if a given table is 'visible',
+ # meaning the first table of that name to be found in the search
+ # path.
+
+ # I *think* we can be assured that this query will always find the
+ # correct column according to standard Postgres semantics.
+ #
+ # -- rbuels
+
+ my $sqlmaker = $self->sql_maker;
+ local $sqlmaker->{bindtype} = 'normal';
+
+ my ($where, @bind) = $sqlmaker->where ({
+ 'a.attnum' => {'>', 0},
+ 'c.relname' => $table,
+ 'a.attname' => $col,
+ -not_bool => 'a.attisdropped',
+ (defined $schema && length $schema)
+ ? ( 'n.nspname' => $schema )
+ : ( -bool => \'pg_catalog.pg_table_is_visible(c.oid)' )
+ });
+
+ my ($seq_expr) = $dbh->selectrow_array(<<EOS,undef,@bind);
+
+SELECT
+ (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid)
+ FROM pg_catalog.pg_attrdef d
+ WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef)
+FROM pg_catalog.pg_class c
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
+ JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
+$where
+
+EOS
+
+ return $seq_expr;
}
+
sub sqlt_type {
return 'PostgreSQL';
}
1;
+__END__
+
=head1 NAME
DBIx::Class::Storage::DBI::Pg - Automatic primary key class 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.
+This driver supports multiple PostgreSQL schemas, with one caveat: for
+performance reasons, data about the search path, sequence names, and
+so forth is queried as needed and CACHED for subsequent uses.
+
+For this reason, once your schema is instantiated, you should not
+change the PostgreSQL schema search path for that schema's database
+connection. If you do, Bad Things may happen.
-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:
+You should do any necessary manipulation of the 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,
use lib qw(t/lib);
use DBICTest;
-{
- package DBICTest::Schema::Casecheck;
-
- use strict;
- use warnings;
- use base 'DBIx::Class';
-
- __PACKAGE__->load_components(qw/Core/);
- __PACKAGE__->table('testschema.casecheck');
- __PACKAGE__->add_columns(qw/id name NAME uc_name storecolumn/);
- __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);
- }
-}
-
-{
- package DBICTest::Schema::ArrayTest;
-
- use strict;
- use warnings;
- use base 'DBIx::Class';
+my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
- __PACKAGE__->load_components(qw/Core/);
- __PACKAGE__->table('testschema.array_test');
- __PACKAGE__->add_columns(qw/id arrayfield/);
- __PACKAGE__->column_info_from_storage(1);
- __PACKAGE__->set_primary_key('id');
+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', '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
-my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_PG_${_}" } qw/DSN USER PASS/};
+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;
-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\',\'anothertestschema\'!)'
- unless ($dsn && $user);
-DBICTest::Schema->load_classes( 'Casecheck', 'ArrayTest' );
-
-# make sure sqlt_type overrides work (::Storage::DBI::Pg does this)
+### pre-connect tests (keep each test separate as to make sure rebless() runs)
{
- my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
+ my $s = DBICTest::Schema->connect($dsn, $user, $pass);
- ok (!$schema->storage->_dbh, 'definitely not connected');
- is ($schema->storage->sqlt_type, 'PostgreSQL', 'sqlt_type correct pre-connection');
-}
+ ok (!$s->storage->_dbh, 'definitely not connected');
-my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
-# Check that datetime_parser returns correctly before we explicitly connect.
-SKIP: {
- eval { require DateTime::Format::Pg };
- skip "DateTime::Format::Pg required", 2 if $@;
+ # Check that datetime_parser returns correctly before we explicitly connect.
+ SKIP: {
+ eval { require DateTime::Format::Pg };
+ skip "DateTime::Format::Pg required", 2 if $@;
- my $store = ref $schema->storage;
- is($store, 'DBIx::Class::Storage::DBI', 'Started with generic storage');
+ my $store = ref $s->storage;
+ is($store, 'DBIx::Class::Storage::DBI', 'Started with generic storage');
- my $parser = $schema->storage->datetime_parser;
- is( $parser, 'DateTime::Format::Pg', 'datetime_parser is as expected');
-}
+ my $parser = $s->storage->datetime_parser;
+ is( $parser, 'DateTime::Format::Pg', 'datetime_parser is as expected');
+ }
-my $dbh = $schema->storage->dbh;
-$schema->source("Artist")->name("testschema.artist");
-$schema->source("SequenceTest")->name("testschema.sequence_test");
+ ok (!$s->storage->_dbh, 'still not connected');
+}
{
- local $SIG{__WARN__} = sub {};
- _cleanup ($schema);
-
- 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 $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');
+ 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');
}
-# 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'
-
-
-# This is in Core now, but it's here just to test that it doesn't break
-$schema->class('Artist')->load_components('PK::Auto');
+### connect, create postgres-specific test schema
-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';
+my $schema = DBICTest::Schema->connect($dsn, $user, $pass);
- is($unq_new && $unq_new->artistid, 1, "and got correct artistid");
+drop_test_schema($schema);
+create_test_schema($schema);
- #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');
+### begin main tests
- #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);
-}
+# run a BIG bunch of tests for last-insert-id / Auto-PK / sequence
+# discovery
+run_apk_tests($schema); #< older set of auto-pk tests
+run_extended_apk_tests($schema); #< new extended set of auto-pk tests
-my $new = $schema->resultset('Artist')->create({ name => 'foo' });
-is($new->artistid, 2, "Auto-PK worked");
-$new = $schema->resultset('Artist')->create({ name => 'bar' });
-is($new->artistid, 3, "Auto-PK worked");
+### type_info tests
my $test_type_info = {
'artistid' => {
},
};
-
-my $type_info = $schema->storage->columns_info_for('testschema.artist');
+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)\)/,
is_deeply($type_info, $test_type_info,
'columns_info_for - column data types');
+
+
+
+####### Array tests
+
+BEGIN {
+ package DBICTest::Schema::ArrayTest;
+ push @main::test_classes, __PACKAGE__;
+
+ use strict;
+ use warnings;
+ use base 'DBIx::Class';
+
+ __PACKAGE__->load_components(qw/Core/);
+ __PACKAGE__->table('dbic_t_schema.array_test');
+ __PACKAGE__->add_columns(qw/id arrayfield/);
+ __PACKAGE__->column_info_from_storage(1);
+ __PACKAGE__->set_primary_key('id');
+
+}
SKIP: {
skip "Need DBD::Pg 2.9.2 or newer for array tests", 4 if $DBD::Pg::VERSION < 2.009002;
}
+
+########## Case check
+
+BEGIN {
+ package DBICTest::Schema::Casecheck;
+ push @main::test_classes, __PACKAGE__;
+
+ use strict;
+ use warnings;
+ use base 'DBIx::Class';
+
+ __PACKAGE__->load_components(qw/Core/);
+ __PACKAGE__->table('dbic_t_schema.casecheck');
+ __PACKAGE__->add_columns(qw/id name NAME uc_name/);
+ __PACKAGE__->column_info_from_storage(1);
+ __PACKAGE__->set_primary_key('id');
+}
+
my $name_info = $schema->source('Casecheck')->column_info( 'name' );
is( $name_info->{size}, 1, "Case sensitive matching info for 'name'" );
my $uc_name_info = $schema->source('Casecheck')->column_info( 'uc_name' );
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("testschema.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");
-
- 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/;
- }
- # 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");
- });
-}
+## Test SELECT ... FOR UPDATE
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("testschema.artist");
+ if(eval "require Sys::SigAction" && !$@) {
+ Sys::SigAction->import( 'set_sig_handler' );
+ }
+ else {
+ skip "Sys::SigAction is not available", 6;
+ }
+
+ my ($timed_out, $artist2);
- $schema->txn_do( sub {
+ for my $t (
+ {
+ # Make sure that an error was raised, and that the update failed
+ 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}->();
+ }
}
+
+######## other older Auto-pk tests
+
+$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");
my $st = $schema->resultset('SequenceTest')->create({ name => 'foo', pkid1 => 55 });
is($st->pkid1, 55, "Oracle Auto-PK without trigger: First primary key set manually");
-sub _cleanup {
- my $schema = shift or return;
- local $SIG{__WARN__} = sub {};
-
- for my $stat (
- 'DROP SCHEMA testschema CASCADE',
- 'DROP SCHEMA anothertestschema CASCADE',
- 'DROP SCHEMA yetanothertestschema CASCADE',
- 'DROP SEQUENCE pkid1_seq',
- 'DROP SEQUENCE pkid2_seq',
- 'DROP SEQUENCE nonpkid_seq',
- ) {
- eval { $schema->storage->_do_query ($stat) };
- }
+done_testing;
+
+exit;
+
+END {
+ drop_test_schema($schema);
+ eapk_drop_all( $schema)
+};
+
+
+######### SUBROUTINES
+
+sub create_test_schema {
+ my $schema = shift;
+ $schema->storage->dbh_do(sub {
+ my (undef,$dbh) = @_;
+
+ local $dbh->{Warn} = 0;
+
+ my $std_artist_table = <<EOS;
+(
+ artistid serial PRIMARY KEY
+ , name VARCHAR(100)
+ , rank INTEGER NOT NULL DEFAULT '13'
+ , charfield CHAR(10)
+ , arrayfield INTEGER[]
+)
+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.sequence_test (
+ pkid1 integer
+ , pkid2 integer
+ , nonpkid integer
+ , name VARCHAR(100)
+ , CONSTRAINT pk PRIMARY KEY(pkid1, pkid2)
+)
+EOS
+ $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");
+ $dbh->do(<<EOS);
+CREATE TABLE dbic_t_schema.casecheck (
+ id serial PRIMARY KEY
+ , "name" VARCHAR(1)
+ , "NAME" VARCHAR(2)
+ , "UC_NAME" VARCHAR(3)
+)
+EOS
+ $dbh->do(<<EOS);
+CREATE TABLE dbic_t_schema.array_test (
+ id serial PRIMARY KEY
+ , arrayfield INTEGER[]
+)
+EOS
+ $dbh->do("CREATE SCHEMA dbic_t_schema_2");
+ $dbh->do("CREATE TABLE dbic_t_schema_2.artist $std_artist_table");
+ $dbh->do("CREATE SCHEMA dbic_t_schema_3");
+ $dbh->do("CREATE TABLE dbic_t_schema_3.artist $std_artist_table");
+ $dbh->do('set search_path=dbic_t_schema,public');
+ $dbh->do("CREATE SCHEMA dbic_t_schema_4");
+ $dbh->do("CREATE SCHEMA dbic_t_schema_5");
+ $dbh->do(<<EOS);
+ CREATE TABLE dbic_t_schema_4.artist
+ (
+ artistid integer not null default nextval('artist_artistid_seq'::regclass) PRIMARY KEY
+ , name VARCHAR(100)
+ , rank INTEGER NOT NULL DEFAULT '13'
+ , charfield CHAR(10)
+ , arrayfield INTEGER[]
+ );
+EOS
+ $dbh->do('set search_path=public,dbic_t_schema,dbic_t_schema_3');
+ $dbh->do('create sequence public.artist_artistid_seq'); #< in the public schema
+ $dbh->do(<<EOS);
+ CREATE TABLE dbic_t_schema_5.artist
+ (
+ artistid integer not null default nextval('public.artist_artistid_seq'::regclass) PRIMARY KEY
+ , name VARCHAR(100)
+ , rank INTEGER NOT NULL DEFAULT '13'
+ , charfield CHAR(10)
+ , arrayfield INTEGER[]
+ );
+EOS
+ $dbh->do('set search_path=dbic_t_schema,public');
+ });
}
-done_testing;
-END { _cleanup($schema) }
+
+sub drop_test_schema {
+ my ( $schema, $warn_exceptions ) = @_;
+
+ $schema->storage->dbh_do(sub {
+ my (undef,$dbh) = @_;
+
+ local $dbh->{Warn} = 0;
+
+ for my $stat (
+ 'DROP SCHEMA dbic_t_schema_5 CASCADE',
+ 'DROP SEQUENCE public.artist_artistid_seq',
+ '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 SCHEMA dbic_t_schema_2 CASCADE',
+ 'DROP SCHEMA dbic_t_schema_3 CASCADE',
+ ) {
+ eval { $dbh->do ($stat) };
+ diag $@ if $@ && $warn_exceptions;
+ }
+ });
+}
+
+
+### auto-pk / last_insert_id / sequence discovery
+sub run_apk_tests {
+ my $schema = shift;
+
+ # 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
+ apk_t_set($schema,'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");
+
+ my @test_schemas = ( [qw| dbic_t_schema_2 1 |],
+ [qw| dbic_t_schema_3 1 |],
+ [qw| dbic_t_schema_4 2 |],
+ [qw| dbic_t_schema_5 1 |],
+ );
+ foreach my $t ( @test_schemas ) {
+ my ($sch_name, $start_num) = @$t;
+ #test with dbic_t_schema_2
+ apk_t_set($schema,"$sch_name.artist");
+ my $another_new;
+ lives_ok {
+ $another_new = $schema->resultset('Artist')->create({ name => 'Tollbooth Willy'});
+ is( $another_new->artistid,$start_num, "got correct artistid for $sch_name")
+ or diag "USED SEQUENCE: ".($schema->source('Artist')->column_info('artistid')->{sequence} || '<none>');
+ } "$sch_name liid 1 did not die"
+ or diag "USED SEQUENCE: ".($schema->source('Artist')->column_info('artistid')->{sequence} || '<none>');
+ lives_ok {
+ $another_new = $schema->resultset('Artist')->create({ name => 'Adam Sandler'});
+ is( $another_new->artistid,$start_num+1, "got correct artistid for $sch_name")
+ or diag "USED SEQUENCE: ".($schema->source('Artist')->column_info('artistid')->{sequence} || '<none>');
+ } "$sch_name liid 2 did not die"
+ or diag "USED SEQUENCE: ".($schema->source('Artist')->column_info('artistid')->{sequence} || '<none>');
+
+ }
+
+ lives_ok {
+ apk_t_set($schema,'dbic_t_schema.artist');
+ my $new = $schema->resultset('Artist')->create({ name => 'foo' });
+ is($new->artistid, 4, "Auto-PK worked");
+ $new = $schema->resultset('Artist')->create({ name => 'bar' });
+ is($new->artistid, 5, "Auto-PK worked");
+ } 'old auto-pk tests did not die either';
+}
+
+# sets the artist table name and clears sequence name cache
+sub apk_t_set {
+ my ( $s, $n ) = @_;
+ $s->source("Artist")->name($n);
+ $s->source('Artist')->column_info('artistid')->{sequence} = undef; #< clear sequence name cache
+}
+
+
+######## 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';
+
+ __PACKAGE__->load_components(qw/Core/);
+ __PACKAGE__->table('apk');
+
+ @eapk_id_columns = qw( id1 id2 id3 id4 );
+ __PACKAGE__->add_columns(
+ map { $_ => { data_type => 'integer', is_auto_increment => 1 } }
+ @eapk_id_columns
+ );
+
+ __PACKAGE__->set_primary_key('id2'); #< note the SECOND column is
+ #the primary key
+}
+
+my @eapk_schemas;
+BEGIN{ @eapk_schemas = map "dbic_apk_$_", 0..5 }
+
+sub run_extended_apk_tests {
+ my $schema = shift;
+
+ #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 and sequences
+ $schema->storage->dbh_do(sub {
+ my ( undef, $dbh ) = @_;
+
+ $dbh->do("CREATE SCHEMA $_")
+ for @eapk_schemas;
+
+ $dbh->do("CREATE SEQUENCE $eapk_schemas[5].fooseq");
+ $dbh->do("CREATE SEQUENCE $eapk_schemas[4].fooseq");
+ $dbh->do("CREATE SEQUENCE $eapk_schemas[3].fooseq");
+
+ $dbh->do("SET search_path = ".join ',', @eapk_schemas );
+ });
+
+ # 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, 0 );
+ eapk_poke( $schema, 2 );
+ eapk_poke( $schema, 4 );
+ eapk_poke( $schema, 1 );
+ eapk_poke( $schema, 0 );
+ eapk_poke( $schema, 1 );
+ 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)
+
+my %seqs; #< sanity-check hash of schema.table.col => currval of its sequence
+
+sub eapk_poke {
+ my ($s, $schema_num) = @_;
+
+ my $schema_name = defined $schema_num
+ ? $eapk_schemas[$schema_num]
+ : '';
+
+ my $schema_name_actual = $schema_name || eapk_get_search_path($s)->[0];
+
+ $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 {
+ my $new;
+ for my $inc (1,2,3) {
+ $new = $schema->resultset('ExtAPK')->create({});
+ 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;
+ for my $id (grep $_ ne 'id2', @eapk_id_columns) {
+ 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_get_search_path($s)->[0];
+
+ 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) = @_;
+
+ $schema->storage->dbh_do(sub {
+ my (undef,$dbh) = @_;
+
+ my $searchpath_save;
+ if ( $a{with_search_path} ) {
+ ($searchpath_save) = $dbh->selectrow_array('SHOW search_path');
+
+ my $search_path = join ',',map {/\D/ ? $_ : $eapk_schemas[$_]} @{$a{with_search_path}};
+
+ $dbh->do("SET search_path = $search_path");
+ }
+
+ my $table_name = $a{qualify_table}
+ ? ($eapk_schemas[$a{qualify_table}] || die). ".apk"
+ : 'apk';
+ local $_[1]->{Warn} = 0;
+
+ my $id_def = $a{nextval}
+ ? "integer primary key not null default nextval('$a{nextval}'::regclass)"
+ : 'serial primary key';
+ $dbh->do(<<EOS);
+CREATE TABLE $table_name (
+ id1 serial
+ , id2 $id_def
+ , id3 serial
+ , id4 serial
+)
+EOS
+
+ if( $searchpath_save ) {
+ $dbh->do("SET search_path = $searchpath_save");
+ }
+ });
+}
+
+sub eapk_drop_all {
+ my ( $schema, $warn_exceptions ) = @_;
+
+ $schema->storage->dbh_do(sub {
+ my (undef,$dbh) = @_;
+
+ local $dbh->{Warn} = 0;
+
+ # drop the test schemas
+ for (@eapk_schemas ) {
+ eval{ $dbh->do("DROP SCHEMA $_ CASCADE") };
+ diag $@ if $@ && $warn_exceptions;
+ }
+
+
+ });
+}