introspect ON DELETE and DEFERRABLE for Oracle
Rafael Kitover [Mon, 3 Sep 2012 19:36:09 +0000 (15:36 -0400)]
Oracle has no ON UPDATE rules, so change the default to NO ACTION so
that users have no surprises when deploying an Oracle schema to SQLite
etc.

Add support to the generic ::Loader::DBI::_table_fk_info for
introspecting ON DELETE/UPDATE rules and DEFERRABLE clauses, as much as
the DBD supports it.

For Oracle I had to get the DEFERRABLE value manually in an override, as
well as making sure the on_update was set, as the Oracle driver uses the
::Loader::DBI method.

Changes
lib/DBIx/Class/Schema/Loader/DBI.pm
lib/DBIx/Class/Schema/Loader/DBI/Oracle.pm
t/10_05ora_common.t
t/lib/dbixcsl_common_tests.pm

diff --git a/Changes b/Changes
index 3f119e8..2cf8cc7 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,9 +1,15 @@
 Revision history for Perl extension DBIx::Class::Schema::Loader
 
+        - Oracle: introspect ON DELETE and DEFERRABLE FK clauses
+        - Oracle WARNING: on_delete is now 'NO ACTION' by default, not
+          'CASCADE'. on_update is now 'NO ACTION' by default (Oracle does not
+          have update rules, this was done to preserve the behavior of the
+          schema when cross-deploying to SQLite.) is_deferrable is now
+          0 by default, not 1.
         - DB2: introspect ON DELETE/UPDATE FK clauses
         - DB2 WARNING: the default for on_delete/on_update is now 'NO ACTION'
           not 'CASCADE', the default for is_deferrable is still 1 because DB2
-          does not have deferrable constraints
+          does not have deferrable constraints.
         - SQLite: introspect ON DELETE/UPDATE and DEFERRABLE FK clauses
         - SQLite WARNING: the default for on_delete/on_update is now 'NO ACTION'
           not 'CASCADE', and the default for is_deferrable is now 0 not 1.
