Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

schema dump improvements from aake

Foreign key definitions now work with CPKs
Bug fixes for partial composite foreign key support when CFKs are not being used. Added CFK spec.
Bugfix: Don't generate materialized view log definitions
Don't try to drop materialized view logs as tables
Bugfix: don't generate duplicate unique keys
Bug fix: Added comma before pk dump
Dump indexes before dumping unique keys
Changed the unique key spec to match an ALTER TABLE statement rather than a table inline definition
Improved composite foreign key support. add_foreign_key() now accepts an array of columns and the schema dumper has been updated accordingly.
Bug fix for when structure dumping foreign keys and reference column is not called 'id'
  • Loading branch information...
commit 59954fa755527a19993445e4053ba50b8957b0b0 1 parent 087d54b
@aake aake authored rsim committed
View
18 lib/active_record/connection_adapters/oracle_enhanced_adapter.rb
@@ -1330,6 +1330,7 @@ def structure_dump #:nodoc:
select_values("select table_name from all_tables t
where owner = sys_context('userenv','session_user') and secondary='N'
and not exists (select mv.mview_name from all_mviews mv where mv.owner = t.owner and mv.mview_name = t.table_name)
+ and not exists (select mvl.log_table from all_mview_logs mvl where mvl.log_owner = t.owner and mvl.log_table = t.table_name)
order by 1").each do |table_name|
virtual_columns = virtual_columns_for(table_name)
ddl = "CREATE#{ ' GLOBAL TEMPORARY' if temporary_table?(table_name)} TABLE \"#{table_name}\" (\n"
@@ -1346,10 +1347,11 @@ def structure_dump #:nodoc:
end
end
ddl << cols.join(",\n ")
- ddl << structure_dump_constraints(table_name)
+ ddl << structure_dump_primary_key(table_name)
ddl << "\n)"
structure << ddl
structure << structure_dump_indexes(table_name)
+ structure << structure_dump_unique_keys(table_name)
end
join_with_statement_token(structure) << structure_dump_fk_constraints
@@ -1384,11 +1386,6 @@ def structure_dump_virtual_column(column, data_default) #:nodoc:
col << " GENERATED ALWAYS AS (#{data_default}) VIRTUAL"
end
- def structure_dump_constraints(table) #:nodoc:
- out = [structure_dump_primary_key(table), structure_dump_unique_keys(table)].flatten.compact
- out.length > 0 ? ",\n#{out.join(",\n")}" : ''
- end
-
def structure_dump_primary_key(table) #:nodoc:
opts = {:name => '', :cols => []}
pks = select_all(<<-SQL, "Primary Keys")
@@ -1404,7 +1401,7 @@ def structure_dump_primary_key(table) #:nodoc:
opts[:name] = row['constraint_name']
opts[:cols][row['position']-1] = row['column_name']
end
- opts[:cols].length > 0 ? " CONSTRAINT #{opts[:name]} PRIMARY KEY (#{opts[:cols].join(',')})" : nil
+ opts[:cols].length > 0 ? ",\n CONSTRAINT #{opts[:name]} PRIMARY KEY (#{opts[:cols].join(',')})" : ''
end
def structure_dump_unique_keys(table) #:nodoc:
@@ -1423,7 +1420,7 @@ def structure_dump_unique_keys(table) #:nodoc:
keys[uk['constraint_name']][uk['position']-1] = uk['column_name']
end
keys.map do |k,v|
- " CONSTRAINT #{k} UNIQUE (#{v.join(',')})"
+ "ALTER TABLE #{table.upcase} ADD CONSTRAINT #{k} UNIQUE (#{v.join(',')})"
end
end
@@ -1447,9 +1444,7 @@ def structure_dump_fk_constraints #:nodoc:
fks = select_all("select table_name from all_tables where owner = sys_context('userenv','session_user') order by 1").map do |table|
if respond_to?(:foreign_keys) && (foreign_keys = foreign_keys(table["table_name"])).any?
foreign_keys.map do |fk|
- column = fk.options[:column] || "#{fk.to_table.to_s.singularize}_id"
- constraint_name = foreign_key_constraint_name(fk.from_table, column, fk.options)
- sql = "ALTER TABLE #{quote_table_name(fk.from_table)} ADD CONSTRAINT #{quote_column_name(constraint_name)} "
+ sql = "ALTER TABLE #{quote_table_name(fk.from_table)} ADD CONSTRAINT #{quote_column_name(fk.options[:name])} "
sql << "#{foreign_key_definition(fk.to_table, fk.options)}"
end
end
@@ -1508,6 +1503,7 @@ def structure_drop #:nodoc:
select_values("select table_name from all_tables t
where owner = sys_context('userenv','session_user') and secondary='N'
and not exists (select mv.mview_name from all_mviews mv where mv.owner = t.owner and mv.mview_name = t.table_name)
+ and not exists (select mvl.log_table from all_mview_logs mvl where mvl.log_owner = t.owner and mvl.log_table = t.table_name)
order by 1").each do |table|
statements << "DROP TABLE \"#{table}\" CASCADE CONSTRAINTS"
end
View
20 lib/active_record/connection_adapters/oracle_enhanced_schema_dumper.rb
@@ -56,14 +56,22 @@ def foreign_keys(table_name, stream)
add_foreign_key_statements = foreign_keys.map do |foreign_key|
statement_parts = [ ('add_foreign_key ' + foreign_key.from_table.inspect) ]
statement_parts << foreign_key.to_table.inspect
- statement_parts << (':name => ' + foreign_key.options[:name].inspect)
- if foreign_key.options[:column] != "#{foreign_key.to_table.singularize}_id"
- statement_parts << (':column => ' + foreign_key.options[:column].inspect)
- end
- if foreign_key.options[:primary_key] != 'id'
- statement_parts << (':primary_key => ' + foreign_key.options[:primary_key].inspect)
+ if foreign_key.options[:columns].size == 1
+ column = foreign_key.options[:columns].first
+ if column != "#{foreign_key.to_table.singularize}_id"
+ statement_parts << (':column => ' + column.inspect)
+ end
+
+ if foreign_key.options[:references].first != 'id'
+ statement_parts << (':primary_key => ' + foreign_key.options[:primary_key].inspect)
+ end
+ else
+ statement_parts << (':columns => ' + foreign_key.options[:columns].inspect)
end
+
+ statement_parts << (':name => ' + foreign_key.options[:name].inspect)
+
unless foreign_key.options[:dependent].blank?
statement_parts << (':dependent => ' + foreign_key.options[:dependent].inspect)
end
View
60 lib/active_record/connection_adapters/oracle_enhanced_schema_statements_ext.rb
@@ -52,31 +52,51 @@ def add_primary_key_trigger(table_name, options)
# people_best_friend_id_fk FOREIGN KEY (best_friend_id) REFERENCES people (id)
# ON DELETE SET NULL
#
+ # ==== Creating a composite foreign key
+ # add_foreign_key(:comments, :posts, :columns => ['post_id', 'author_id'], :name => 'comments_post_fk')
+ # generates
+ # ALTER TABLE comments ADD CONSTRAINT
+ # comments_post_fk FOREIGN KEY (post_id, author_id) REFERENCES posts (post_id, author_id)
+ #
# === Supported options
# [:column]
# Specify the column name on the from_table that references the to_table. By default this is guessed
# to be the singular name of the to_table with "_id" suffixed. So a to_table of :posts will use "post_id"
# as the default <tt>:column</tt>.
+ # [:columns]
+ # An array of column names when defining composite foreign keys. An alias of <tt>:column</tt> provided for improved readability.
# [:primary_key]
# Specify the column name on the to_table that is referenced by this foreign key. By default this is
- # assumed to be "id".
+ # assumed to be "id". Ignored when defining composite foreign keys.
# [:name]
# Specify the name of the foreign key constraint. This defaults to use from_table and foreign key column.
# [:dependent]
# If set to <tt>:delete</tt>, the associated records in from_table are deleted when records in to_table table are deleted.
# If set to <tt>:nullify</tt>, the foreign key column is set to +NULL+.
def add_foreign_key(from_table, to_table, options = {})
- column = options[:column] || "#{to_table.to_s.singularize}_id"
- constraint_name = foreign_key_constraint_name(from_table, column, options)
+ columns = options[:column] || options[:columns] || "#{to_table.to_s.singularize}_id"
+ constraint_name = foreign_key_constraint_name(from_table, columns, options)
sql = "ALTER TABLE #{quote_table_name(from_table)} ADD CONSTRAINT #{quote_column_name(constraint_name)} "
sql << foreign_key_definition(to_table, options)
execute sql
end
def foreign_key_definition(to_table, options = {}) #:nodoc:
- column = options[:column] || "#{to_table.to_s.singularize}_id"
- primary_key = options[:primary_key] || "id"
- sql = "FOREIGN KEY (#{quote_column_name(column)}) REFERENCES #{quote_table_name(to_table)}(#{primary_key})"
+ columns = (options[:column] || options[:columns]).to_a
+
+ if columns.size > 1
+ # composite foreign key
+ columns_sql = columns.map {|c| quote_column_name(c)}.join(',')
+ references = options[:references] || columns
+ references_sql = references.map {|c| quote_column_name(c)}.join(',')
+ else
+ columns_sql = quote_column_name(columns.first || "#{to_table.to_s.singularize}_id")
+ references = options[:references] ? options[:references].first : nil
+ references_sql = quote_column_name(options[:primary_key] || references || "id")
+ end
+
+ sql = "FOREIGN KEY (#{columns_sql}) REFERENCES #{quote_table_name(to_table)}(#{references_sql})"
+
case options[:dependent]
when :nullify
sql << " ON DELETE SET NULL"
@@ -106,9 +126,12 @@ def remove_foreign_key(from_table, options)
private
- def foreign_key_constraint_name(table_name, column, options = {})
- constraint_name = original_name = options[:name] || "#{table_name}_#{column}_fk"
+ def foreign_key_constraint_name(table_name, columns, options = {})
+ columns = columns.to_a
+ constraint_name = original_name = options[:name] || "#{table_name}_#{columns.join('_')}_fk"
+
return constraint_name if constraint_name.length <= OracleEnhancedAdapter::IDENTIFIER_MAX_LENGTH
+
# leave just first three letters from each word
constraint_name = constraint_name.split('_').map{|w| w[0,3]}.join('_')
# generate unique name using hash function
@@ -128,7 +151,7 @@ def foreign_keys(table_name) #:nodoc:
fk_info = select_all(<<-SQL, 'Foreign Keys')
SELECT r.table_name to_table
- ,rc.column_name primary_key
+ ,rc.column_name references_column
,cc.column_name
,c.constraint_name name
,c.delete_rule
@@ -144,18 +167,27 @@ def foreign_keys(table_name) #:nodoc:
AND rc.owner = r.owner
AND rc.constraint_name = r.constraint_name
AND rc.position = cc.position
+ ORDER BY name, to_table, column_name, references_column
SQL
+ fks = {}
+
fk_info.map do |row|
- options = {:column => oracle_downcase(row['column_name']), :name => oracle_downcase(row['name']),
- :primary_key => oracle_downcase(row['primary_key'])}
+ name = oracle_downcase(row['name'])
+ fks[name] ||= { :columns => [], :to_table => oracle_downcase(row['to_table']), :references => [] }
+ fks[name][:columns] << oracle_downcase(row['column_name'])
+ fks[name][:references] << oracle_downcase(row['references_column'])
case row['delete_rule']
when 'CASCADE'
- options[:dependent] = :delete
+ fks[name][:dependent] = :delete
when 'SET NULL'
- options[:dependent] = :nullify
+ fks[name][:dependent] = :nullify
end
- OracleEnhancedForeignKeyDefinition.new(table_name, oracle_downcase(row['to_table']), options)
+ end
+
+ fks.map do |k, v|
+ options = {:name => k, :columns => v[:columns], :references => v[:references], :dependent => v[:dependent]}
+ OracleEnhancedForeignKeyDefinition.new(table_name, v[:to_table], options)
end
end
View
50 spec/active_record/connection_adapters/oracle_enhanced_adapter_structure_dumper_spec.rb
@@ -29,6 +29,12 @@ class ::TestPost < ActiveRecord::Base
@conn.execute "DROP TRIGGER test_post_trigger" rescue nil
@conn.execute "DROP TYPE TEST_TYPE" rescue nil
@conn.execute "DROP TABLE bars" rescue nil
+ @conn.execute "ALTER TABLE foos drop CONSTRAINT UK_BAZ" rescue nil
+ @conn.execute "ALTER TABLE foos drop CONSTRAINT UK_FOOZ_BAZ" rescue nil
+ @conn.execute "ALTER TABLE foos drop column fooz_id" rescue nil
+ @conn.execute "ALTER TABLE foos drop column baz_id" rescue nil
+ @conn.execute "ALTER TABLE test_posts drop column fooz_id" rescue nil
+ @conn.execute "ALTER TABLE test_posts drop column baz_id" rescue nil
end
it "should dump single primary key" do
@@ -60,6 +66,48 @@ class ::TestPost < ActiveRecord::Base
dump.split('\n').length.should == 1
dump.should =~ /ALTER TABLE \"?TEST_POSTS\"? ADD CONSTRAINT \"?FK_TEST_POST_FOO\"? FOREIGN KEY \(\"?FOO_ID\"?\) REFERENCES \"?FOOS\"?\(\"?ID\"?\)/i
end
+
+ it "should dump foreign keys when reference column name is not 'id'" do
+ @conn.add_column :foos, :baz_id, :integer
+
+ @conn.execute <<-SQL
+ ALTER TABLE FOOS
+ ADD CONSTRAINT UK_BAZ UNIQUE (BAZ_ID)
+ SQL
+
+ @conn.add_column :test_posts, :baz_id, :integer
+
+ @conn.execute <<-SQL
+ ALTER TABLE TEST_POSTS
+ ADD CONSTRAINT fk_test_post_baz FOREIGN KEY (baz_id) REFERENCES foos(baz_id)
+ SQL
+
+ dump = ActiveRecord::Base.connection.structure_dump_fk_constraints
+ dump.split('\n').length.should == 1
+ dump.should =~ /ALTER TABLE \"?TEST_POSTS\"? ADD CONSTRAINT \"?FK_TEST_POST_BAZ\"? FOREIGN KEY \(\"?BAZ_ID\"?\) REFERENCES \"?FOOS\"?\(\"?BAZ_ID\"?\)/i
+ end
+
+ it "should dump composite foreign keys" do
+ @conn.add_column :foos, :fooz_id, :integer
+ @conn.add_column :foos, :baz_id, :integer
+
+ @conn.execute <<-SQL
+ ALTER TABLE FOOS
+ ADD CONSTRAINT UK_FOOZ_BAZ UNIQUE (BAZ_ID,FOOZ_ID)
+ SQL
+
+ @conn.add_column :test_posts, :fooz_id, :integer
+ @conn.add_column :test_posts, :baz_id, :integer
+
+ @conn.execute <<-SQL
+ ALTER TABLE TEST_POSTS
+ ADD CONSTRAINT fk_test_post_fooz_baz FOREIGN KEY (baz_id,fooz_id) REFERENCES foos(baz_id,fooz_id)
+ SQL
+
+ dump = ActiveRecord::Base.connection.structure_dump_fk_constraints
+ dump.split('\n').length.should == 1
+ dump.should =~ /ALTER TABLE \"?TEST_POSTS\"? ADD CONSTRAINT \"?FK_TEST_POST_FOOZ_BAZ\"? FOREIGN KEY \(\"?BAZ_ID\"?\,\"?FOOZ_ID\"?\) REFERENCES \"?FOOS\"?\(\"?BAZ_ID\"?\,\"?FOOZ_ID\"?\)/i
+ end
it "should not error when no foreign keys are present" do
dump = ActiveRecord::Base.connection.structure_dump_fk_constraints
@@ -108,7 +156,7 @@ class ::TestPost < ActiveRecord::Base
add CONSTRAINT uk_foo_foo_id UNIQUE (foo, foo_id)
SQL
dump = ActiveRecord::Base.connection.structure_dump_unique_keys("test_posts")
- dump.should == [" CONSTRAINT UK_FOO_FOO_ID UNIQUE (FOO,FOO_ID)"]
+ dump.should == ["ALTER TABLE TEST_POSTS ADD CONSTRAINT UK_FOO_FOO_ID UNIQUE (FOO,FOO_ID)"]
dump = ActiveRecord::Base.connection.structure_dump
dump.should =~ /CONSTRAINT UK_FOO_FOO_ID UNIQUE \(FOO,FOO_ID\)/
View
20 spec/active_record/connection_adapters/oracle_enhanced_schema_dump_spec.rb
@@ -151,7 +151,8 @@ def drop_test_posts_table
after(:each) do
schema_define do
- remove_foreign_key :test_comments, :test_posts
+ remove_foreign_key :test_comments, :test_posts rescue nil
+ remove_foreign_key :test_comments, :name => 'comments_posts_baz_fooz_fk' rescue nil
end
end
after(:all) do
@@ -210,6 +211,23 @@ def drop_test_posts_table
standard_dump(:ignore_tables => [ /test_posts/i ]).should =~ /add_foreign_key "test_comments"/
end
+ it "should include composite foreign keys" do
+ schema_define do
+ add_column :test_posts, :baz_id, :integer
+ add_column :test_posts, :fooz_id, :integer
+
+ execute <<-SQL
+ ALTER TABLE TEST_POSTS
+ ADD CONSTRAINT UK_FOOZ_BAZ UNIQUE (BAZ_ID,FOOZ_ID)
+ SQL
+
+ add_column :test_comments, :baz_id, :integer
+ add_column :test_comments, :fooz_id, :integer
+
+ add_foreign_key :test_comments, :test_posts, :columns => ["baz_id", "fooz_id"], :name => 'comments_posts_baz_fooz_fk'
+ end
+ standard_dump.should =~ /add_foreign_key "test_comments", "test_posts", :columns => \["baz_id", "fooz_id"\], :name => "comments_posts_baz_fooz_fk"/
+ end
end
describe "synonyms" do
View
43 spec/active_record/connection_adapters/oracle_enhanced_schema_spec.rb
@@ -489,6 +489,49 @@ class ::TestComment < ActiveRecord::Base
TestPost.delete(p.id)
TestComment.find_by_id(c.id).test_post_id.should be_nil
end
+
+ it "should add a composite foreign key" do
+ schema_define do
+ add_column :test_posts, :baz_id, :integer
+ add_column :test_posts, :fooz_id, :integer
+
+ execute <<-SQL
+ ALTER TABLE TEST_POSTS
+ ADD CONSTRAINT UK_FOOZ_BAZ UNIQUE (BAZ_ID,FOOZ_ID)
+ SQL
+
+ add_column :test_comments, :baz_id, :integer
+ add_column :test_comments, :fooz_id, :integer
+
+ add_foreign_key :test_comments, :test_posts, :columns => ["baz_id", "fooz_id"]
+ end
+
+ lambda do
+ TestComment.create(:body => "test", :fooz_id => 1, :baz_id => 1)
+ end.should raise_error() {|e| e.message.should =~
+ /ORA-02291.*\.TES_COM_BAZ_ID_FOO_ID_FK/}
+ end
+
+ it "should add a composite foreign key with name" do
+ schema_define do
+ add_column :test_posts, :baz_id, :integer
+ add_column :test_posts, :fooz_id, :integer
+
+ execute <<-SQL
+ ALTER TABLE TEST_POSTS
+ ADD CONSTRAINT UK_FOOZ_BAZ UNIQUE (BAZ_ID,FOOZ_ID)
+ SQL
+
+ add_column :test_comments, :baz_id, :integer
+ add_column :test_comments, :fooz_id, :integer
+
+ add_foreign_key :test_comments, :test_posts, :columns => ["baz_id", "fooz_id"], :name => 'comments_posts_baz_fooz_fk'
+ end
+
+ lambda do
+ TestComment.create(:body => "test", :baz_id => 1, :fooz_id => 1)
+ end.should raise_error() {|e| e.message.should =~ /ORA-02291.*\.COMMENTS_POSTS_BAZ_FOOZ_FK/}
+ end
it "should remove foreign key by table name" do
schema_define do
Please sign in to comment.
Something went wrong with that request. Please try again.