Skip to content

Commit fbe181b

Browse files
authored
Merge pull request rsim#2741 from yahonda/fix-dbms-metadata-multi-stmt-split
Split multi-statement TABLE DDL CLOBs in the DBMS_METADATA dump path
2 parents 2e5ffee + 3c840ca commit fbe181b

2 files changed

Lines changed: 105 additions & 21 deletions

File tree

lib/active_record/connection_adapters/oracle_enhanced/structure_dump/dbms_metadata.rb

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module DbmsMetadata # :nodoc:
2626

2727
private
2828
def dbms_metadata_structure_dump
29-
dbms_metadata_with_transforms do
29+
dbms_metadata_with_transforms(sql_terminator: true) do
3030
structure = []
3131
table_names = list_schema_objects("TABLE", non_mview_only: true)
3232
skip_indexes = constraint_backed_index_names
@@ -41,10 +41,12 @@ def dbms_metadata_structure_dump
4141
names.each do |name|
4242
next if object_type == "INDEX" && skip_indexes.include?(name.upcase)
4343
ddl = dbms_metadata_get_ddl(object_type.tr(" ", "_"), name)
44-
if ddl && object_type == "INDEX" && invisible_indexes.include?(name.upcase)
45-
ddl = ensure_invisible_keyword(ddl)
44+
next unless ddl
45+
statements = split_dbms_metadata_sql_ddl(ddl)
46+
if object_type == "INDEX" && invisible_indexes.include?(name.upcase)
47+
statements = statements.map { |stmt| ensure_invisible_keyword(stmt) }
4648
end
47-
structure << ddl if ddl
49+
structure.concat(statements)
4850
end
4951
end
5052

@@ -66,7 +68,7 @@ def dbms_metadata_structure_dump
6668

6769
fk_statements = table_names.flat_map do |table_name|
6870
fk_ddl = dbms_metadata_get_dependent_ddl("REF_CONSTRAINT", table_name)
69-
fk_ddl ? split_dbms_metadata_ddl(fk_ddl) : []
71+
fk_ddl ? split_dbms_metadata_sql_ddl(fk_ddl) : []
7072
end
7173

7274
join_with_statement_token(structure) << join_with_statement_token(fk_statements)
@@ -94,8 +96,8 @@ def dbms_metadata_structure_dump_synonyms
9496
end
9597
end
9698

97-
def dbms_metadata_with_transforms
98-
configure_dbms_metadata_transforms
99+
def dbms_metadata_with_transforms(sql_terminator: false)
100+
configure_dbms_metadata_transforms(sql_terminator: sql_terminator)
99101
yield
100102
ensure
101103
reset_dbms_metadata_transforms
@@ -179,14 +181,22 @@ def dbms_metadata_structure_dump_column_comments(table_name)
179181

180182
# Suppress installation-specific output, in spirit of
181183
# `pg_dump --schema-only --no-owner --no-tablespaces`.
182-
def configure_dbms_metadata_transforms
184+
# When `sql_terminator: true` is requested, also tell Oracle
185+
# to append a SQL terminator (`;`) to each statement so the
186+
# caller can split a multi-statement TABLE DDL CLOB on the
187+
# boundary; this is only safe for the SQL DDL path (TABLE /
188+
# SEQUENCE / VIEW / INDEX). The PL/SQL DDL path leaves
189+
# SQLTERMINATOR at the default FALSE because PL/SQL bodies
190+
# contain `;` characters internally (`END;`).
191+
def configure_dbms_metadata_transforms(sql_terminator: false)
183192
execute(<<~SQL)
184193
BEGIN
185194
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'STORAGE', FALSE);
186195
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'TABLESPACE', FALSE);
187196
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'SEGMENT_ATTRIBUTES', FALSE);
188197
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'EMIT_SCHEMA', FALSE);
189198
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'REF_CONSTRAINTS', FALSE);
199+
DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'SQLTERMINATOR', #{sql_terminator ? "TRUE" : "FALSE"});
190200
END;
191201
SQL
192202
end
@@ -246,6 +256,19 @@ def split_dbms_metadata_ddl(ddl)
246256
return [] if ddl.nil?
247257
ddl.split(/\n\s*\n/).map(&:strip).reject(&:empty?)
248258
end
259+
260+
# `GET_DDL('TABLE', t)` can return more than one SQL statement
261+
# in a single CLOB when the table has a UNIQUE constraint
262+
# backed by a separately-named index (Oracle emits the
263+
# `CREATE TABLE`, the `CREATE UNIQUE INDEX`, and the
264+
# `ALTER TABLE ... ADD CONSTRAINT ... USING INDEX ...` as a
265+
# group). With `SQLTERMINATOR=TRUE` each statement ends in
266+
# `;`, so split on the terminator to surface each statement
267+
# to the structure-dump output as its own entry.
268+
def split_dbms_metadata_sql_ddl(ddl)
269+
return [] if ddl.nil?
270+
ddl.split(/;\s*(?:\n|\z)/).map(&:strip).reject(&:empty?)
271+
end
249272
end
250273
end
251274
end

spec/active_record/connection_adapters/oracle_enhanced/dbms_metadata_structure_dump_spec.rb

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,6 @@
157157
stmt.match?(/\AALTER\s+INDEX\s+"?IX_DBMS_META_INV_UNIQ"?\s+INVISIBLE\s*\z/i)
158158
end
159159
expect(alter_stmt).not_to be_nil
160-
161-
# And the standalone CREATE INDEX should NOT be emitted (the
162-
# constraint inline already created the index).
163-
standalone = dump.split("\n\n/\n\n").select do |stmt|
164-
stmt.match?(/\ACREATE\s+(?:UNIQUE\s+)?INDEX\s+"?IX_DBMS_META_INV_UNIQ"?/i)
165-
end
166-
expect(standalone).to be_empty
167160
ensure
168161
schema_define { drop_table :test_dbms_meta_inv_uniq, if_exists: true }
169162
end
@@ -232,21 +225,89 @@
232225
expect(dump).not_to match(/PCTFREE\s+/i)
233226
end
234227

