Support identity columns in PostgreSQL v10
[dbsrgits/DBIx-Class-Schema-Loader.git] / t / 10_03pg_common.t
1 use DBIx::Class::Schema::Loader::Optional::Dependencies
2     -skip_all_without => 'test_rdbms_pg';
3
4 use strict;
5 use warnings;
6 use utf8;
7 use DBIx::Class::Schema::Loader 'make_schema_at';
8 use DBIx::Class::Schema::Loader::Utils qw/no_warnings slurp_file/;
9 use Test::More;
10 use Test::Exception;
11 use Try::Tiny;
12 use File::Path 'rmtree';
13 use namespace::clean;
14
15 use lib qw(t/lib);
16 use dbixcsl_common_tests ();
17 use dbixcsl_test_dir '$tdir';
18
19 use constant EXTRA_DUMP_DIR => "$tdir/pg_extra_dump";
20
21 my $dsn      = $ENV{DBICTEST_PG_DSN} || '';
22 my $user     = $ENV{DBICTEST_PG_USER} || '';
23 my $password = $ENV{DBICTEST_PG_PASS} || '';
24
25 dbixcsl_common_tests->new(
26     vendor      => 'Pg',
27     auto_inc_pk => 'SERIAL NOT NULL PRIMARY KEY',
28     dsn         => $dsn,
29     user        => $user,
30     password    => $password,
31     loader_options  => { preserve_case => 1 },
32     connect_info_opts => {
33         pg_enable_utf8 => 1,
34         on_connect_do  => [ 'SET client_min_messages=WARNING' ],
35     },
36     quote_char  => '"',
37     default_is_deferrable => 0,
38     default_on_clause => 'NO ACTION',
39     data_types  => {
40         # http://www.postgresql.org/docs/7.4/interactive/datatype.html
41         #
42         # Numeric Types
43         boolean     => { data_type => 'boolean' },
44         bool        => { data_type => 'boolean' },
45         'bool default false'
46                     => { data_type => 'boolean', default_value => \'false' },
47         'bool default true'
48                     => { data_type => 'boolean', default_value => \'true' },
49         'bool default 0::bool'
50                     => { data_type => 'boolean', default_value => \'false' },
51         'bool default 1::bool'
52                     => { data_type => 'boolean', default_value => \'true' },
53
54         bigint      => { data_type => 'bigint' },
55         int8        => { data_type => 'bigint' },
56         bigserial   => { data_type => 'bigint', is_auto_increment => 1 },
57         serial8     => { data_type => 'bigint', is_auto_increment => 1 },
58         integer     => { data_type => 'integer' },
59         int         => { data_type => 'integer' },
60         int4        => { data_type => 'integer' },
61         serial      => { data_type => 'integer', is_auto_increment => 1 },
62         serial4     => { data_type => 'integer', is_auto_increment => 1 },
63         smallint    => { data_type => 'smallint' },
64         int2        => { data_type => 'smallint' },
65
66         money       => { data_type => 'money' },
67
68         'double precision' => { data_type => 'double precision' },
69         float8             => { data_type => 'double precision' },
70         real               => { data_type => 'real' },
71         float4             => { data_type => 'real' },
72         'float(24)'        => { data_type => 'real' },
73         'float(25)'        => { data_type => 'double precision' },
74         'float(53)'        => { data_type => 'double precision' },
75         float              => { data_type => 'double precision' },
76
77         numeric            => { data_type => 'numeric' },
78         decimal            => { data_type => 'numeric' },
79         'numeric(6,3)'     => { data_type => 'numeric', size => [6,3] },
80         'decimal(6,3)'     => { data_type => 'numeric', size => [6,3] },
81
82         # Bit String Types
83         'bit varying(2)' => { data_type => 'varbit', size => 2 },
84         'varbit(2)'      => { data_type => 'varbit', size => 2 },
85         'bit varying'    => { data_type => 'varbit' },
86         'varbit'         => { data_type => 'varbit' },
87         bit              => { data_type => 'bit', size => 1 },
88         'bit(3)'         => { data_type => 'bit', size => 3 },
89
90         # Network Types
91         inet    => { data_type => 'inet' },
92         cidr    => { data_type => 'cidr' },
93         macaddr => { data_type => 'macaddr' },
94
95         # Geometric Types
96         point   => { data_type => 'point' },
97         line    => { data_type => 'line' },
98         lseg    => { data_type => 'lseg' },
99         box     => { data_type => 'box' },
100         path    => { data_type => 'path' },
101         polygon => { data_type => 'polygon' },
102         circle  => { data_type => 'circle' },
103
104         # Character Types
105         'character varying(2)'           => { data_type => 'varchar', size => 2 },
106         'varchar(2)'                     => { data_type => 'varchar', size => 2 },
107         'character(2)'                   => { data_type => 'char', size => 2 },
108         'char(2)'                        => { data_type => 'char', size => 2 },
109         # check that default null is correctly rewritten
110         'char(3) default null'           => { data_type => 'char', size => 3,
111                                               default_value => \'null' },
112         'character'                      => { data_type => 'char', size => 1 },
113         'char'                           => { data_type => 'char', size => 1 },
114         text                             => { data_type => 'text' },
115         # varchar with no size has unlimited size, we rewrite to 'text'
116         varchar                          => { data_type => 'text',
117                                               original => { data_type => 'varchar' } },
118         # check default null again (to make sure ref is safe)
119         'varchar(3) default null'        => { data_type => 'varchar', size => 3,
120                                               default_value => \'null' },
121
122         # Datetime Types
123         date                             => { data_type => 'date' },
124         interval                         => { data_type => 'interval' },
125         'interval(0)'                    => { data_type => 'interval', size => 0 },
126         'interval(2)'                    => { data_type => 'interval', size => 2 },
127         time                             => { data_type => 'time' },
128         'time(0)'                        => { data_type => 'time', size => 0 },
129         'time(2)'                        => { data_type => 'time', size => 2 },
130         'time without time zone'         => { data_type => 'time' },
131         'time(0) without time zone'      => { data_type => 'time', size => 0 },
132         'time with time zone'            => { data_type => 'time with time zone' },
133         'time(0) with time zone'         => { data_type => 'time with time zone', size => 0 },
134         'time(2) with time zone'         => { data_type => 'time with time zone', size => 2 },
135         timestamp                        => { data_type => 'timestamp' },
136         'timestamp default now()'        => { data_type => 'timestamp',
137                                               default_value => \'current_timestamp',
138                                               original => { default_value => \'now()' } },
139         'timestamp(0)'                   => { data_type => 'timestamp', size => 0 },
140         'timestamp(2)'                   => { data_type => 'timestamp', size => 2 },
141         'timestamp without time zone'    => { data_type => 'timestamp' },
142         'timestamp(0) without time zone' => { data_type => 'timestamp', size => 0 },
143         'timestamp(2) without time zone' => { data_type => 'timestamp', size => 2 },
144
145         'timestamp with time zone'       => { data_type => 'timestamp with time zone' },
146         'timestamp(0) with time zone'    => { data_type => 'timestamp with time zone', size => 0 },
147         'timestamp(2) with time zone'    => { data_type => 'timestamp with time zone', size => 2 },
148
149         # Blob Types
150         bytea => { data_type => 'bytea' },
151
152         # Enum Types
153         pg_loader_test_enum => { data_type => 'enum',
154                                  extra => { custom_type_name => 'pg_loader_test_enum',
155                                             list => [ qw/foo bar baz/] } },
156     },
157     pre_create => [
158         q{
159             CREATE TYPE pg_loader_test_enum AS ENUM (
160                 'foo', 'bar', 'baz'
161             )
162         },
163     ],
164     extra       => {
165         create => [
166             q{
167                 CREATE SCHEMA dbicsl_test
168             },
169             q{
170                 CREATE SEQUENCE dbicsl_test.myseq
171             },
172             q{
173                 CREATE TABLE pg_loader_test1 (
174                     id INTEGER NOT NULL DEFAULT nextval('dbicsl_test.myseq') PRIMARY KEY,
175                     value VARCHAR(100)
176                 )
177             },
178             qq{
179                 COMMENT ON TABLE pg_loader_test1 IS 'The\15\12Table ∑'
180             },
181             qq{
182                 COMMENT ON COLUMN pg_loader_test1.value IS 'The\15\12Column'
183             },
184             q{
185                 CREATE TABLE pg_loader_test2 (
186                     id SERIAL PRIMARY KEY,
187                     value VARCHAR(100)
188                 )
189             },
190             q{
191                 COMMENT ON TABLE pg_loader_test2 IS 'very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long comment'
192             },
193             q{
194                 CREATE SCHEMA "dbicsl-test"
195             },
196             q{
197                 CREATE TABLE "dbicsl-test".pg_loader_test4 (
198                     id SERIAL PRIMARY KEY,
199                     value VARCHAR(100)
200                 )
201             },
202             q{
203                 CREATE TABLE "dbicsl-test".pg_loader_test5 (
204                     id SERIAL PRIMARY KEY,
205                     value VARCHAR(100),
206                     four_id INTEGER REFERENCES "dbicsl-test".pg_loader_test4 (id),
207                     CONSTRAINT loader_test5_uniq UNIQUE (four_id)
208                 )
209             },
210             q{
211                 CREATE SCHEMA "dbicsl.test"
212             },
213             q{
214                 CREATE TABLE "dbicsl.test".pg_loader_test5 (
215                     pk SERIAL PRIMARY KEY,
216                     value VARCHAR(100),
217                     four_id INTEGER REFERENCES "dbicsl-test".pg_loader_test4 (id),
218                     CONSTRAINT loader_test5_uniq UNIQUE (four_id)
219                 )
220             },
221             q{
222                 CREATE TABLE "dbicsl.test".pg_loader_test6 (
223                     id SERIAL PRIMARY KEY,
224                     value VARCHAR(100),
225                     pg_loader_test4_id INTEGER REFERENCES "dbicsl-test".pg_loader_test4 (id)
226                 )
227             },
228             q{
229                 CREATE TYPE "dbicsl.test".pg_loader_test_enum2 AS ENUM ('wibble','wobble')
230             },
231             q{
232                 CREATE TABLE "dbicsl.test".pg_loader_test7 (
233                     id SERIAL PRIMARY KEY,
234                     value "dbicsl.test".pg_loader_test_enum2,
235                     six_id INTEGER UNIQUE REFERENCES "dbicsl.test".pg_loader_test6 (id)
236                 )
237             },
238             q{
239                 CREATE TABLE "dbicsl-test".pg_loader_test8 (
240                     id SERIAL PRIMARY KEY,
241                     value VARCHAR(100),
242                     pg_loader_test7_id INTEGER REFERENCES "dbicsl.test".pg_loader_test7 (id)
243                 )
244             },
245             # 4 through 8 are used for the multi-schema tests
246             q{
247                 create table pg_loader_test9 (
248                     id bigserial primary key
249                 )
250             },
251             q{
252                 create table pg_loader_test10 (
253                     id bigserial primary key,
254                     nine_id int,
255                     foreign key (nine_id) references pg_loader_test9(id)
256                         on delete restrict on update set null deferrable
257                 )
258             },
259             q{
260                 create view pg_loader_test11 as
261                     select * from pg_loader_test1
262             },
263             q{
264                 create table pg_loader_test12 (
265                     id integer not null,
266                     value integer,
267                     active boolean,
268                     name text
269                 )
270             },
271             q{
272                 create unique index uniq_id_lc_name on pg_loader_test12 (
273                     id, lower(name)
274                 )
275             },
276             q{
277                 create unique index uniq_uc_name_id on pg_loader_test12 (
278                     upper(name), id
279                 )
280             },
281             q{
282                 create unique index pg_loader_test12_value on pg_loader_test12 (
283                     value
284                 )
285             },
286             q{
287                 create unique index pg_loader_test12_name_active on pg_loader_test12 (
288                     name
289                 ) where active
290             },
291         ],
292         pre_drop_ddl => [
293             'DROP SCHEMA dbicsl_test CASCADE',
294             'DROP SCHEMA "dbicsl-test" CASCADE',
295             'DROP SCHEMA "dbicsl.test" CASCADE',
296             'DROP TYPE pg_loader_test_enum',
297             'DROP VIEW pg_loader_test11',
298         ],
299         drop  => [ qw/pg_loader_test1 pg_loader_test2 pg_loader_test9 pg_loader_test10 pg_loader_test12/ ],
300         count => 11 + 33 * 2,   # regular + multi-schema * 2
301         run   => sub {
302             my ($schema, $monikers, $classes) = @_;
303
304             is $schema->source($monikers->{pg_loader_test1})->column_info('id')->{sequence},
305                 'dbicsl_test.myseq',
306                 'qualified sequence detected';
307
308             my $class    = $classes->{pg_loader_test1};
309             my $filename = $schema->loader->get_dump_filename($class);
310
311             my $code = slurp_file $filename;
312
313             like $code, qr/^=head1 NAME\n\n^$class - The\nTable ∑\n\n^=cut\n/m,
314                 'table comment';
315
316             like $code, qr/^=head2 value\n\n(.+:.+\n)+\nThe\nColumn\n\n/m,
317                 'column comment and attrs';
318
319             $class    = $classes->{pg_loader_test2};
320             $filename = $schema->loader->get_dump_filename($class);
321
322             $code = slurp_file $filename;
323
324             like $code, qr/^=head1 NAME\n\n^$class\n\n=head1 DESCRIPTION\n\n^very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long comment\n\n^=cut\n/m,
325                 'long table comment is in DESCRIPTION';
326
327             # test on delete/update fk clause introspection
328             ok ((my $rel_info = $schema->source('PgLoaderTest10')->relationship_info('nine')),
329                 'got rel info');
330
331             is $rel_info->{attrs}{on_delete}, 'RESTRICT',
332                 'ON DELETE clause introspected correctly';
333
334             is $rel_info->{attrs}{on_update}, 'SET NULL',
335                 'ON UPDATE clause introspected correctly';
336
337             is $rel_info->{attrs}{is_deferrable}, 1,
338                 'DEFERRABLE clause introspected correctly';
339
340             foreach my $db_schema (['dbicsl-test', 'dbicsl.test'], '%') {
341                 lives_and {
342                     rmtree EXTRA_DUMP_DIR;
343
344                     my @warns;
345                     local $SIG{__WARN__} = sub {
346                         push @warns, $_[0] unless $_[0] =~ /\bcollides\b/;
347                     };
348
349                     make_schema_at(
350                         'PGMultiSchema',
351                         {
352                             naming => 'current',
353                             db_schema => $db_schema,
354                             preserve_case => 1,
355                             dump_directory => EXTRA_DUMP_DIR,
356                             quiet => 1,
357                         },
358                         [ $dsn, $user, $password, {
359                             on_connect_do  => [ 'SET client_min_messages=WARNING' ],
360                         } ],
361                     );
362
363                     diag join "\n", @warns if @warns;
364
365                     is @warns, 0;
366                 } 'dumped schema for "dbicsl-test" and "dbicsl.test" schemas with no warnings';
367
368                 my ($test_schema, $rsrc, $rs, $row, %uniqs, $rel_info);
369
370                 lives_and {
371                     ok $test_schema = PGMultiSchema->connect($dsn, $user, $password, {
372                         on_connect_do  => [ 'SET client_min_messages=WARNING' ],
373                     });
374                 } 'connected test schema';
375
376                 lives_and {
377                     ok $rsrc = $test_schema->source('PgLoaderTest4');
378                 } 'got source for table in schema name with dash';
379
380                 is try { $rsrc->column_info('id')->{is_auto_increment} }, 1,
381                     'column in schema name with dash';
382
383                 is try { $rsrc->column_info('value')->{data_type} }, 'varchar',
384                     'column in schema name with dash';
385
386                 is try { $rsrc->column_info('value')->{size} }, 100,
387                     'column in schema name with dash';
388
389                 lives_and {
390                     ok $rs = $test_schema->resultset('PgLoaderTest4');
391                 } 'got resultset for table in schema name with dash';
392
393                 lives_and {
394                     ok $row = $rs->create({ value => 'foo' });
395                 } 'executed SQL on table in schema name with dash';
396
397                 $rel_info = try { $rsrc->relationship_info('dbicsl_dash_test_pg_loader_test5') };
398
399                 is_deeply $rel_info->{cond}, {
400                     'foreign.four_id' => 'self.id'
401                 }, 'relationship in schema name with dash';
402
403                 is $rel_info->{attrs}{accessor}, 'single',
404                     'relationship in schema name with dash';
405
406                 is $rel_info->{attrs}{join_type}, 'LEFT',
407                     'relationship in schema name with dash';
408
409                 lives_and {
410                     ok $rsrc = $test_schema->source('DbicslDashTestPgLoaderTest5');
411                 } 'got source for table in schema name with dash';
412
413                 %uniqs = try { $rsrc->unique_constraints };
414
415                 is keys %uniqs, 2,
416                     'got unique and primary constraint in schema name with dash';
417
418                 delete $uniqs{primary};
419
420                 is_deeply(
421                     (values %uniqs)[0], ['four_id'],
422                     'unique constraint is correct in schema name with dash'
423                 );
424
425                 lives_and {
426                     ok $rsrc = $test_schema->source('PgLoaderTest6');
427                 } 'got source for table in schema name with dot';
428
429                 is try { $rsrc->column_info('id')->{is_auto_increment} }, 1,
430                     'column in schema name with dot introspected correctly';
431
432                 is try { $rsrc->column_info('value')->{data_type} }, 'varchar',
433                     'column in schema name with dot introspected correctly';
434
435                 is try { $rsrc->column_info('value')->{size} }, 100,
436                     'column in schema name with dot introspected correctly';
437
438                 lives_and {
439                     ok $rs = $test_schema->resultset('PgLoaderTest6');
440                 } 'got resultset for table in schema name with dot';
441
442                 lives_and {
443                     ok $row = $rs->create({ value => 'foo' });
444                 } 'executed SQL on table in schema name with dot';
445
446                 $rel_info = try { $rsrc->relationship_info('pg_loader_test7') };
447
448                 is_deeply $rel_info->{cond}, {
449                     'foreign.six_id' => 'self.id'
450                 }, 'relationship in schema name with dot';
451
452                 is $rel_info->{attrs}{accessor}, 'single',
453                     'relationship in schema name with dot';
454
455                 is $rel_info->{attrs}{join_type}, 'LEFT',
456                     'relationship in schema name with dot';
457
458                 lives_and {
459                     ok $rsrc = $test_schema->source('PgLoaderTest7');
460                     my $col_info = $rsrc->column_info('value');
461                     is $col_info->{data_type}, 'enum',
462                         'enum column in schema name with dot';
463                     is $col_info->{extra}{custom_type_name}, '"dbicsl.test".pg_loader_test_enum2',
464                         'original data type for enum in schema name with dot';
465                     is_deeply $col_info->{extra}{list}, [qw(wibble wobble)],
466                         'value list for for enum in schema name with dot';
467                 } 'got source for table in schema name with dot';
468
469                 %uniqs = try { $rsrc->unique_constraints };
470
471                 is keys %uniqs, 2,
472                     'got unique and primary constraint in schema name with dot';
473
474                 delete $uniqs{primary};
475
476                 is_deeply(
477                     (values %uniqs)[0], ['six_id'],
478                     'unique constraint is correct in schema name with dot'
479                 );
480
481                 lives_and {
482                     ok $test_schema->source('PgLoaderTest6')
483                         ->has_relationship('pg_loader_test4');
484                 } 'cross-schema relationship in multi-db_schema';
485
486                 lives_and {
487                     ok $test_schema->source('PgLoaderTest4')
488                         ->has_relationship('pg_loader_test6s');
489                 } 'cross-schema relationship in multi-db_schema';
490
491                 lives_and {
492                     ok $test_schema->source('PgLoaderTest8')
493                         ->has_relationship('pg_loader_test7');
494                 } 'cross-schema relationship in multi-db_schema';
495
496                 lives_and {
497                     ok $test_schema->source('PgLoaderTest7')
498                         ->has_relationship('pg_loader_test8s');
499                 } 'cross-schema relationship in multi-db_schema';
500             }
501
502             # test that views are marked as such
503             my $view_source = $schema->resultset($monikers->{pg_loader_test11})->result_source;
504             isa_ok $view_source, 'DBIx::Class::ResultSource::View',
505                 'view result source';
506
507             like $view_source->view_definition,
508                 qr/\A \s* select\b .* \bfrom \s+ pg_loader_test1 \s* \z/imsx,
509                 'view definition';
510
511             is_deeply
512                 { $schema->source($monikers->{pg_loader_test12})->unique_constraints },
513                 { pg_loader_test12_value => ['value'] },
514                 'unique indexes are dumped correctly';
515         },
516     },
517     cond_extra => [
518         {
519             desc => 'identity columns',
520             skip => 'requires PostgreSQL 10',
521             cond => sub { $_[0]->{pg_server_version} >= 100000 },
522             create => [
523                 q{
524                     create table pg_loader_test13 (
525                         always integer generated always as identity,
526                         by_default integer generated by default as identity
527                     )
528                 },
529             ],
530             drop => [ qw(pg_loader_test13) ],
531             run => sub {
532                 my ($schema, $monikers, $classes) = @_;
533
534                 my $rsrc13 = $schema->source($monikers->{pg_loader_test13});
535                 for my $col (qw(by_default always)) {
536                     my $info = $rsrc13->column_info($col);
537                     (my $when = uc $col) =~ tr/_/ /;
538
539                     ok $info->{is_auto_increment},
540                         "is_auto_increment for GENERATED $when AS IDENTITY";
541
542                     is $info->{extra}{generated_as_identity}, $col,
543                         "generated_as_identity for GENERATED $when AS IDENTITY";
544                 }
545             },
546         },
547     ],
548 )->run_tests();
549
550 END {
551     rmtree EXTRA_DUMP_DIR unless $ENV{SCHEMA_LOADER_TESTS_NOCLEANUP};
552 }
553 # vim:et sw=4 sts=4 tw=0: