|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require "active_record/connection_adapters/oracle_enhanced/structure_dump" |
| 4 | + |
| 5 | +module ActiveRecord # :nodoc: |
| 6 | + module ConnectionAdapters # :nodoc: |
| 7 | + module OracleEnhanced # :nodoc: |
| 8 | + module StructureDump # :nodoc: |
| 9 | + # DBMS_METADATA-backed implementation of `structure_dump`, |
| 10 | + # `structure_dump_db_stored_code`, and `structure_dump_synonyms`. |
| 11 | + # Selected by `OracleEnhancedAdapter.structure_dump_method`: |
| 12 | + # |
| 13 | + # * `:auto` (default) — `:dbms_metadata` on Oracle 12.1+, otherwise |
| 14 | + # `:data_dictionary`. |
| 15 | + # * `:dbms_metadata` — force the new path; raises `ArgumentError` |
| 16 | + # on pre-12.1 servers (mirrors PR #2576's `identifier_max_length: |
| 17 | + # :long` policy). |
| 18 | + # * `:data_dictionary` — delegate to `super` (the original `ALL_*` |
| 19 | + # implementation). |
| 20 | + # |
| 21 | + # Output shape is informed by mysqldump / pg_dump --schema-only: |
| 22 | + # statements are reloadable on a fresh database, machine-specific |
| 23 | + # noise (storage / tablespace / segment attributes / schema qualifiers) |
| 24 | + # is suppressed, and statements are separated by the same |
| 25 | + # `STATEMENT_TOKEN` the dictionary-views implementation uses so that |
| 26 | + # `execute_structure_dump` can split and apply them. |
| 27 | + module DbmsMetadata # :nodoc: |
| 28 | + def structure_dump |
| 29 | + case resolved_structure_dump_method |
| 30 | + when :dbms_metadata then dbms_metadata_structure_dump |
| 31 | + when :data_dictionary then super |
| 32 | + end |
| 33 | + end |
| 34 | + |
| 35 | + def structure_dump_db_stored_code |
| 36 | + case resolved_structure_dump_method |
| 37 | + when :dbms_metadata then dbms_metadata_structure_dump_db_stored_code |
| 38 | + when :data_dictionary then super |
| 39 | + end |
| 40 | + end |
| 41 | + |
| 42 | + def structure_dump_synonyms |
| 43 | + case resolved_structure_dump_method |
| 44 | + when :dbms_metadata then dbms_metadata_structure_dump_synonyms |
| 45 | + when :data_dictionary then super |
| 46 | + end |
| 47 | + end |
| 48 | + |
| 49 | + private |
| 50 | + def resolved_structure_dump_method |
| 51 | + case OracleEnhancedAdapter.structure_dump_method |
| 52 | + when :auto |
| 53 | + use_dbms_metadata_dump? ? :dbms_metadata : :data_dictionary |
| 54 | + when :dbms_metadata |
| 55 | + unless use_dbms_metadata_dump? |
| 56 | + raise ArgumentError, |
| 57 | + "structure_dump_method: :dbms_metadata requires Oracle 12.1 or later " \ |
| 58 | + "(connected server reports #{database_version}). " \ |
| 59 | + "Use :auto to fall back to :data_dictionary on older releases." |
| 60 | + end |
| 61 | + :dbms_metadata |
| 62 | + when :data_dictionary |
| 63 | + :data_dictionary |
| 64 | + else |
| 65 | + raise ArgumentError, |
| 66 | + "Unknown structure_dump_method " \ |
| 67 | + "#{OracleEnhancedAdapter.structure_dump_method.inspect}; " \ |
| 68 | + "expected :auto, :dbms_metadata, or :data_dictionary." |
| 69 | + end |
| 70 | + end |
| 71 | + |
| 72 | + def dbms_metadata_structure_dump |
| 73 | + configure_dbms_metadata_transforms |
| 74 | + structure = [] |
| 75 | + |
| 76 | + sequence_names = select_values(<<~SQL.squish, "SCHEMA") |
| 77 | + SELECT sequence_name FROM all_sequences |
| 78 | + WHERE sequence_owner = SYS_CONTEXT('userenv', 'current_schema') |
| 79 | + ORDER BY 1 |
| 80 | + SQL |
| 81 | + sequence_names.each do |seq_name| |
| 82 | + ddl = dbms_metadata_get_ddl("SEQUENCE", seq_name) |
| 83 | + structure << ddl if ddl |
| 84 | + end |
| 85 | + |
| 86 | + tables = select_values(<<~SQL.squish, "SCHEMA") |
| 87 | + SELECT table_name FROM all_tables t |
| 88 | + WHERE owner = SYS_CONTEXT('userenv', 'current_schema') AND secondary = 'N' |
| 89 | + AND NOT EXISTS (SELECT mv.mview_name FROM all_mviews mv |
| 90 | + WHERE mv.owner = t.owner AND mv.mview_name = t.table_name) |
| 91 | + AND NOT EXISTS (SELECT mvl.log_table FROM all_mview_logs mvl |
| 92 | + WHERE mvl.log_owner = t.owner AND mvl.log_table = t.table_name) |
| 93 | + ORDER BY 1 |
| 94 | + SQL |
| 95 | + tables.each do |table_name| |
| 96 | + ddl = dbms_metadata_get_ddl("TABLE", table_name) |
| 97 | + structure << ddl if ddl |
| 98 | + |
| 99 | + # `CONSTRAINTS=TRUE` already inlines PK/UNIQUE constraint DDL |
| 100 | + # (and their backing indexes) into the table DDL, so emitting |
| 101 | + # `GET_DDL("INDEX", ...)` for those would duplicate them. |
| 102 | + constraint_index_names = constraint_backed_index_names(table_name) |
| 103 | + indexes(table_name).each do |idx| |
| 104 | + next if constraint_index_names.include?(idx.name.upcase) |
| 105 | + idx_ddl = dbms_metadata_get_ddl("INDEX", idx.name.upcase) |
| 106 | + structure << idx_ddl if idx_ddl |
| 107 | + end |
| 108 | + |
| 109 | + # `primary_key_trigger:` (#2615) emits a row trigger named |
| 110 | + # `<table>_pkt`; pick those up via GET_DEPENDENT_DDL('TRIGGER'). |
| 111 | + trg_ddl = dbms_metadata_get_dependent_ddl("TRIGGER", table_name) |
| 112 | + structure.concat(split_dbms_metadata_ddl(trg_ddl)) if trg_ddl |
| 113 | + |
| 114 | + structure.concat(dbms_metadata_structure_dump_table_comments(table_name)) |
| 115 | + structure.concat(dbms_metadata_structure_dump_column_comments(table_name)) |
| 116 | + end |
| 117 | + |
| 118 | + fk_statements = [] |
| 119 | + tables.each do |table_name| |
| 120 | + fk_ddl = dbms_metadata_get_dependent_ddl("REF_CONSTRAINT", table_name) |
| 121 | + fk_statements.concat(split_dbms_metadata_ddl(fk_ddl)) if fk_ddl |
| 122 | + end |
| 123 | + |
| 124 | + view_names = select_values(<<~SQL.squish, "SCHEMA") |
| 125 | + SELECT view_name FROM all_views |
| 126 | + WHERE owner = SYS_CONTEXT('userenv', 'current_schema') |
| 127 | + ORDER BY view_name ASC |
| 128 | + SQL |
| 129 | + view_names.each do |view_name| |
| 130 | + ddl = dbms_metadata_get_ddl("VIEW", view_name) |
| 131 | + structure << ddl if ddl |
| 132 | + end |
| 133 | + |
| 134 | + join_with_statement_token(structure) << |
| 135 | + join_with_statement_token(fk_statements) |
| 136 | + ensure |
| 137 | + reset_dbms_metadata_transforms |
| 138 | + end |
| 139 | + |
| 140 | + def dbms_metadata_structure_dump_db_stored_code |
| 141 | + configure_dbms_metadata_transforms |
| 142 | + structure = [] |
| 143 | + |
| 144 | + # `GET_DDL("PACKAGE", ...)` returns *both* the spec and body, and |
| 145 | + # likewise for TYPE. Selecting 'PACKAGE BODY' / 'TYPE BODY' here |
| 146 | + # would dump the body a second time. |
| 147 | + all_source = select_all(<<~SQL.squish, "SCHEMA") |
| 148 | + SELECT DISTINCT name, type |
| 149 | + FROM all_source |
| 150 | + WHERE type IN ('PROCEDURE', 'PACKAGE', 'FUNCTION', 'TRIGGER', 'TYPE') |
| 151 | + AND name NOT LIKE 'BIN$%' |
| 152 | + AND owner = SYS_CONTEXT('userenv', 'current_schema') ORDER BY type |
| 153 | + SQL |
| 154 | + all_source.each do |source| |
| 155 | + ddl = dbms_metadata_get_ddl(source["type"], source["name"]) |
| 156 | + structure << ddl if ddl |
| 157 | + end |
| 158 | + |
| 159 | + structure << dbms_metadata_structure_dump_synonyms |
| 160 | + |
| 161 | + join_with_statement_token(structure) |
| 162 | + ensure |
| 163 | + reset_dbms_metadata_transforms |
| 164 | + end |
| 165 | + |
| 166 | + def dbms_metadata_structure_dump_synonyms |
| 167 | + configure_dbms_metadata_transforms |
| 168 | + structure = [] |
| 169 | + synonym_names = select_values(<<~SQL.squish, "SCHEMA") |
| 170 | + SELECT synonym_name FROM all_synonyms |
| 171 | + WHERE owner = SYS_CONTEXT('userenv', 'current_schema') |
| 172 | + SQL |
| 173 | + synonym_names.each do |synonym_name| |
| 174 | + ddl = dbms_metadata_get_ddl("SYNONYM", synonym_name) |
| 175 | + structure << ddl if ddl |
| 176 | + end |
| 177 | + join_with_statement_token(structure) |
| 178 | + ensure |
| 179 | + reset_dbms_metadata_transforms |
| 180 | + end |
| 181 | + |
| 182 | + def constraint_backed_index_names(table_name) |
| 183 | + select_values(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)]) |
| 184 | + SELECT index_name FROM all_constraints |
| 185 | + WHERE owner = SYS_CONTEXT('userenv', 'current_schema') |
| 186 | + AND table_name = :table_name |
| 187 | + AND constraint_type IN ('P', 'U') |
| 188 | + AND index_name IS NOT NULL |
| 189 | + SQL |
| 190 | + end |
| 191 | + |
| 192 | + # COMMENT ON is not reliably available via |
| 193 | + # DBMS_METADATA.GET_DEPENDENT_DDL('COMMENT'), so query directly. |
| 194 | + def dbms_metadata_structure_dump_table_comments(table_name) |
| 195 | + comment = table_comment(table_name) |
| 196 | + return [] if comment.nil? |
| 197 | + ["COMMENT ON TABLE #{quote_table_name(table_name)} IS '#{quote_string(comment)}'"] |
| 198 | + end |
| 199 | + |
| 200 | + def dbms_metadata_structure_dump_column_comments(table_name) |
| 201 | + comments = [] |
| 202 | + columns = select_values(<<~SQL.squish, "SCHEMA", [bind_string("table_name", table_name)]) |
| 203 | + SELECT column_name FROM all_tab_columns |
| 204 | + WHERE owner = SYS_CONTEXT('userenv', 'current_schema') |
| 205 | + AND table_name = :table_name ORDER BY column_id |
| 206 | + SQL |
| 207 | + columns.each do |column| |
| 208 | + comment = column_comment(table_name, column) |
| 209 | + unless comment.nil? |
| 210 | + comments << "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column)} IS '#{quote_string(comment)}'" |
| 211 | + end |
| 212 | + end |
| 213 | + comments |
| 214 | + end |
| 215 | + |
| 216 | + # Suppress installation-specific output, in spirit of |
| 217 | + # `pg_dump --schema-only --no-owner --no-tablespaces`. |
| 218 | + def configure_dbms_metadata_transforms |
| 219 | + execute(<<~SQL) |
| 220 | + BEGIN |
| 221 | + DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'STORAGE', FALSE); |
| 222 | + DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'TABLESPACE', FALSE); |
| 223 | + DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'SEGMENT_ATTRIBUTES', FALSE); |
| 224 | + DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'EMIT_SCHEMA', FALSE); |
| 225 | + DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'REF_CONSTRAINTS', FALSE); |
| 226 | + END; |
| 227 | + SQL |
| 228 | + end |
| 229 | + |
| 230 | + def reset_dbms_metadata_transforms |
| 231 | + execute(<<~SQL) |
| 232 | + BEGIN |
| 233 | + DBMS_METADATA.SET_TRANSFORM_PARAM(DBMS_METADATA.SESSION_TRANSFORM, 'DEFAULT'); |
| 234 | + END; |
| 235 | + SQL |
| 236 | + end |
| 237 | + |
| 238 | + def dbms_metadata_get_ddl(object_type, object_name) |
| 239 | + result = select_value( |
| 240 | + "SELECT DBMS_METADATA.GET_DDL(#{quote(object_type)}, #{quote(object_name)}) FROM DUAL", |
| 241 | + "SCHEMA" |
| 242 | + ) |
| 243 | + clean_dbms_metadata_ddl(result) |
| 244 | + rescue ActiveRecord::StatementInvalid => e |
| 245 | + # ORA-31603: object not found — e.g., constraint-backing index names |
| 246 | + # returned by `indexes()` that DBMS_METADATA classifies as |
| 247 | + # CONSTRAINT, not INDEX. The constraint DDL is already inlined in |
| 248 | + # the parent TABLE GET_DDL output, so silently skipping is correct. |
| 249 | + raise unless e.message.include?("ORA-31603") |
| 250 | + nil |
| 251 | + end |
| 252 | + |
| 253 | + def dbms_metadata_get_dependent_ddl(dependent_type, base_object_name) |
| 254 | + result = select_value( |
| 255 | + "SELECT DBMS_METADATA.GET_DEPENDENT_DDL(#{quote(dependent_type)}, #{quote(base_object_name)}) FROM DUAL", |
| 256 | + "SCHEMA" |
| 257 | + ) |
| 258 | + clean_dbms_metadata_ddl(result) |
| 259 | + rescue ActiveRecord::StatementInvalid => e |
| 260 | + # ORA-31608: dependent object not found (e.g., no triggers / no FKs). |
| 261 | + raise unless e.message.include?("ORA-31608") |
| 262 | + nil |
| 263 | + end |
| 264 | + |
| 265 | + def clean_dbms_metadata_ddl(ddl) |
| 266 | + return nil if ddl.nil? |
| 267 | + result = ddl.to_s.strip |
| 268 | + result.empty? ? nil : result |
| 269 | + end |
| 270 | + |
| 271 | + # GET_DEPENDENT_DDL can return multiple DDL statements concatenated |
| 272 | + # in a single CLOB. Split on blank-line boundaries. |
| 273 | + def split_dbms_metadata_ddl(ddl) |
| 274 | + return [] if ddl.nil? |
| 275 | + ddl.split(/\n\s*\n/).map(&:strip).reject(&:empty?) |
| 276 | + end |
| 277 | + end |
| 278 | + |
| 279 | + prepend DbmsMetadata |
| 280 | + end |
| 281 | + end |
| 282 | + end |
| 283 | +end |
0 commit comments