235-
it "does not emit a standalone CREATE INDEX for a UNIQUE constraint's backing index" do
228+
it "splits a UNIQUE-constraint-with-named-backing-index into separate CREATE INDEX and ADD CONSTRAINT statements" do
236229
schema_define do
237230
create_table :test_dbms_metadata_posts, force: true do |t|
238231
t.string :email
239232
end
240233
add_index :test_dbms_metadata_posts, :email, unique: true, name: "ix_test_dbms_metadata_email"
241234
end
242235
dump = @conn.structure_dump
243-
# `CONSTRAINTS=TRUE` already inlines the unique constraint (and its
244-
# backing index) into the CREATE TABLE DDL. Emitting a separate
245-
# `CREATE UNIQUE INDEX` for the same name would duplicate it.
246-
standalone_index_stmts = dump.split("\n\n/\n\n").select do |stmt|
236+
stmts = dump.split("\n\n/\n\n")
237+
238+
# `GET_DDL('TABLE', ...)` returns CREATE TABLE, CREATE UNIQUE INDEX,
239+
# and ALTER TABLE ... ADD CONSTRAINT ... USING INDEX as one CLOB
240+
# when the UNIQUE constraint references a named backing index.
241+
# `SQLTERMINATOR=TRUE` plus `split_dbms_metadata_sql_ddl` splits
242+
# them so each one is a separate dump entry; `structure_load`
243+
# would otherwise feed the whole CLOB as one statement and Oracle
244+
# would reject it with ORA-00922.
245+
create_index_stmts = stmts.select do |stmt|
247246
stmt.match?(/\ACREATE\s+UNIQUE\s+INDEX\s+"?IX_TEST_DBMS_METADATA_EMAIL"?/i)
248247
end
249-
expect(standalone_index_stmts).to be_empty
248+
expect(create_index_stmts.size).to eq(1)
249+
250+
add_constraint_stmts = stmts.select do |stmt|
251+
stmt.match?(/\AALTER\s+TABLE\s+"?TEST_DBMS_METADATA_POSTS"?\s+ADD\s+CONSTRAINT\s+"?IX_TEST_DBMS_METADATA_EMAIL"?\s+UNIQUE/i)
252+
end
253+
expect(add_constraint_stmts.size).to eq(1)
254+
end
255+
256+
# Round-trip regression for the same table shape: dump → drop → load
257+
# used to fail with ORA-00922 because the multi-statement `GET_DDL`
258+
# CLOB was sent to Oracle as a single statement.
259+
it "round-trips a UNIQUE-with-named-backing-index table through structure_dump/structure_load" do
260+
require "tempfile"
261+
schema_define do
262+
create_table :test_dbms_metadata_posts, force: true do |t|
263+
t.string :email
264+
end
265+
add_index :test_dbms_metadata_posts, :email, unique: true, name: "ix_test_dbms_metadata_email"
266+
end
267+
268+
temp_file = Tempfile.create(["oracle_enhanced_roundtrip", ".sql"]).path
269+
config = ActiveRecord::Base.connection_db_config.configuration_hash
270+
271+
begin
272+
ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, temp_file)
273+
ActiveRecord::Tasks::DatabaseTasks.drop(config)
274+
ActiveRecord::Tasks::DatabaseTasks.structure_load(config, temp_file)
275+
expect(@conn.table_exists?(:test_dbms_metadata_posts)).to be(true)
276+
ensure
277+
File.unlink(temp_file) if File.exist?(temp_file)
278+
end
279+
end
280+
281+
# Round-trip regression for foreign keys: with SQLTERMINATOR=TRUE
282+
# the dependent FK DDL also ends with ';', so the dump must run it
283+
# through `split_dbms_metadata_sql_ddl` to strip the trailing
284+
# terminator. Otherwise the load raises ORA-00911 on the trailing ';'.
285+
it "round-trips foreign-key constraints through structure_dump/structure_load" do
286+
require "tempfile"
287+
schema_define do
288+
create_table :test_dbms_metadata_posts, force: true do |t|
289+
t.string :title
290+
end
291+
create_table :test_dbms_metadata_children, force: true do |t|
292+
t.integer :test_dbms_metadata_post_id
293+
end
294+
add_foreign_key :test_dbms_metadata_children, :test_dbms_metadata_posts,
295+
column: :test_dbms_metadata_post_id, name: "fk_dbms_meta_rt"
296+
end
297+
298+
temp_file = Tempfile.create(["oracle_enhanced_fk_rt", ".sql"]).path
299+
config = ActiveRecord::Base.connection_db_config.configuration_hash
300+
301+
begin
302+
ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, temp_file)
303+
ActiveRecord::Tasks::DatabaseTasks.drop(config)
304+
ActiveRecord::Tasks::DatabaseTasks.structure_load(config, temp_file)
305+
expect(@conn.table_exists?(:test_dbms_metadata_children)).to be(true)
306+
fk = @conn.foreign_keys(:test_dbms_metadata_children).find { |f| f.name == "fk_dbms_meta_rt" }
307+
expect(fk).not_to be_nil
308+
ensure
309+
File.unlink(temp_file) if File.exist?(temp_file)
310+
end
250311
end
251312
end
252313

0 commit comments

Comments
 (0)