index 0915574..90a09aa 100644 (file)
@@ -119,7 +119,7 @@ sub _dbh_tables {
 sub _supports_db_schema { 1 }
 
 # Returns an array of table objects
-sub _tables_list { 
+sub _tables_list {
     my ($self, $opts) = (shift, shift);
 
     my @tables;
@@ -267,7 +267,7 @@ sub _table_columns {
 }
 
 # Returns arrayref of pk col names
-sub _table_pk_info { 
+sub _table_pk_info {
     my ($self, $table) = @_;
 
     return [] if $self->_disable_pk_detection;
@@ -397,6 +397,14 @@ sub _table_fk_info {
 
     my %rels;
 
+    my @rules = (
+        'CASCADE',
+        'RESTRICT',
+        'SET NULL',
+        'NO ACTION',
+        'SET DEFAULT',
+    );
+
     my $i = 1; # for unnamed rels, which hopefully have only 1 column ...
     REL: while(my $raw_rel = $sth->fetchrow_arrayref) {
         my $uk_scm  = $raw_rel->[1];
@@ -407,6 +415,17 @@ sub _table_fk_info {
         my $key_seq = $raw_rel->[8] - 1;
         my $relid   = ($raw_rel->[11] || ( "__dcsld__" . $i++ ));
 
+        my $update_rule = $raw_rel->[9];
+        my $delete_rule = $raw_rel->[10];
+
+        $update_rule = $rules[$update_rule] if defined $update_rule;
+        $delete_rule = $rules[$delete_rule] if defined $delete_rule;
+
+        my $is_deferrable = $raw_rel->[13];
+
+        ($is_deferrable = $is_deferrable == 7 ? 0 : 1)
+            if defined $is_deferrable;
+
         foreach my $var ($uk_scm, $uk_tbl, $uk_col, $fk_scm, $fk_col, $relid) {
             $var =~ s/[\Q$self->{quote_char}\E]//g if defined $var;
         }
@@ -417,7 +436,7 @@ sub _table_fk_info {
             next REL;
         }
 
-        $rels{$relid}{tbl} = DBIx::Class::Schema::Loader::Table->new(
+        $rels{$relid}{tbl} ||= DBIx::Class::Schema::Loader::Table->new(
             loader => $self,
             name   => $uk_tbl,
             schema => $uk_scm,
@@ -425,7 +444,11 @@ sub _table_fk_info {
                 ignore_schema => 1
             )),
         );
-        
+
+        $rels{$relid}{attrs}{on_delete}     = $delete_rule if $delete_rule;
+        $rels{$relid}{attrs}{on_update}     = $update_rule if $update_rule;
+        $rels{$relid}{attrs}{is_deferrable} = $is_deferrable if defined $is_deferrable;
+
         # Add this data IN ORDER
         $rels{$relid}{rcols}[$key_seq] = $uk_col;
         $rels{$relid}{lcols}[$key_seq] = $fk_col;
@@ -438,6 +461,12 @@ sub _table_fk_info {
             remote_columns => [ grep defined, @{ $rels{$relid}{rcols} } ],
             local_columns  => [ grep defined, @{ $rels{$relid}{lcols} } ],
             remote_table   => $rels{$relid}->{tbl},
+            (exists $rels{$relid}{attrs} ?
+                (attrs => $rels{$relid}{attrs})
+                :
+                ()
+            ),
+            _constraint_name => $relid,
         });
     }
 
@@ -563,7 +592,7 @@ sub _dbh_type_info_type_name {
     # We wrap it in a try block for MSSQL+DBD::Sybase, which can have issues.
     # TODO investigate further
     my $type_info = try { $self->dbh->type_info($type_num) };
-    
+
     return $type_info ? $type_info->{TYPE_NAME} : undef;
 }
 
index e6ef0a9..3b24d68 100644 (file)
@@ -78,6 +78,32 @@ sub _filter_tables {
     return $self->next::method(@_);
 }
 
+sub _table_fk_info {
+    my $self = shift;
+    my ($table) = @_;
+
+    my $rels = $self->next::method(@_);
+
+    my $deferrable_sth = $self->dbh->prepare_cached(<<'EOF');
+select deferrable from all_constraints
+where owner = ? and table_name = ? and constraint_name = ?
+EOF
+
+    foreach my $rel (@$rels) {
+        # Oracle does not have update rules
+        $rel->{attrs}{on_update} = 'NO ACTION';;
+
+        # DBD::Oracle's foreign_key_info does not return DEFERRABILITY, so we get it ourselves
+        my ($deferrable) = $self->dbh->selectrow_array(
+            $deferrable_sth, undef, $table->schema, $table->name, $rel->{_constraint_name}
+        );
+
+        $rel->{attrs}{is_deferrable} = $deferrable && $deferrable =~ /^DEFERRABLE/i ? 1 : 0;
+    }
+
+    return $rels;
+}
+
 sub _table_uniq_info {
     my ($self, $table) = @_;
 
index 566a5de..b7142b2 100644 (file)
@@ -25,7 +25,7 @@ my $auto_inc_cb = sub {
     my ($table, $col) = @_;
     return (
         qq{ CREATE SEQUENCE ${table}_${col}_seq START WITH 1 INCREMENT BY 1},
-        qq{ 
+        qq{
             CREATE OR REPLACE TRIGGER ${table}_${col}_trigger
             BEFORE INSERT ON ${table}
             FOR EACH ROW
@@ -45,9 +45,12 @@ my $tester = dbixcsl_common_tests->new(
     vendor      => 'Oracle',
     auto_inc_pk => 'INTEGER NOT NULL PRIMARY KEY',
     auto_inc_cb => $auto_inc_cb,
-    auto_inc_drop_cb => $auto_inc_drop_cb, 
+    auto_inc_drop_cb => $auto_inc_drop_cb,
     preserve_case_mode_is_exclusive => 1,
     quote_char                      => '"',
+    default_is_deferrable => 0,
+    default_on_delete_clause => 'NO ACTION',
+    default_on_update_clause => 'NO ACTION',
     dsn         => $dsn,
     user        => $user,
     password    => $password,
@@ -155,7 +158,7 @@ my $tester = dbixcsl_common_tests->new(
     },
     extra => {
         create => [
-            q{ 
+            q{
                 CREATE TABLE oracle_loader_test1 (
                     id NUMBER(11),
                     value VARCHAR2(100)
@@ -163,9 +166,23 @@ my $tester = dbixcsl_common_tests->new(
             },
             q{ COMMENT ON TABLE oracle_loader_test1 IS 'oracle_loader_test1 table comment' },
             q{ COMMENT ON COLUMN oracle_loader_test1.value IS 'oracle_loader_test1.value column comment' },
+            # 4 through 8 are used for the multi-schema tests
+            q{
+                create table oracle_loader_test9 (
+                    id int primary key
+                )
+            },
+            q{
+                create table oracle_loader_test10 (
+                    id int primary key,
+                    nine_id int,
+                    foreign key (nine_id) references oracle_loader_test9(id)
+                        on delete set null deferrable
+                )
+            },
         ],
-        drop  => [qw/oracle_loader_test1/],
-        count => 3 + 30 * 2,
+        drop  => [qw/oracle_loader_test1 oracle_loader_test9 oracle_loader_test10/],
+        count => 7 + 30 * 2,
         run   => sub {
             my ($monikers, $classes);
             ($schema, $monikers, $classes) = @_;
@@ -192,6 +209,19 @@ my $tester = dbixcsl_common_tests->new(
             like $code, qr/^=head2 value\n\n(.+:.+\n)+\noracle_loader_test1\.value column comment\n\n/m,
                 'column comment and attrs';
 
+            # test on delete/update fk clause introspection
+            ok ((my $rel_info = $schema->source('OracleLoaderTest10')->relationship_info('nine')),
+                'got rel info');
+
+            is $rel_info->{attrs}{on_delete}, 'SET NULL',
+                'ON DELETE clause introspected correctly';
+
+            is $rel_info->{attrs}{on_update}, 'NO ACTION',
+                'ON UPDATE clause set to NO ACTION by default';
+
+            is $rel_info->{attrs}{is_deferrable}, 1,
+                'DEFERRABLE clause introspected correctly';
+
             SKIP: {
                 skip 'Set the DBICTEST_ORA_EXTRAUSER_DSN, _USER and _PASS environment variables to run the cross-schema relationship tests', 6 * 2
                     unless $ENV{DBICTEST_ORA_EXTRAUSER_DSN};
index e00920b..13c8002 100644 (file)
@@ -791,13 +791,17 @@ qr/\n__PACKAGE__->load_components\("TestSchemaComponent", "\+TestSchemaComponent
 
         my $default_on_clause = $self->{default_on_clause} || 'CASCADE';
 
+        my $default_on_delete_clause = $self->{default_on_delete_clause} || $default_on_clause;
+
         is try { $rsobj4->result_source->relationship_info('fkid_singular')->{attrs}{on_delete} },
-            $default_on_clause,
-            "on_delete is $default_on_clause on belongs_to by default";
+            $default_on_delete_clause,
+            "on_delete is $default_on_delete_clause on belongs_to by default";
+
+        my $default_on_update_clause = $self->{default_on_update_clause} || $default_on_clause;
 
         is try { $rsobj4->result_source->relationship_info('fkid_singular')->{attrs}{on_update} },
-            $default_on_clause,
-            "on_update is $default_on_clause on belongs_to by default";
+            $default_on_update_clause,
+            "on_update is $default_on_update_clause on belongs_to by default";
 
         my $default_is_deferrable = $self->{default_is_deferrable};