Fix detection of qualified sequence names for Oracle (RT#90341)
[dbsrgits/DBIx-Class-Schema-Loader.git] / t / 10_05ora_common.t
1 use strict;
2 use warnings;
3 use Test::More;
4 use Test::Exception;
5 use DBIx::Class::Schema::Loader 'make_schema_at';
6 use DBIx::Class::Schema::Loader::Utils qw/slurp_file split_name/;
7 use Try::Tiny;
8 use File::Path 'rmtree';
9 use String::ToIdentifier::EN::Unicode 'to_identifier';
10 use namespace::clean;
11
12 use lib qw(t/lib);
13 use dbixcsl_common_tests ();
14 use dbixcsl_test_dir '$tdir';
15
16 use constant EXTRA_DUMP_DIR => "$tdir/ora_extra_dump";
17
18 my $dsn      = $ENV{DBICTEST_ORA_DSN} || '';
19 my $user     = $ENV{DBICTEST_ORA_USER} || '';
20 my $password = $ENV{DBICTEST_ORA_PASS} || '';
21
22 my ($schema, $extra_schema); # for cleanup in END for extra tests
23
24 my $auto_inc_cb = sub {
25     my ($table, $col) = @_;
26     return (
27         qq{ CREATE SEQUENCE ${table}_${col}_seq START WITH 1 INCREMENT BY 1},
28         qq{
29             CREATE OR REPLACE TRIGGER ${table}_${col}_trigger
30             BEFORE INSERT ON ${table}
31             FOR EACH ROW
32             BEGIN
33                 SELECT ${table}_${col}_seq.nextval INTO :NEW.${col} FROM dual;
34             END;
35         }
36     );
37 };
38
39 my $auto_inc_drop_cb = sub {
40     my ($table, $col) = @_;
41     return qq{ DROP SEQUENCE ${table}_${col}_seq };
42 };
43
44 my $tester = dbixcsl_common_tests->new(
45     vendor      => 'Oracle',
46     auto_inc_pk => 'INTEGER NOT NULL PRIMARY KEY',
47     auto_inc_cb => $auto_inc_cb,
48     auto_inc_drop_cb => $auto_inc_drop_cb,
49     preserve_case_mode_is_exclusive => 1,
50     quote_char                      => '"',
51     default_is_deferrable => 0,
52     default_on_delete_clause => 'NO ACTION',
53     default_on_update_clause => 'NO ACTION',
54     dsn         => $dsn,
55     user        => $user,
56     password    => $password,
57     data_types  => {
58         # From:
59         # http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/sql_elements001.htm#i54330
60         #
61         # These tests require at least Oracle 9.2, because of the VARCHAR to
62         # VARCHAR2 casting.
63         #
64         # Character Types
65         'char'         => { data_type => 'char',      size => 1  },
66         'char(11)'     => { data_type => 'char',      size => 11 },
67         'nchar'        => { data_type => 'nchar',     size => 1  },
68         'national character'
69                        => { data_type => 'nchar',     size => 1  },
70         'nchar(11)'    => { data_type => 'nchar',     size => 11 },
71         'national character(11)'
72                        => { data_type => 'nchar',     size => 11 },
73         'varchar(20)'  => { data_type => 'varchar2',  size => 20 },
74         'varchar2(20)' => { data_type => 'varchar2',  size => 20 },
75         'nvarchar2(20)'=> { data_type => 'nvarchar2', size => 20 },
76         'national character varying(20)'
77                        => { data_type => 'nvarchar2', size => 20 },
78
79         # Numeric Types
80         #
81         # integer/decimal/numeric is alised to NUMBER
82         #
83         'integer'      => { data_type => 'integer', original => { data_type => 'number', size => [38,0] } },
84         'int'          => { data_type => 'integer', original => { data_type => 'number', size => [38,0] } },
85         'smallint'     => { data_type => 'integer', original => { data_type => 'number', size => [38,0] } },
86
87         # very long DEFAULT throws an ORA-24345
88         "number(15) DEFAULT to_number(decode(substrb(userenv('CLIENT_INFO'),1,1),' ',null,substrb(userenv('CLIENT_INFO'),1,10)))" => {
89             data_type => 'numeric', size => [15,0], original => { data_type => 'number' },
90             default_value => \"to_number(decode(substrb(userenv('CLIENT_INFO'),1,1),' ',null,substrb(userenv('CLIENT_INFO'),1,10)))"
91         },
92
93         'decimal'      => { data_type => 'integer', original => { data_type => 'number', size => [38,0] } },
94         'dec'          => { data_type => 'integer', original => { data_type => 'number', size => [38,0] } },
95         'numeric'      => { data_type => 'integer', original => { data_type => 'number', size => [38,0] } },
96
97         'decimal(3)'   => { data_type => 'numeric', size => [3,0], original => { data_type => 'number' } },
98         'dec(3)'       => { data_type => 'numeric', size => [3,0], original => { data_type => 'number' } },
99         'numeric(3)'   => { data_type => 'numeric', size => [3,0], original => { data_type => 'number' } },
100
101         'decimal(3,3)' => { data_type => 'numeric', size => [3,3], original => { data_type => 'number' } },
102         'dec(3,3)'     => { data_type => 'numeric', size => [3,3], original => { data_type => 'number' } },
103         'numeric(3,3)' => { data_type => 'numeric', size => [3,3], original => { data_type => 'number' } },
104
105         'binary_float'  => { data_type => 'real',             original => { data_type => 'binary_float'  } },
106         'binary_double' => { data_type => 'double precision', original => { data_type => 'binary_double' } },
107
108         # these are not mentioned in the summary chart, must be aliased
109         real            => { data_type => 'real',             original => { data_type => 'float', size => 63  } },
110         'float(63)'     => { data_type => 'real',             original => { data_type => 'float', size => 63  } },
111         'float(64)'     => { data_type => 'double precision', original => { data_type => 'float', size => 64  } },
112         'float(126)'    => { data_type => 'double precision', original => { data_type => 'float', size => 126 } },
113         float           => { data_type => 'double precision', original => { data_type => 'float', size => 126 } },
114
115         # Blob Types
116         'raw(50)'      => { data_type => 'raw', size => 50 },
117         'clob'         => { data_type => 'clob' },
118         'nclob'        => { data_type => 'nclob' },
119         'blob'         => { data_type => 'blob' },
120         'bfile'        => { data_type => 'bfile' },
121         'long'         => { data_type => 'long' },
122         'long raw'     => { data_type => 'long raw' },
123
124         # Datetime Types
125         'date'         => { data_type => 'datetime', original => { data_type => 'date' } },
126         'date default sysdate'
127                        => { data_type => 'datetime', default_value => \'current_timestamp',
128                             original  => { data_type => 'date', default_value => \'sysdate' } },
129         'timestamp'    => { data_type => 'timestamp' },
130         'timestamp default current_timestamp'
131                        => { data_type => 'timestamp', default_value => \'current_timestamp' },
132         'timestamp(3)' => { data_type => 'timestamp', size => 3 },
133         'timestamp with time zone'
134                        => { data_type => 'timestamp with time zone' },
135         'timestamp(3) with time zone'
136                        => { data_type => 'timestamp with time zone', size => 3 },
137         'timestamp with local time zone'
138                        => { data_type => 'timestamp with local time zone' },
139         'timestamp(3) with local time zone'
140                        => { data_type => 'timestamp with local time zone', size => 3 },
141         'interval year to month'
142                        => { data_type => 'interval year to month' },
143         'interval year(3) to month'
144                        => { data_type => 'interval year to month', size => 3 },
145         'interval day to second'
146                        => { data_type => 'interval day to second' },
147         'interval day(3) to second'
148                        => { data_type => 'interval day to second', size => [3,6] },
149         'interval day to second(3)'
150                        => { data_type => 'interval day to second', size => [2,3] },
151         'interval day(3) to second(3)'
152                        => { data_type => 'interval day to second', size => [3,3] },
153
154         # Other Types
155         'rowid'        => { data_type => 'rowid' },
156         'urowid'       => { data_type => 'urowid' },
157         'urowid(3333)' => { data_type => 'urowid', size => 3333 },
158     },
159     extra => {
160         create => [
161             q{
162                 CREATE TABLE oracle_loader_test1 (
163                     id NUMBER(11),
164                     value VARCHAR2(100)
165                 )
166             },
167             q{ COMMENT ON TABLE oracle_loader_test1 IS 'oracle_loader_test1 table comment' },
168             q{ COMMENT ON COLUMN oracle_loader_test1.value IS 'oracle_loader_test1.value column comment' },
169             # 4 through 8 are used for the multi-schema tests
170             q{
171                 create table oracle_loader_test9 (
172                     id int primary key
173                 )
174             },
175             q{
176                 create table oracle_loader_test10 (
177                     id int primary key,
178                     nine_id int,
179                     foreign key (nine_id) references oracle_loader_test9(id)
180                         on delete set null deferrable
181                 )
182             },
183         ],
184         drop  => [qw/oracle_loader_test1 oracle_loader_test9 oracle_loader_test10/],
185         count => 7 + 31 * 2,
186         run   => sub {
187             my ($monikers, $classes);
188             ($schema, $monikers, $classes) = @_;
189
190             SKIP: {
191                 if (my $source = $monikers->{loader_test1s}) {
192                     is $schema->source($source)->column_info('id')->{sequence},
193                         'loader_test1s_id_seq',
194                         'Oracle sequence detection';
195                 }
196                 else {
197                     skip 'not running common tests', 1;
198                 }
199             }
200
201             my $class = $classes->{oracle_loader_test1};
202
203             my $filename = $schema->loader->get_dump_filename($class);
204             my $code = slurp_file $filename;
205
206             like $code, qr/^=head1 NAME\n\n^$class - oracle_loader_test1 table comment\n\n^=cut\n/m,
207                 'table comment';
208
209             like $code, qr/^=head2 value\n\n(.+:.+\n)+\noracle_loader_test1\.value column comment\n\n/m,
210                 'column comment and attrs';
211
212             # test on delete/update fk clause introspection
213             ok ((my $rel_info = $schema->source('OracleLoaderTest10')->relationship_info('nine')),
214                 'got rel info');
215
216             is $rel_info->{attrs}{on_delete}, 'SET NULL',
217                 'ON DELETE clause introspected correctly';
218
219             is $rel_info->{attrs}{on_update}, 'NO ACTION',
220                 'ON UPDATE clause set to NO ACTION by default';
221
222             is $rel_info->{attrs}{is_deferrable}, 1,
223                 'DEFERRABLE clause introspected correctly';
224
225             SKIP: {
226                 skip 'Set the DBICTEST_ORA_EXTRAUSER_DSN, _USER and _PASS environment variables to run the cross-schema relationship tests', 31 * 2
227                     unless $ENV{DBICTEST_ORA_EXTRAUSER_DSN};
228
229                 $extra_schema = $schema->clone;
230                 $extra_schema->connection(@ENV{map "DBICTEST_ORA_EXTRAUSER_$_",
231                     qw/DSN USER PASS/
232                 });
233
234                 my $dbh1 = $schema->storage->dbh;
235                 my $dbh2 = $extra_schema->storage->dbh;
236
237                 my ($schema1) = $dbh1->selectrow_array('SELECT USER FROM DUAL');
238                 my ($schema2) = $dbh2->selectrow_array('SELECT USER FROM DUAL');
239
240                 $dbh1->do(<<'EOF');
241                     CREATE TABLE oracle_loader_test4 (
242                         id INT NOT NULL PRIMARY KEY,
243                         value VARCHAR(100)
244                     )
245 EOF
246
247                 $dbh1->do($_) for $auto_inc_cb->(lc "${schema1}.oracle_loader_test4", 'id');
248
249                 $dbh1->do("GRANT ALL ON oracle_loader_test4 TO $schema2");
250                 $dbh1->do("GRANT ALL ON oracle_loader_test4_id_seq TO $schema2");
251
252                 $dbh1->do(<<"EOF");
253                     CREATE TABLE oracle_loader_test5 (
254                         id INT NOT NULL PRIMARY KEY,
255                         value VARCHAR(100),
256                         four_id INT REFERENCES ${schema1}.oracle_loader_test4 (id),
257                         CONSTRAINT ora_loader5_uniq UNIQUE (four_id)
258                     )
259 EOF
260                 $dbh1->do($_) for $auto_inc_cb->('oracle_loader_test5', 'id');
261                 $dbh1->do("GRANT ALL ON oracle_loader_test5 TO $schema2");
262                 $dbh1->do("GRANT ALL ON oracle_loader_test5_id_seq TO $schema2");
263
264                 $dbh2->do(<<"EOF");
265                     CREATE TABLE oracle_loader_test5 (
266                         pk INT NOT NULL PRIMARY KEY,
267                         value VARCHAR(100),
268                         four_id INT REFERENCES ${schema1}.oracle_loader_test4 (id),
269                         CONSTRAINT ora_loader5_uniq UNIQUE (four_id)
270                     )
271 EOF
272                 $dbh2->do($_) for $auto_inc_cb->('oracle_loader_test5', 'pk');
273                 $dbh2->do("GRANT ALL ON oracle_loader_test5 TO $schema1");
274                 $dbh2->do("GRANT ALL ON oracle_loader_test5_pk_seq TO $schema1");
275
276                 $dbh2->do(<<"EOF");
277                     CREATE TABLE oracle_loader_test6 (
278                         id INT NOT NULL PRIMARY KEY,
279                         value VARCHAR(100),
280                         oracle_loader_test4_id INT REFERENCES ${schema1}.oracle_loader_test4 (id)
281                     )
282 EOF
283                 $dbh2->do($_) for $auto_inc_cb->('oracle_loader_test6', 'id');
284                 $dbh2->do("GRANT ALL ON oracle_loader_test6 to $schema1");
285                 $dbh2->do("GRANT ALL ON oracle_loader_test6_id_seq TO $schema1");
286
287                 $dbh2->do(<<"EOF");
288                     CREATE TABLE oracle_loader_test7 (
289                         id INT NOT NULL PRIMARY KEY,
290                         value VARCHAR(100),
291                         six_id INT UNIQUE REFERENCES ${schema2}.oracle_loader_test6 (id)
292                     )
293 EOF
294                 $dbh2->do($_) for $auto_inc_cb->('oracle_loader_test7', 'id');
295                 $dbh2->do("GRANT ALL ON oracle_loader_test7 to $schema1");
296                 $dbh2->do("GRANT ALL ON oracle_loader_test7_id_seq TO $schema1");
297
298                 $dbh1->do(<<"EOF");
299                     CREATE TABLE oracle_loader_test8 (
300                         id INT NOT NULL PRIMARY KEY,
301                         value VARCHAR(100),
302                         oracle_loader_test7_id INT REFERENCES ${schema2}.oracle_loader_test7 (id)
303                     )
304 EOF
305                 $dbh1->do($_) for $auto_inc_cb->('oracle_loader_test8', 'id');
306                 $dbh1->do("GRANT ALL ON oracle_loader_test8 to $schema2");
307                 $dbh1->do("GRANT ALL ON oracle_loader_test8_id_seq TO $schema2");
308
309                 # We add schema to moniker_parts, so make a monikers hash for
310                 # the tests, of the form schemanum.tablenum
311                 my $schema1_moniker = join '', map ucfirst lc, split_name to_identifier $schema1;
312                 my $schema2_moniker = join '', map ucfirst lc, split_name to_identifier $schema2;
313
314                 my %monikers;
315                 $monikers{'1.5'} = $schema1_moniker . 'OracleLoaderTest5';
316                 $monikers{'2.5'} = $schema2_moniker . 'OracleLoaderTest5';
317
318                 foreach my $db_schema ([$schema1, $schema2], '%') {
319                     lives_and {
320                         rmtree EXTRA_DUMP_DIR;
321
322                         my @warns;
323                         local $SIG{__WARN__} = sub {
324                             push @warns, $_[0] unless $_[0] =~ /\bcollides\b/;
325                         };
326
327                         make_schema_at(
328                             'OracleMultiSchema',
329                             {
330                                 naming => 'current',
331                                 db_schema => $db_schema,
332                                 dump_directory => EXTRA_DUMP_DIR,
333                                 quiet => 1,
334                             },
335                             [ $dsn, $user, $password ],
336                         );
337
338                         diag join "\n", @warns if @warns;
339
340                         is @warns, 0;
341                     } qq{dumped schema for "$schema1" and "$schema2" schemas with no warnings};
342
343                     my ($test_schema, $rsrc, $rs, $row, %uniqs, $rel_info);
344
345                     lives_and {
346                         ok $test_schema = OracleMultiSchema->connect($dsn, $user, $password);
347                     } 'connected test schema';
348
349                     lives_and {
350                         ok $rsrc = $test_schema->source('OracleLoaderTest4');
351                     } 'got source for table in schema1';
352
353                     is try { $rsrc->column_info('id')->{is_auto_increment} }, 1,
354                         'column in schema1';
355
356                     is try { $rsrc->column_info('id')->{sequence} }, lc "${schema1}.oracle_loader_test4_id_seq",
357                         'sequence in schema1';
358
359                     is try { $rsrc->column_info('value')->{data_type} }, 'varchar2',
360                         'column in schema1';
361
362                     is try { $rsrc->column_info('value')->{size} }, 100,
363                         'column in schema1';
364
365                     lives_and {
366                         ok $rs = $test_schema->resultset('OracleLoaderTest4');
367                     } 'got resultset for table in schema1';
368
369                     lives_and {
370                         ok $row = $rs->create({ value => 'foo' });
371                     } 'executed SQL on table in schema1';
372
373                     my $schema1_identifier = join '_', map lc, split_name to_identifier $schema1;
374
375                     $rel_info = try { $rsrc->relationship_info(
376                         $schema1_identifier . '_oracle_loader_test5'
377                     ) };
378
379                     is_deeply $rel_info->{cond}, {
380                         'foreign.four_id' => 'self.id'
381                     }, 'relationship in schema1';
382
383                     is $rel_info->{attrs}{accessor}, 'single',
384                         'relationship in schema1';
385
386                     is $rel_info->{attrs}{join_type}, 'LEFT',
387                         'relationship in schema1';
388
389                     lives_and {
390                         ok $rsrc = $test_schema->source($monikers{'1.5'});
391                     } 'got source for table in schema1';
392
393                     %uniqs = try { $rsrc->unique_constraints };
394
395                     is keys %uniqs, 2,
396                         'got unique and primary constraint in schema1';
397
398                     delete $uniqs{primary};
399
400                     is_deeply ((values %uniqs)[0], ['four_id'],
401                         'correct unique constraint in schema1');
402
403                     lives_and {
404                         ok $rsrc = $test_schema->source('OracleLoaderTest6');
405                     } 'got source for table in schema2';
406
407                     is try { $rsrc->column_info('id')->{is_auto_increment} }, 1,
408                         'column in schema2 introspected correctly';
409
410                     is try { $rsrc->column_info('value')->{data_type} }, 'varchar2',
411                         'column in schema2 introspected correctly';
412
413                     is try { $rsrc->column_info('value')->{size} }, 100,
414                         'column in schema2 introspected correctly';
415
416                     lives_and {
417                         ok $rs = $test_schema->resultset('OracleLoaderTest6');
418                     } 'got resultset for table in schema2';
419
420                     lives_and {
421                         ok $row = $rs->create({ value => 'foo' });
422                     } 'executed SQL on table in schema2';
423
424                     $rel_info = try { $rsrc->relationship_info('oracle_loader_test7') };
425
426                     is_deeply $rel_info->{cond}, {
427                         'foreign.six_id' => 'self.id'
428                     }, 'relationship in schema2';
429
430                     is $rel_info->{attrs}{accessor}, 'single',
431                         'relationship in schema2';
432
433                     is $rel_info->{attrs}{join_type}, 'LEFT',
434                         'relationship in schema2';
435
436                     lives_and {
437                         ok $rsrc = $test_schema->source('OracleLoaderTest7');
438                     } 'got source for table in schema2';
439
440                     %uniqs = try { $rsrc->unique_constraints };
441
442                     is keys %uniqs, 2,
443                         'got unique and primary constraint in schema2';
444
445                     delete $uniqs{primary};
446
447                     is_deeply ((values %uniqs)[0], ['six_id'],
448                         'correct unique constraint in schema2');
449
450                     lives_and {
451                         ok $test_schema->source('OracleLoaderTest6')
452                             ->has_relationship('oracle_loader_test4');
453                     } 'cross-schema relationship in multi-db_schema';
454
455                     lives_and {
456                         ok $test_schema->source('OracleLoaderTest4')
457                             ->has_relationship('oracle_loader_test6s');
458                     } 'cross-schema relationship in multi-db_schema';
459
460                     lives_and {
461                         ok $test_schema->source('OracleLoaderTest8')
462                             ->has_relationship('oracle_loader_test7');
463                     } 'cross-schema relationship in multi-db_schema';
464
465                     lives_and {
466                         ok $test_schema->source('OracleLoaderTest7')
467                             ->has_relationship('oracle_loader_test8s');
468                     } 'cross-schema relationship in multi-db_schema';
469                 }
470             }
471         },
472     },
473 );
474
475 if( !$dsn || !$user ) {
476     $tester->skip_tests('You need to set the DBICTEST_ORA_DSN, _USER, and _PASS environment variables');
477 }
478 else {
479     $tester->run_tests();
480 }
481
482 END {
483     if (not $ENV{SCHEMA_LOADER_TESTS_NOCLEANUP}) {
484         if (my $dbh2 = try { $extra_schema->storage->dbh }) {
485             my $dbh1 = $schema->storage->dbh;
486
487             try {
488                 $dbh1->do($_) for $auto_inc_drop_cb->('oracle_loader_test8', 'id');
489                 $dbh2->do($_) for $auto_inc_drop_cb->('oracle_loader_test7', 'id');
490                 $dbh2->do($_) for $auto_inc_drop_cb->('oracle_loader_test6', 'id');
491                 $dbh2->do($_) for $auto_inc_drop_cb->('oracle_loader_test5', 'pk');
492                 $dbh1->do($_) for $auto_inc_drop_cb->('oracle_loader_test5', 'id');
493                 $dbh1->do($_) for $auto_inc_drop_cb->('oracle_loader_test4', 'id');
494             }
495             catch {
496                 die "Error dropping sequences for cross-schema test tables: $_";
497             };
498
499             try {
500                 $dbh1->do('DROP TABLE oracle_loader_test8');
501                 $dbh2->do('DROP TABLE oracle_loader_test7');
502                 $dbh2->do('DROP TABLE oracle_loader_test6');
503                 $dbh2->do('DROP TABLE oracle_loader_test5');
504                 $dbh1->do('DROP TABLE oracle_loader_test5');
505                 $dbh1->do('DROP TABLE oracle_loader_test4');
506             }
507             catch {
508                 die "Error dropping cross-schema test tables: $_";
509             };
510         }
511
512         rmtree EXTRA_DUMP_DIR;
513     }
514 }
515 # vim:et sw=4 sts=4 tw=0: