Exclude tables in the Oracle Recycle Bin (RT#128149)
[dbsrgits/DBIx-Class-Schema-Loader.git] / lib / DBIx / Class / Schema / Loader / DBI / Oracle.pm
1 package DBIx::Class::Schema::Loader::DBI::Oracle;
2
3 use strict;
4 use warnings;
5 use base 'DBIx::Class::Schema::Loader::DBI::Component::QuotedDefault';
6 use mro 'c3';
7 use Try::Tiny;
8 use DBIx::Class::Schema::Loader::Utils qw/sigwarn_silencer/;
9 use namespace::clean;
10
11 our $VERSION = '0.07049';
12
13 =head1 NAME
14
15 DBIx::Class::Schema::Loader::DBI::Oracle - DBIx::Class::Schema::Loader::DBI
16 Oracle Implementation.
17
18 =head1 DESCRIPTION
19
20 See L<DBIx::Class::Schema::Loader> and L<DBIx::Class::Schema::Loader::Base>.
21
22 =cut
23
24 sub _setup {
25     my $self = shift;
26
27     $self->next::method(@_);
28
29     my ($current_schema) = $self->dbh->selectrow_array('SELECT USER FROM DUAL');
30
31     $self->db_schema([ $current_schema ]) unless $self->db_schema;
32
33     if (@{ $self->db_schema } == 1 && $self->db_schema->[0] ne '%'
34         && lc($self->db_schema->[0]) ne lc($current_schema)) {
35         $self->dbh->do('ALTER SESSION SET current_schema=' . $self->db_schema->[0]);
36     }
37
38     if (not defined $self->preserve_case) {
39         $self->preserve_case(0);
40     }
41     elsif ($self->preserve_case) {
42         $self->schema->storage->sql_maker->quote_char('"');
43         $self->schema->storage->sql_maker->name_sep('.');
44     }
45 }
46
47 sub _build_name_sep { '.' }
48
49 sub _system_schemas {
50     my $self = shift;
51
52     # From http://www.adp-gmbh.ch/ora/misc/known_schemas.html
53
54     return ($self->next::method(@_), qw/ANONYMOUS APEX_PUBLIC_USER APEX_030200 APPQOSSYS CTXSYS DBSNMP DIP DMSYS EXFSYS LBACSYS MDDATA MDSYS MGMT_VIEW OLAPSYS ORACLE_OCM ORDDATA ORDPLUGINS ORDSYS OUTLN SI_INFORMTN_SCHEMA SPATIAL_CSW_ADMIN_USR SPATIAL_WFS_ADMIN_USR SYS SYSMAN SYSTEM TRACESRV MTSSYS OASPUBLIC OWBSYS OWBSYS_AUDIT WEBSYS WK_PROXY WKSYS WK_TEST WMSYS XDB OSE$HTTP$ADMIN AURORA$JIS$UTILITY$ AURORA$ORB$UNAUTHENTICATED/, qr/^FLOWS_\d\d\d\d\d\d\z/);
55 }
56
57 sub _system_tables {
58     my $self = shift;
59
60     return (
61         $self->next::method(@_),
62         'PLAN_TABLE',
63         qr/\ABIN\$.*\$\d+\z/,   # Tables in the recycle bin
64     );
65 }
66
67 sub _dbh_tables {
68     my ($self, $schema) = @_;
69
70     return $self->dbh->tables(undef, $schema, '%', 'TABLE,VIEW');
71 }
72
73 sub _filter_tables {
74     my $self = shift;
75
76     # silence a warning from older DBD::Oracles in tests
77     local $SIG{__WARN__} = sigwarn_silencer(
78         qr/^Field \d+ has an Oracle type \(\d+\) which is not explicitly supported/
79     );
80
81     return $self->next::method(@_);
82 }
83
84 sub _table_fk_info {
85     my $self = shift;
86     my ($table) = @_;
87
88     my $rels = $self->next::method(@_);
89
90     my $deferrable_sth = $self->dbh->prepare_cached(<<'EOF');
91 select deferrable from all_constraints
92 where owner = ? and table_name = ? and constraint_name = ? and status = 'ENABLED'
93 EOF
94
95     my @enabled_rels;
96     foreach my $rel (@$rels) {
97         # Oracle does not have update rules
98         $rel->{attrs}{on_update} = 'NO ACTION';;
99
100         # DBD::Oracle's foreign_key_info does not return DEFERRABILITY, so we get it ourselves
101         # Also use this to filter out disabled foreign keys, which are returned by DBD::Oracle < 1.76
102         my $deferrable = $self->dbh->selectrow_array(
103             $deferrable_sth, undef, $table->schema, $table->name, $rel->{_constraint_name}
104         ) or next;
105
106         $rel->{attrs}{is_deferrable} = $deferrable =~ /^DEFERRABLE/i ? 1 : 0;
107         push @enabled_rels, $rel;
108     }
109
110     return \@enabled_rels;
111 }
112
113 sub _table_uniq_info {
114     my ($self, $table) = @_;
115
116     my $sth = $self->dbh->prepare_cached(<<'EOF', {}, 1);
117 SELECT ac.constraint_name, acc.column_name
118 FROM all_constraints ac, all_cons_columns acc
119 WHERE acc.table_name=? AND acc.owner = ?
120     AND ac.table_name = acc.table_name AND ac.owner = acc.owner
121     AND acc.constraint_name = ac.constraint_name
122     AND ac.constraint_type = 'U'
123     AND ac.status = 'ENABLED'
124 ORDER BY acc.position
125 EOF
126
127     $sth->execute($table->name, $table->schema);
128
129     my %constr_names;
130
131     while(my $constr = $sth->fetchrow_arrayref) {
132         my $constr_name = $self->_lc($constr->[0]);
133         my $constr_col  = $self->_lc($constr->[1]);
134         push @{$constr_names{$constr_name}}, $constr_col;
135     }
136
137     return [ map { [ $_ => $constr_names{$_} ] } sort keys %constr_names ];
138 }
139
140 sub _table_comment {
141     my $self = shift;
142     my ($table) = @_;
143
144     my $table_comment = $self->next::method(@_);
145
146     return $table_comment if $table_comment;
147
148     ($table_comment) = $self->dbh->selectrow_array(<<'EOF', {}, $table->schema, $table->name);
149 SELECT comments FROM all_tab_comments
150 WHERE owner = ?
151   AND table_name = ?
152   AND (table_type = 'TABLE' OR table_type = 'VIEW')
153 EOF
154
155     return $table_comment
156 }
157
158 sub _column_comment {
159     my $self = shift;
160     my ($table, $column_number, $column_name) = @_;
161
162     my $column_comment = $self->next::method(@_);
163
164     return $column_comment if $column_comment;
165
166     ($column_comment) = $self->dbh->selectrow_array(<<'EOF', {}, $table->schema, $table->name, $self->_uc($column_name));
167 SELECT comments FROM all_col_comments
168 WHERE owner = ?
169   AND table_name = ?
170   AND column_name = ?
171 EOF
172
173     return $column_comment
174 }
175
176 sub _columns_info_for {
177     my $self = shift;
178     my ($table) = @_;
179
180     my $result = $self->next::method(@_);
181
182     local $self->dbh->{LongReadLen} = 1_000_000;
183     local $self->dbh->{LongTruncOk} = 1;
184
185     my $sth = $self->dbh->prepare_cached(<<'EOF', {}, 1);
186 SELECT trigger_body
187 FROM all_triggers
188 WHERE table_name = ? AND table_owner = ? AND status = 'ENABLED'
189 AND upper(trigger_type) LIKE '%BEFORE EACH ROW%' AND lower(triggering_event) LIKE '%insert%'
190 EOF
191
192     $sth->execute($table->name, $table->schema);
193
194     while (my ($trigger_body) = $sth->fetchrow_array) {
195         if (my ($seq_schema, $seq_name) = $trigger_body =~ /(?:"?(\w+)"?\.)?"?(\w+)"?\.nextval/i) {
196             if (my ($col_name) = $trigger_body =~ /:new\.(\w+)/i) {
197                 $col_name = $self->_lc($col_name);
198
199                 $result->{$col_name}{is_auto_increment} = 1;
200
201                 $seq_schema = $self->_lc($seq_schema || $table->schema);
202                 $seq_name   = $self->_lc($seq_name);
203
204                 $result->{$col_name}{sequence} = ($self->qualify_objects ? ($seq_schema . '.') : '') . $seq_name;
205             }
206         }
207     }
208
209     # Old DBD::Oracle report the size in (UTF-16) bytes, not characters
210     my $nchar_size_factor = $DBD::Oracle::VERSION >= 1.52 ? 1 : 2;
211
212     while (my ($col, $info) = each %$result) {
213         no warnings 'uninitialized';
214
215         my $sth = $self->dbh->prepare_cached(<<'EOF', {}, 1);
216 SELECT data_type, data_length
217 FROM all_tab_columns
218 WHERE column_name = ? AND table_name = ? AND owner = ?
219 EOF
220         $sth->execute($self->_uc($col), $table->name, $table->schema);
221         my ($data_type, $data_length) = $sth->fetchrow_array;
222         $sth->finish;
223         $data_type = lc $data_type;
224
225         if ($data_type =~ /^(?:n(?:var)?char2?|u?rowid|nclob|timestamp\(\d+\)(?: with(?: local)? time zone)?|binary_(?:float|double))\z/i) {
226             $info->{data_type} = $data_type;
227
228             if ($data_type =~ /^u?rowid\z/i) {
229                 $info->{size} = $data_length;
230             }
231         }
232
233         if ($info->{data_type} =~ /^(?:n?[cb]lob|long(?: raw)?|bfile|date|binary_(?:float|double)|rowid)\z/i) {
234             delete $info->{size};
235         }
236
237         if ($info->{data_type} =~ /^n(?:var)?char2?\z/i) {
238             if (ref $info->{size}) {
239                 $info->{size} = $info->{size}[0] / 8;
240             }
241             else {
242                 $info->{size} = $info->{size} / $nchar_size_factor;
243             }
244         }
245         elsif ($info->{data_type} =~ /^(?:var)?char2?\z/i) {
246             if (ref $info->{size}) {
247                 $info->{size} = $info->{size}[0];
248             }
249         }
250         elsif (lc($info->{data_type}) =~ /^(?:number|decimal)\z/i) {
251             $info->{original}{data_type} = 'number';
252             $info->{data_type}           = 'numeric';
253
254             if (try { $info->{size}[0] == 38 && $info->{size}[1] == 0 }) {
255                 $info->{original}{size} = $info->{size};
256
257                 $info->{data_type} = 'integer';
258                 delete $info->{size};
259             }
260         }
261         elsif (my ($precision) = $info->{data_type} =~ /^timestamp\((\d+)\)(?: with (?:local )?time zone)?\z/i) {
262             $info->{data_type} = join ' ', $info->{data_type} =~ /[a-z]+/ig;
263
264             if ($precision == 6) {
265                 delete $info->{size};
266             }
267             else {
268                 $info->{size} = $precision;
269             }
270         }
271         elsif ($info->{data_type} =~ /timestamp/i && ref $info->{size} && $info->{size}[0] == 0) {
272             my $size = $info->{size}[1];
273             delete $info->{size};
274             $info->{size} = $size unless $size == 6;
275         }
276         elsif (($precision) = $info->{data_type} =~ /^interval year\((\d+)\) to month\z/i) {
277             $info->{data_type} = join ' ', $info->{data_type} =~ /[a-z]+/ig;
278
279             if ($precision == 2) {
280                 delete $info->{size};
281             }
282             else {
283                 $info->{size} = $precision;
284             }
285         }
286         elsif (my ($day_precision, $second_precision) = $info->{data_type} =~ /^interval day\((\d+)\) to second\((\d+)\)\z/i) {
287             $info->{data_type} = join ' ', $info->{data_type} =~ /[a-z]+/ig;
288
289             if ($day_precision == 2 && $second_precision == 6) {
290                 delete $info->{size};
291             }
292             else {
293                 $info->{size} = [ $day_precision, $second_precision ];
294             }
295         }
296         elsif ($info->{data_type} =~ /^interval year to month\z/i && ref $info->{size}) {
297             my $precision = $info->{size}[0];
298
299             if ($precision == 2) {
300                 delete $info->{size};
301             }
302             else {
303                 $info->{size} = $precision;
304             }
305         }
306         elsif ($info->{data_type} =~ /^interval day to second\z/i && ref $info->{size}) {
307             if ($info->{size}[0] == 2 && $info->{size}[1] == 6) {
308                 delete $info->{size};
309             }
310         }
311         elsif (lc($info->{data_type}) eq 'float') {
312             $info->{original}{data_type} = 'float';
313             $info->{original}{size}      = $info->{size};
314
315             if ($info->{size} <= 63) {
316                 $info->{data_type} = 'real';
317             }
318             else {
319                 $info->{data_type} = 'double precision';
320             }
321             delete $info->{size};
322         }
323         elsif (lc($info->{data_type}) eq 'double precision') {
324             $info->{original}{data_type} = 'float';
325
326             my $size = try { $info->{size}[0] };
327
328             $info->{original}{size} = $size;
329
330             if ($size <= 63) {
331                 $info->{data_type} = 'real';
332             }
333             delete $info->{size};
334         }
335         elsif (lc($info->{data_type}) eq 'urowid' && $info->{size} == 4000) {
336             delete $info->{size};
337         }
338         elsif ($info->{data_type} eq '-9104') {
339             $info->{data_type} = 'rowid';
340             delete $info->{size};
341         }
342         elsif ($info->{data_type} eq '-2') {
343             $info->{data_type} = 'raw';
344             $info->{size} = try { $info->{size}[0] / 2 };
345         }
346         elsif (lc($info->{data_type}) eq 'date') {
347             $info->{data_type}           = 'datetime';
348             $info->{original}{data_type} = 'date';
349         }
350         elsif (lc($info->{data_type}) eq 'binary_float') {
351             $info->{data_type}           = 'real';
352             $info->{original}{data_type} = 'binary_float';
353         }
354         elsif (lc($info->{data_type}) eq 'binary_double') {
355             $info->{data_type}           = 'double precision';
356             $info->{original}{data_type} = 'binary_double';
357         }
358
359         # DEFAULT could be missed by ::DBI because of ORA-24345
360         if (not defined $info->{default_value}) {
361             local $self->dbh->{LongReadLen} = 1_000_000;
362             local $self->dbh->{LongTruncOk} = 1;
363             my $sth = $self->dbh->prepare_cached(<<'EOF', {}, 1);
364 SELECT data_default
365 FROM all_tab_columns
366 WHERE column_name = ? AND table_name = ? AND owner = ?
367 EOF
368             $sth->execute($self->_uc($col), $table->name, $table->schema);
369             my ($default) = $sth->fetchrow_array;
370             $sth->finish;
371
372             # this is mostly copied from ::DBI::QuotedDefault
373             if (defined $default) {
374                 s/^\s+//, s/\s+\z// for $default;
375
376                 if ($default =~ /^'(.*?)'\z/) {
377                     $info->{default_value} = $1;
378                 }
379                 elsif ($default =~ /^(-?\d.*?)\z/) {
380                     $info->{default_value} = $1;
381                 }
382                 elsif ($default =~ /^NULL\z/i) {
383                     my $null = 'null';
384                     $info->{default_value} = \$null;
385                 }
386                 elsif ($default ne '') {
387                     my $val = $default;
388                     $info->{default_value} = \$val;
389                 }
390             }
391         }
392
393         if ((try { lc(${ $info->{default_value} }) }||'') eq 'sysdate') {
394             my $current_timestamp  = 'current_timestamp';
395             $info->{default_value} = \$current_timestamp;
396
397             my $sysdate = 'sysdate';
398             $info->{original}{default_value} = \$sysdate;
399         }
400     }
401
402     return $result;
403 }
404
405 sub _dbh_column_info {
406     my $self  = shift;
407     my ($dbh) = @_;
408
409     # try to avoid ORA-24345
410     local $dbh->{LongReadLen} = 1_000_000;
411     local $dbh->{LongTruncOk} = 1;
412
413     return $self->next::method(@_);
414 }
415
416 sub _view_definition {
417     my ($self, $view) = @_;
418
419     return scalar $self->schema->storage->dbh->selectrow_array(<<'EOF', {}, $view->schema, $view->name);
420 SELECT text
421 FROM all_views
422 WHERE owner = ? AND view_name = ?
423 EOF
424 }
425
426 =head1 SEE ALSO
427
428 L<DBIx::Class::Schema::Loader>, L<DBIx::Class::Schema::Loader::Base>,
429 L<DBIx::Class::Schema::Loader::DBI>
430
431 =head1 AUTHORS
432
433 See L<DBIx::Class::Schema::Loader/AUTHORS>.
434
435 =head1 LICENSE
436
437 This library is free software; you can redistribute it and/or modify it under
438 the same terms as Perl itself.
439
440 =cut
441
442 1;
443 # vim:et sts=4 sw=4 tw=0: