Skip to content

Use DBMS_METADATA for structure dump#2513

Merged
yahonda merged 1 commit into
rsim:masterfrom
yahonda:use-dbms-metadata-for-structure-dump-v2
May 3, 2026
Merged

Use DBMS_METADATA for structure dump#2513
yahonda merged 1 commit into
rsim:masterfrom
yahonda:use-dbms-metadata-for-structure-dump-v2

Conversation

@yahonda

@yahonda yahonda commented Apr 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a second structure_dump implementation built on Oracle's DBMS_METADATA.GET_DDL / GET_DEPENDENT_DDL alongside the existing data-dictionary (ALL_*) one. The new path lives in a separate file (lib/active_record/connection_adapters/oracle_enhanced/structure_dump/dbms_metadata.rb) — the existing structure_dump.rb is untouched, so selecting :data_dictionary retains exact master behavior.

Selection

OracleEnhancedAdapter.structure_dump_method accepts:

  • :auto (default) — DBMS_METADATA on Oracle 12.1+, otherwise data-dictionary. The 12.1 floor is a project-policy version gate (the IDENTITY / EDITIONABLE / modern SET_TRANSFORM_PARAM era), exposed as use_dbms_metadata_dump?. DBMS_METADATA.GET_DDL itself works on Oracle 9i+; this is a project decision, not a strict capability check.
  • :dbms_metadata — force DBMS_METADATA. Raises ArgumentError on pre-12.1 (mirrors PR Replace use_legacy_identifier_length with identifier_max_length (:auto default) #2576's identifier_max_length: :long fail-fast policy).
  • :data_dictionary — force the implementation that assembles DDL from the ALL_* static data dictionary views in Ruby. Remains explicitly selectable on 12.1+ as well.

Implementation shape

The DBMS_METADATA path walks ALL_OBJECTS per object_type rather than hand-rolling per-kind queries. This naturally picks up MATERIALIZED VIEW, TYPE, TRIGGER, SYNONYM and friends without separate code paths and minimizes future maintenance when Oracle adds new object kinds. FK constraints (REF_CONSTRAINT) are emitted in a single trailing block; cross-table dependency order is left to the user (or to a fresh-schema reload) rather than tracked here.

Two modules in the same file split responsibilities so the dispatcher logic doesn't bleed into the DBMS_METADATA implementation:

  • Dispatcherprepend-ed; routes the three entry points (structure_dump, structure_dump_db_stored_code, structure_dump_synonyms) to either DBMS_METADATA or super (data-dictionary).
  • DbmsMetadatainclude-d; the DBMS_METADATA implementation itself, with no dispatch logic.

Output

Output shape is informed by mysqldump --no-data / pg_dump --schema-only:

  • STORAGE / TABLESPACE / SEGMENT_ATTRIBUTES / EMIT_SCHEMA suppressed via DBMS_METADATA.SET_TRANSFORM_PARAM so the dump is portable across installations and schemas. Oracle honors these for TABLE / INDEX / SEQUENCE.
  • MATERIALIZED VIEW is an Oracle-side exception: even with object_type-specific overrides Oracle does not honour the suppression on MV, so MV DDL retains physical attributes (PCTFREE, TABLESPACE, SEGMENT CREATION, etc.) as Oracle emits them. Since MVs aren't creatable through Rails' standard migration helpers anyway, no post-processing pass is added — Oracle's output is used as-is.
  • REF_CONSTRAINTS emitted as separate ALTER TABLE … ADD CONSTRAINT statements after all tables.
  • Inline constraints via Oracle's CONSTRAINTS=TRUE default; UNIQUE backing indexes that DBMS_METADATA already inlines into the table DDL are filtered out so they aren't emitted twice.
  • STATEMENT_TOKEN separation preserved (Oracle's SQLTERMINATOR=FALSE default) so execute_structure_dump splits as before.
  • primary_key_trigger: (Restore trigger-based primary key generation as opt-in primary_key_trigger: option #2615) row triggers are picked up via the TRIGGER object_type pass — addresses #2513 (comment).
  • COMMENT ON TABLE / COMMENT ON COLUMN queried directly because GET_DEPENDENT_DDL("COMMENT", ...) is unreliable.
  • structure_dump_db_stored_code selects 'PROCEDURE', 'PACKAGE', 'FUNCTION', 'TYPE' only — GET_DDL("PACKAGE", ...) already returns spec + body, so selecting 'PACKAGE BODY' would emit the body twice.
  • Only SET_TRANSFORM_PARAM calls whose value differs from Oracle's default are issued (STORAGE / TABLESPACE / SEGMENT_ATTRIBUTES / EMIT_SCHEMA / REF_CONSTRAINTS).

Tests

A new dbms_metadata_structure_dump_spec.rb exercises the new path with structural assertions only (CREATE TABLE / CREATE INDEX / ALTER TABLE … ADD CONSTRAINT / COMMENT ON … / no STORAGE / TABLESPACE / PCTFREE noise) — exact byte-level DDL is intentionally not part of the contract, mirroring how pg_dump / mysqldump are tested upstream. Regression coverage added for the unique-index and PACKAGE-BODY duplicate-output bugs found during development. A schema-option spec verifies that :schema connection-time switching is honored (the dump walks SYS_CONTEXT('userenv', 'current_schema'), not the connecting user).

The existing structure_dump_spec.rb pins structure_dump_method = :data_dictionary in before(:all) and restores it in after(:all), so the data-dictionary backend's exact-DDL assertions continue to run.

Test plan

  • bundle exec rspec spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb — green (pinned to :data_dictionary)
  • bundle exec rspec spec/active_record/connection_adapters/oracle_enhanced/dbms_metadata_structure_dump_spec.rb — green
  • bundle exec rspec full suite — 747 examples, 0 failures, 12 pending
  • bundle exec rubocop — clean

Closes #2513.

@yahonda

yahonda commented Apr 27, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up: #2615 (currently open) restores trigger-based primary key generation as the primary_key_trigger: opt-in. If #2615 lands, db:structure:dump should also pick up the BEFORE INSERT row trigger emitted by create_pk_trigger so that db:structure:load can recreate the table + sequence + trigger faithfully.

DBMS_METADATA.GET_DEPENDENT_DDL('TRIGGER', table_name, schema) returns the trigger DDL as a CLOB, which fits the GET_DDL-based approach this PR is taking. Worth adding when both PRs settle, either to this PR (if it stays open until #2615 merges) or as a follow-up.

The Ruby-flavored db/schema.rb round-trip is already handled inside #2615 via SchemaDumper; this note is purely about the db/structure.sql path.

yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 2, 2026
Closes rsim#2513.

Adds a second `structure_dump` implementation built on Oracle's
`DBMS_METADATA.GET_DDL` / `GET_DEPENDENT_DDL`, the Oracle-native
equivalent of `pg_dump --schema-only` / `mysqldump --no-data`.

The original `ALL_*` data-dictionary-based implementation in
`StructureDump` is left untouched. A new module `StructureDump::DbmsMetadata`
prepends to `StructureDump` and dispatches each entry point —
`structure_dump`, `structure_dump_db_stored_code`, `structure_dump_synonyms` —
based on `OracleEnhancedAdapter.structure_dump_method`:

  * `:dbms_metadata` (default) — call the new DBMS_METADATA path.
  * `:data_dictionary` — fall through via `super` to the existing
    `ALL_*`-based implementation.

The toggle is intentionally a Rails-app-global `cattr_accessor` rather
than a per-connection `database.yml` key. The choice of structure-dump
backend is an implementation strategy, not something that varies across
the databases an app connects to.

The `:data_dictionary` path is retained as a fallback while the new
backend stabilises and may be deprecated and eventually removed in a
future release. Defaulting to `:dbms_metadata` ensures real-world usage
(and bug reports against the new path) before the old one is dropped —
the original `ALL_*` implementation has needed regular touch-ups for
schema features (most recently CHECK constraints in rsim#2500) that
`DBMS_METADATA` would surface for free.

Output transforms are configured to match the spirit of mysqldump /
pg_dump --schema-only:

  * STORAGE / TABLESPACE / SEGMENT_ATTRIBUTES → FALSE
    (no installation-specific noise)
  * EMIT_SCHEMA → FALSE
    (portable across schemas)
  * SQLTERMINATOR → FALSE
    (Rails uses STATEMENT_TOKEN to split DDL on load)
  * CONSTRAINTS → TRUE, REF_CONSTRAINTS → FALSE
    (inline column constraints with the table; emit referential
    constraints separately as ALTER TABLE statements after all tables
    are created so DDL load order is correct)
  * PRETTY → TRUE

Triggers emitted by the `primary_key_trigger:` opt-in (rsim#2615) are
picked up via `GET_DEPENDENT_DDL('TRIGGER', table_name)` so a structure
dump + load round-trip recreates the table + sequence + trigger
faithfully.

Tests:

  * `structure_dump_spec.rb` — existing examples assume the
    data-dictionary backend's exact DDL text; pinned to
    `structure_dump_method = :data_dictionary` in `before(:all)` so they
    keep verifying that path.
  * `dbms_metadata_structure_dump_spec.rb` — new file covering the
    DBMS_METADATA path. Asserts on the dump's structural shape
    (presence of CREATE TABLE / CREATE INDEX / ALTER TABLE … REFERENCES /
    COMMENT ON statements; absence of STORAGE / TABLESPACE clauses)
    rather than exact DDL text. Also covers the toggle: default value,
    delegation to `:data_dictionary`, and `ArgumentError` on unknown
    values.

Test plan:

  * `bundle exec rspec` on Oracle 23ai — 711 examples, 0 failures, 11
    pending.
  * `bundle exec rubocop` — clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the use-dbms-metadata-for-structure-dump-v2 branch from e3ee0a7 to 3157801 Compare May 2, 2026 15:13
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
`structure_dump` (the `:data_dictionary` backend) emits identifiers
inconsistently: tables and columns inside `CREATE TABLE` /
`CREATE INDEX` are quoted, but PRIMARY KEY column lists,
UNIQUE-constraint `ALTER TABLE` statements, view names, and synonym
names are not. The result is gratuitous diff noise across runs and
unnecessary divergence from the `:dbms_metadata` backend (rsim#2513),
which always quotes.

Quote table names, column names, constraint names, view names, and
synonym names consistently in:

- structure_dump_primary_key
- structure_dump_unique_keys
- structure_dump_views
- structure_dump_synonyms

Also drop the stray double space in `CREATE  INDEX`. The space is
emitted explicitly by the template
(`CREATE#{' UNIQUE' if unique} INDEX ...`) so a future edit cannot
silently produce `CREATE UNIQUEINDEX`. Compact the PK / unique-key
column lists before quoting so a sparse `position` array cannot
reach `quote_column_name(nil)`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
`structure_dump` emits identifiers inconsistently: tables and columns
inside `CREATE TABLE` / `CREATE INDEX` are quoted, but PRIMARY KEY
column lists, UNIQUE-constraint `ALTER TABLE` statements, view names,
and synonym names are not. The result is gratuitous diff noise across
runs.

Quote table names, column names, constraint names, view names, and
synonym names consistently in:

- structure_dump_primary_key
- structure_dump_unique_keys
- structure_dump_views
- structure_dump_synonyms

Also drop the stray double space in `CREATE  INDEX`. The space is
emitted explicitly by the template
(`CREATE#{' UNIQUE' if unique} INDEX ...`) so a future edit cannot
silently produce `CREATE UNIQUEINDEX`. Compact the PK / unique-key
column lists before quoting so a sparse `position` array cannot
reach `quote_column_name(nil)`.

The unquoted identifiers were surfaced while exploring a
DBMS_METADATA-based structure_dump backend in rsim#2513; this change
stands independent of that work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Closes rsim#2513.

Adds a second `structure_dump` implementation built on Oracle's
`DBMS_METADATA.GET_DDL` / `GET_DEPENDENT_DDL`, the Oracle-native
equivalent of `pg_dump --schema-only` / `mysqldump --no-data`.

The original `ALL_*` data-dictionary-based implementation in
`StructureDump` is left untouched. A new module `StructureDump::DbmsMetadata`
prepends to `StructureDump` and dispatches each entry point —
`structure_dump`, `structure_dump_db_stored_code`, `structure_dump_synonyms` —
based on `OracleEnhancedAdapter.structure_dump_method`:

  * `:dbms_metadata` (default) — call the new DBMS_METADATA path.
  * `:data_dictionary` — fall through via `super` to the existing
    `ALL_*`-based implementation.

The toggle is intentionally a Rails-app-global `cattr_accessor` rather
than a per-connection `database.yml` key. The choice of structure-dump
backend is an implementation strategy, not something that varies across
the databases an app connects to.

The `:data_dictionary` path is retained as a fallback while the new
backend stabilises and may be deprecated and eventually removed in a
future release. Defaulting to `:dbms_metadata` ensures real-world usage
(and bug reports against the new path) before the old one is dropped —
the original `ALL_*` implementation has needed regular touch-ups for
schema features (most recently CHECK constraints in rsim#2500) that
`DBMS_METADATA` would surface for free.

Output transforms are configured to match the spirit of mysqldump /
pg_dump --schema-only:

  * STORAGE / TABLESPACE / SEGMENT_ATTRIBUTES → FALSE
    (no installation-specific noise)
  * EMIT_SCHEMA → FALSE
    (portable across schemas)
  * SQLTERMINATOR → FALSE
    (Rails uses STATEMENT_TOKEN to split DDL on load)
  * CONSTRAINTS → TRUE, REF_CONSTRAINTS → FALSE
    (inline column constraints with the table; emit referential
    constraints separately as ALTER TABLE statements after all tables
    are created so DDL load order is correct)
  * PRETTY → TRUE

Triggers emitted by the `primary_key_trigger:` opt-in (rsim#2615) are
picked up via `GET_DEPENDENT_DDL('TRIGGER', table_name)` so a structure
dump + load round-trip recreates the table + sequence + trigger
faithfully.

Tests:

  * `structure_dump_spec.rb` — existing examples assume the
    data-dictionary backend's exact DDL text; pinned to
    `structure_dump_method = :data_dictionary` in `before(:all)` so they
    keep verifying that path.
  * `dbms_metadata_structure_dump_spec.rb` — new file covering the
    DBMS_METADATA path. Asserts on the dump's structural shape
    (presence of CREATE TABLE / CREATE INDEX / ALTER TABLE … REFERENCES /
    COMMENT ON statements; absence of STORAGE / TABLESPACE clauses)
    rather than exact DDL text. Also covers the toggle: default value,
    delegation to `:data_dictionary`, and `ArgumentError` on unknown
    values.

Test plan:

  * `bundle exec rspec` on Oracle 23ai — 711 examples, 0 failures, 11
    pending.
  * `bundle exec rubocop` — clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the use-dbms-metadata-for-structure-dump-v2 branch from c3842d7 to 71baf24 Compare May 3, 2026 08:03
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Closes rsim#2513.

Adds a second `structure_dump` implementation built on Oracle's
`DBMS_METADATA.GET_DDL` / `GET_DEPENDENT_DDL` alongside the existing
data-dictionary (`ALL_*`) one. The new backend lives in a separate
file and installs via `prepend DbmsMetadata`, so the legacy path in
`structure_dump.rb` is untouched.

Selectable globally via `OracleEnhancedAdapter.structure_dump_method`:

- `:auto` (default) — `:dbms_metadata` on Oracle 12.1+,
  `:data_dictionary` otherwise. The 12.1 floor is a project-policy
  version gate (the IDENTITY / EDITIONABLE / modern
  SET_TRANSFORM_PARAM era), exposed as `use_dbms_metadata_dump?`,
  not a strict database capability check.
- `:dbms_metadata` — force the new path; raises `ArgumentError` on
  pre-12.1 (mirrors PR rsim#2576's `identifier_max_length: :long`
  fail-fast policy).
- `:data_dictionary` — force the original implementation. Remains
  explicitly selectable on 12.1+ as well.

Output is informed by `mysqldump --no-data` / `pg_dump --schema-only`:

- `STORAGE` / `TABLESPACE` / `SEGMENT_ATTRIBUTES` / `EMIT_SCHEMA`
  suppressed via `DBMS_METADATA.SET_TRANSFORM_PARAM` so the dump is
  portable across installations and schemas.
- `REF_CONSTRAINTS` emitted as separate `ALTER TABLE … ADD
  CONSTRAINT` statements after all tables.
- Inline constraints via Oracle's `CONSTRAINTS=TRUE` default; UNIQUE
  backing indexes that DBMS_METADATA already inlines into the table
  DDL are filtered out so they aren't emitted twice.
- `STATEMENT_TOKEN` separation preserved (Oracle's `SQLTERMINATOR=FALSE`
  default) so `execute_structure_dump` splits as before.
- `primary_key_trigger:` (rsim#2615) row triggers picked up via
  `GET_DEPENDENT_DDL("TRIGGER", table_name)`.
- COMMENT ON TABLE / COMMENT ON COLUMN queried directly because
  `GET_DEPENDENT_DDL("COMMENT")` is unreliable.
- `structure_dump_db_stored_code` selects `'PROCEDURE', 'PACKAGE',
  'FUNCTION', 'TRIGGER', 'TYPE'` only — `GET_DDL("PACKAGE", ...)`
  already returns spec + body, so selecting `'PACKAGE BODY'` would
  emit the body twice.

Specs follow the `pg_dump` / `mysqldump` testing convention: assert
output shape (`CREATE TABLE` / `CREATE INDEX` / `ALTER TABLE … ADD
CONSTRAINT` / `COMMENT` / no `STORAGE` / `TABLESPACE` / `PCTFREE`)
rather than exact byte-level DDL. The existing `structure_dump_spec`
pins `:data_dictionary` in `before(:all)` so its exact-DDL
assertions still apply to the legacy backend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the use-dbms-metadata-for-structure-dump-v2 branch from e9bc077 to 77fec0e Compare May 3, 2026 08:16
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Closes rsim#2513.

Adds a second `structure_dump` implementation built on Oracle's
`DBMS_METADATA.GET_DDL` / `GET_DEPENDENT_DDL` alongside the existing
data-dictionary (`ALL_*`) one. The new backend lives in a separate
file and installs via `prepend DbmsMetadata`, so the legacy path in
`structure_dump.rb` is untouched.

Selectable globally via `OracleEnhancedAdapter.structure_dump_method`:

- `:auto` (default) — `:dbms_metadata` on Oracle 12.1+,
  `:data_dictionary` otherwise. The 12.1 floor is a project-policy
  version gate (the IDENTITY / EDITIONABLE / modern
  SET_TRANSFORM_PARAM era), exposed as `use_dbms_metadata_dump?`.
- `:dbms_metadata` — force the new path; raises `ArgumentError` on
  pre-12.1 (mirrors PR rsim#2576's `identifier_max_length: :long`
  fail-fast policy).
- `:data_dictionary` — force the original implementation. Remains
  explicitly selectable on 12.1+ as well.

Implementation walks `ALL_OBJECTS` per object_type rather than
hand-rolling per-kind queries; this naturally picks up
MATERIALIZED VIEW / TYPE / TRIGGER / SYNONYM / etc. without separate
code paths. FK constraints (REF_CONSTRAINT) are emitted in a single
trailing block; cross-table dependency order is left to the user (or
to a fresh-schema reload) rather than tracked here.

Output is informed by `mysqldump --no-data` / `pg_dump --schema-only`:

- `STORAGE` / `TABLESPACE` / `SEGMENT_ATTRIBUTES` / `EMIT_SCHEMA`
  suppressed via `DBMS_METADATA.SET_TRANSFORM_PARAM` so the dump is
  portable across installations and schemas. Oracle honors these for
  TABLE / INDEX / SEQUENCE.
- `MATERIALIZED VIEW` DDL is an exception: Oracle does not honour the
  same suppression on MV (object_type-specific override does not work
  either), so MV DDL retains physical attributes (PCTFREE, TABLESPACE,
  SEGMENT CREATION, etc.) as Oracle emits them. Since MVs aren't
  creatable through Rails' standard migration helpers anyway, no
  post-processing pass is added — Oracle's output is used as-is.
- `REF_CONSTRAINTS` emitted as separate `ALTER TABLE … ADD
  CONSTRAINT` statements after all tables.
- Inline constraints via Oracle's `CONSTRAINTS=TRUE` default; UNIQUE
  backing indexes that DBMS_METADATA already inlines into the table
  DDL are filtered out so they aren't emitted twice.
- `STATEMENT_TOKEN` separation preserved (Oracle's `SQLTERMINATOR=FALSE`
  default) so `execute_structure_dump` splits as before.
- `primary_key_trigger:` (rsim#2615) row triggers picked up via the
  TRIGGER object_type pass.
- COMMENT ON TABLE / COMMENT ON COLUMN queried directly because
  `GET_DEPENDENT_DDL("COMMENT")` is unreliable.
- `structure_dump_db_stored_code` selects 'PROCEDURE', 'PACKAGE',
  'FUNCTION', 'TYPE' — `GET_DDL("PACKAGE", ...)` already returns
  spec + body, so selecting 'PACKAGE BODY' would emit the body twice.

Specs follow the `pg_dump` / `mysqldump` testing convention: assert
output shape rather than exact byte-level DDL. A schema-option spec
verifies that `:schema` connection-time switching is honored
(structure_dump walks `SYS_CONTEXT('userenv', 'current_schema')`,
not the connecting user). The existing `structure_dump_spec` pins
`:data_dictionary` in `before(:all)` so its exact-DDL assertions
still apply to the legacy backend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the use-dbms-metadata-for-structure-dump-v2 branch from 77fec0e to 8a5467d Compare May 3, 2026 12:51
@yahonda yahonda marked this pull request as ready for review May 3, 2026 12:51
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Closes rsim#2513.

Adds a second `structure_dump` implementation built on Oracle's
`DBMS_METADATA.GET_DDL` / `GET_DEPENDENT_DDL` alongside the existing
data-dictionary (`ALL_*`) one. The new backend lives in a separate
file and installs via `prepend DbmsMetadata`, so the legacy path in
`structure_dump.rb` is untouched.

Selectable globally via `OracleEnhancedAdapter.structure_dump_method`:

- `:auto` (default) — `:dbms_metadata` on Oracle 12.1+,
  `:data_dictionary` otherwise. The 12.1 floor is a project-policy
  version gate (the IDENTITY / EDITIONABLE / modern
  SET_TRANSFORM_PARAM era), exposed as `use_dbms_metadata_dump?`.
- `:dbms_metadata` — force the new path; raises `ArgumentError` on
  pre-12.1 (mirrors PR rsim#2576's `identifier_max_length: :long`
  fail-fast policy).
- `:data_dictionary` — force the original implementation. Remains
  explicitly selectable on 12.1+ as well.

Implementation walks `ALL_OBJECTS` per object_type rather than
hand-rolling per-kind queries; this naturally picks up
MATERIALIZED VIEW / TYPE / TRIGGER / SYNONYM / etc. without separate
code paths. FK constraints (REF_CONSTRAINT) are emitted in a single
trailing block; cross-table dependency order is left to the user (or
to a fresh-schema reload) rather than tracked here.

Output is informed by `mysqldump --no-data` / `pg_dump --schema-only`:

- `STORAGE` / `TABLESPACE` / `SEGMENT_ATTRIBUTES` / `EMIT_SCHEMA`
  suppressed via `DBMS_METADATA.SET_TRANSFORM_PARAM` so the dump is
  portable across installations and schemas. Oracle honors these for
  TABLE / INDEX / SEQUENCE.
- `MATERIALIZED VIEW` DDL is an exception: Oracle does not honour the
  same suppression on MV (object_type-specific override does not work
  either), so MV DDL retains physical attributes (PCTFREE, TABLESPACE,
  SEGMENT CREATION, etc.) as Oracle emits them. Since MVs aren't
  creatable through Rails' standard migration helpers anyway, no
  post-processing pass is added — Oracle's output is used as-is.
- `REF_CONSTRAINTS` emitted as separate `ALTER TABLE … ADD
  CONSTRAINT` statements after all tables.
- Inline constraints via Oracle's `CONSTRAINTS=TRUE` default; UNIQUE
  backing indexes that DBMS_METADATA already inlines into the table
  DDL are filtered out so they aren't emitted twice.
- `STATEMENT_TOKEN` separation preserved (Oracle's `SQLTERMINATOR=FALSE`
  default) so `execute_structure_dump` splits as before.
- `primary_key_trigger:` (rsim#2615) row triggers picked up via the
  TRIGGER object_type pass.
- COMMENT ON TABLE / COMMENT ON COLUMN queried directly because
  `GET_DEPENDENT_DDL("COMMENT")` is unreliable.
- `structure_dump_db_stored_code` selects 'PROCEDURE', 'PACKAGE',
  'FUNCTION', 'TYPE' — `GET_DDL("PACKAGE", ...)` already returns
  spec + body, so selecting 'PACKAGE BODY' would emit the body twice.

Specs follow the `pg_dump` / `mysqldump` testing convention: assert
output shape rather than exact byte-level DDL. A schema-option spec
verifies that `:schema` connection-time switching is honored
(structure_dump walks `SYS_CONTEXT('userenv', 'current_schema')`,
not the connecting user). The existing `structure_dump_spec` pins
`:data_dictionary` in `before(:all)` so its exact-DDL assertions
still apply to the legacy backend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the use-dbms-metadata-for-structure-dump-v2 branch from e2c379f to cb4af6d Compare May 3, 2026 14:41
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Closes rsim#2513.

Adds a second `structure_dump` implementation built on Oracle's
`DBMS_METADATA.GET_DDL` / `GET_DEPENDENT_DDL` alongside the existing
data-dictionary (`ALL_*`) one. The new backend lives in a separate
file and installs via `prepend DbmsMetadata`, so the legacy path in
`structure_dump.rb` is untouched.

Selectable globally via `OracleEnhancedAdapter.structure_dump_method`:

- `:auto` (default) — `:dbms_metadata` on Oracle 12.1+,
  `:data_dictionary` otherwise. The 12.1 floor is a project-policy
  version gate (the IDENTITY / EDITIONABLE / modern
  SET_TRANSFORM_PARAM era), exposed as `use_dbms_metadata_dump?`.
- `:dbms_metadata` — force the new path; raises `ArgumentError` on
  pre-12.1 (mirrors PR rsim#2576's `identifier_max_length: :long`
  fail-fast policy).
- `:data_dictionary` — force the original implementation. Remains
  explicitly selectable on 12.1+ as well.

Implementation walks `ALL_OBJECTS` per object_type rather than
hand-rolling per-kind queries; this naturally picks up
MATERIALIZED VIEW / TYPE / TRIGGER / SYNONYM / etc. without separate
code paths. FK constraints (REF_CONSTRAINT) are emitted in a single
trailing block; cross-table dependency order is left to the user (or
to a fresh-schema reload) rather than tracked here.

Output is informed by `mysqldump --no-data` / `pg_dump --schema-only`:

- `STORAGE` / `TABLESPACE` / `SEGMENT_ATTRIBUTES` / `EMIT_SCHEMA`
  suppressed via `DBMS_METADATA.SET_TRANSFORM_PARAM` so the dump is
  portable across installations and schemas. Oracle honors these for
  TABLE / INDEX / SEQUENCE.
- `MATERIALIZED VIEW` DDL is an exception: Oracle does not honour the
  same suppression on MV (object_type-specific override does not work
  either), so MV DDL retains physical attributes (PCTFREE, TABLESPACE,
  SEGMENT CREATION, etc.) as Oracle emits them. Since MVs aren't
  creatable through Rails' standard migration helpers anyway, no
  post-processing pass is added — Oracle's output is used as-is.
- `REF_CONSTRAINTS` emitted as separate `ALTER TABLE … ADD
  CONSTRAINT` statements after all tables.
- Inline constraints via Oracle's `CONSTRAINTS=TRUE` default; UNIQUE
  backing indexes that DBMS_METADATA already inlines into the table
  DDL are filtered out so they aren't emitted twice.
- `STATEMENT_TOKEN` separation preserved (Oracle's `SQLTERMINATOR=FALSE`
  default) so `execute_structure_dump` splits as before.
- `primary_key_trigger:` (rsim#2615) row triggers picked up via the
  TRIGGER object_type pass.
- COMMENT ON TABLE / COMMENT ON COLUMN queried directly because
  `GET_DEPENDENT_DDL("COMMENT")` is unreliable.
- `structure_dump_db_stored_code` selects 'PROCEDURE', 'PACKAGE',
  'FUNCTION', 'TYPE' — `GET_DDL("PACKAGE", ...)` already returns
  spec + body, so selecting 'PACKAGE BODY' would emit the body twice.

Specs follow the `pg_dump` / `mysqldump` testing convention: assert
output shape rather than exact byte-level DDL. A schema-option spec
verifies that `:schema` connection-time switching is honored
(structure_dump walks `SYS_CONTEXT('userenv', 'current_schema')`,
not the connecting user). The existing `structure_dump_spec` pins
`:data_dictionary` in `before(:all)` so its exact-DDL assertions
still apply to the legacy backend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the use-dbms-metadata-for-structure-dump-v2 branch from cb4af6d to b725280 Compare May 3, 2026 14:45
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Closes rsim#2513.

Adds a second `structure_dump` implementation built on Oracle's
`DBMS_METADATA.GET_DDL` / `GET_DEPENDENT_DDL` alongside the existing
data-dictionary (`ALL_*`) one. The new backend lives in a separate
file and installs via `prepend DbmsMetadata`, so the legacy path in
`structure_dump.rb` is untouched.

Selectable globally via `OracleEnhancedAdapter.structure_dump_method`:

- `:auto` (default) — `:dbms_metadata` on Oracle 12.1+,
  `:data_dictionary` otherwise. The 12.1 floor is a project-policy
  version gate (the IDENTITY / EDITIONABLE / modern
  SET_TRANSFORM_PARAM era), exposed as `use_dbms_metadata_dump?`.
- `:dbms_metadata` — force the new path; raises `ArgumentError` on
  pre-12.1 (mirrors PR rsim#2576's `identifier_max_length: :long`
  fail-fast policy).
- `:data_dictionary` — force the original implementation. Remains
  explicitly selectable on 12.1+ as well.

Implementation walks `ALL_OBJECTS` per object_type rather than
hand-rolling per-kind queries; this naturally picks up
MATERIALIZED VIEW / TYPE / TRIGGER / SYNONYM / etc. without separate
code paths. FK constraints (REF_CONSTRAINT) are emitted in a single
trailing block; cross-table dependency order is left to the user (or
to a fresh-schema reload) rather than tracked here.

Output is informed by `mysqldump --no-data` / `pg_dump --schema-only`:

- `STORAGE` / `TABLESPACE` / `SEGMENT_ATTRIBUTES` / `EMIT_SCHEMA`
  suppressed via `DBMS_METADATA.SET_TRANSFORM_PARAM` so the dump is
  portable across installations and schemas. Oracle honors these for
  TABLE / INDEX / SEQUENCE.
- `MATERIALIZED VIEW` DDL is an exception: Oracle does not honour the
  same suppression on MV (object_type-specific override does not work
  either), so MV DDL retains physical attributes (PCTFREE, TABLESPACE,
  SEGMENT CREATION, etc.) as Oracle emits them. Since MVs aren't
  creatable through Rails' standard migration helpers anyway, no
  post-processing pass is added — Oracle's output is used as-is.
- `REF_CONSTRAINTS` emitted as separate `ALTER TABLE … ADD
  CONSTRAINT` statements after all tables.
- Inline constraints via Oracle's `CONSTRAINTS=TRUE` default; UNIQUE
  backing indexes that DBMS_METADATA already inlines into the table
  DDL are filtered out so they aren't emitted twice.
- `STATEMENT_TOKEN` separation preserved (Oracle's `SQLTERMINATOR=FALSE`
  default) so `execute_structure_dump` splits as before.
- `primary_key_trigger:` (rsim#2615) row triggers picked up via the
  TRIGGER object_type pass.
- COMMENT ON TABLE / COMMENT ON COLUMN queried directly because
  `GET_DEPENDENT_DDL("COMMENT")` is unreliable.
- `structure_dump_db_stored_code` selects 'PROCEDURE', 'PACKAGE',
  'FUNCTION', 'TYPE' — `GET_DDL("PACKAGE", ...)` already returns
  spec + body, so selecting 'PACKAGE BODY' would emit the body twice.

Specs follow the `pg_dump` / `mysqldump` testing convention: assert
output shape rather than exact byte-level DDL. A schema-option spec
verifies that `:schema` connection-time switching is honored
(structure_dump walks `SYS_CONTEXT('userenv', 'current_schema')`,
not the connecting user). The existing `structure_dump_spec` pins
`:data_dictionary` in `before(:all)` so its exact-DDL assertions
still apply to the legacy backend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the use-dbms-metadata-for-structure-dump-v2 branch from b725280 to 890c4ed Compare May 3, 2026 14:46
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Closes rsim#2513.

Adds a second `structure_dump` implementation built on Oracle's
`DBMS_METADATA.GET_DDL` / `GET_DEPENDENT_DDL` alongside the existing
data-dictionary (`ALL_*`) one. The new backend lives in a separate
file and installs via `prepend DbmsMetadata`, so the legacy path in
`structure_dump.rb` is untouched.

Selectable globally via `OracleEnhancedAdapter.structure_dump_method`:

- `:auto` (default) — `:dbms_metadata` on Oracle 12.1+,
  `:data_dictionary` otherwise. The 12.1 floor is a project-policy
  version gate (the IDENTITY / EDITIONABLE / modern
  SET_TRANSFORM_PARAM era), exposed as `use_dbms_metadata_dump?`.
- `:dbms_metadata` — force the new path; raises `ArgumentError` on
  pre-12.1 (mirrors PR rsim#2576's `identifier_max_length: :long`
  fail-fast policy).
- `:data_dictionary` — force the original implementation. Remains
  explicitly selectable on 12.1+ as well.

Implementation walks `ALL_OBJECTS` per object_type rather than
hand-rolling per-kind queries; this naturally picks up
MATERIALIZED VIEW / TYPE / TRIGGER / SYNONYM / etc. without separate
code paths. FK constraints (REF_CONSTRAINT) are emitted in a single
trailing block; cross-table dependency order is left to the user (or
to a fresh-schema reload) rather than tracked here.

Output is informed by `mysqldump --no-data` / `pg_dump --schema-only`:

- `STORAGE` / `TABLESPACE` / `SEGMENT_ATTRIBUTES` / `EMIT_SCHEMA`
  suppressed via `DBMS_METADATA.SET_TRANSFORM_PARAM` so the dump is
  portable across installations and schemas. Oracle honors these for
  TABLE / INDEX / SEQUENCE.
- `MATERIALIZED VIEW` DDL is an exception: Oracle does not honour the
  same suppression on MV (object_type-specific override does not work
  either), so MV DDL retains physical attributes (PCTFREE, TABLESPACE,
  SEGMENT CREATION, etc.) as Oracle emits them. Since MVs aren't
  creatable through Rails' standard migration helpers anyway, no
  post-processing pass is added — Oracle's output is used as-is.
- `REF_CONSTRAINTS` emitted as separate `ALTER TABLE … ADD
  CONSTRAINT` statements after all tables.
- Inline constraints via Oracle's `CONSTRAINTS=TRUE` default; UNIQUE
  backing indexes that DBMS_METADATA already inlines into the table
  DDL are filtered out so they aren't emitted twice.
- `STATEMENT_TOKEN` separation preserved (Oracle's `SQLTERMINATOR=FALSE`
  default) so `execute_structure_dump` splits as before.
- `primary_key_trigger:` (rsim#2615) row triggers picked up via the
  TRIGGER object_type pass.
- COMMENT ON TABLE / COMMENT ON COLUMN queried directly because
  `GET_DEPENDENT_DDL("COMMENT")` is unreliable.
- `structure_dump_db_stored_code` selects 'PROCEDURE', 'PACKAGE',
  'FUNCTION', 'TYPE' — `GET_DDL("PACKAGE", ...)` already returns
  spec + body, so selecting 'PACKAGE BODY' would emit the body twice.

Specs follow the `pg_dump` / `mysqldump` testing convention: assert
output shape rather than exact byte-level DDL. A schema-option spec
verifies that `:schema` connection-time switching is honored
(structure_dump walks `SYS_CONTEXT('userenv', 'current_schema')`,
not the connecting user). The existing `structure_dump_spec` pins
`:data_dictionary` in `before(:all)` so its exact-DDL assertions
still apply to the legacy backend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the use-dbms-metadata-for-structure-dump-v2 branch from 890c4ed to e0aaa5c Compare May 3, 2026 14:56
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Closes rsim#2513.

Adds a second `structure_dump` implementation built on Oracle's
`DBMS_METADATA.GET_DDL` / `GET_DEPENDENT_DDL` alongside the existing
data-dictionary (`ALL_*`) one. The new backend lives in a separate
file and installs via `prepend DbmsMetadata`, so the legacy path in
`structure_dump.rb` is untouched.

Selectable globally via `OracleEnhancedAdapter.structure_dump_method`:

- `:auto` (default) — `:dbms_metadata` on Oracle 12.1+,
  `:data_dictionary` otherwise. The 12.1 floor is a project-policy
  version gate (the IDENTITY / EDITIONABLE / modern
  SET_TRANSFORM_PARAM era), exposed as `use_dbms_metadata_dump?`.
- `:dbms_metadata` — force the new path; raises `ArgumentError` on
  pre-12.1 (mirrors PR rsim#2576's `identifier_max_length: :long`
  fail-fast policy).
- `:data_dictionary` — force the original implementation. Remains
  explicitly selectable on 12.1+ as well.

Implementation walks `ALL_OBJECTS` per object_type rather than
hand-rolling per-kind queries; this naturally picks up
MATERIALIZED VIEW / TYPE / TRIGGER / SYNONYM / etc. without separate
code paths. FK constraints (REF_CONSTRAINT) are emitted in a single
trailing block; cross-table dependency order is left to the user (or
to a fresh-schema reload) rather than tracked here.

Output is informed by `mysqldump --no-data` / `pg_dump --schema-only`:

- `STORAGE` / `TABLESPACE` / `SEGMENT_ATTRIBUTES` / `EMIT_SCHEMA`
  suppressed via `DBMS_METADATA.SET_TRANSFORM_PARAM` so the dump is
  portable across installations and schemas. Oracle honors these for
  TABLE / INDEX / SEQUENCE.
- `MATERIALIZED VIEW` DDL is an exception: Oracle does not honour the
  same suppression on MV (object_type-specific override does not work
  either), so MV DDL retains physical attributes (PCTFREE, TABLESPACE,
  SEGMENT CREATION, etc.) as Oracle emits them. Since MVs aren't
  creatable through Rails' standard migration helpers anyway, no
  post-processing pass is added — Oracle's output is used as-is.
- `REF_CONSTRAINTS` emitted as separate `ALTER TABLE … ADD
  CONSTRAINT` statements after all tables.
- Inline constraints via Oracle's `CONSTRAINTS=TRUE` default; UNIQUE
  backing indexes that DBMS_METADATA already inlines into the table
  DDL are filtered out so they aren't emitted twice.
- `STATEMENT_TOKEN` separation preserved (Oracle's `SQLTERMINATOR=FALSE`
  default) so `execute_structure_dump` splits as before.
- `primary_key_trigger:` (rsim#2615) row triggers picked up via the
  TRIGGER object_type pass.
- COMMENT ON TABLE / COMMENT ON COLUMN queried directly because
  `GET_DEPENDENT_DDL("COMMENT")` is unreliable.
- `structure_dump_db_stored_code` selects 'PROCEDURE', 'PACKAGE',
  'FUNCTION', 'TYPE' — `GET_DDL("PACKAGE", ...)` already returns
  spec + body, so selecting 'PACKAGE BODY' would emit the body twice.

Specs follow the `pg_dump` / `mysqldump` testing convention: assert
output shape rather than exact byte-level DDL. A schema-option spec
verifies that `:schema` connection-time switching is honored
(structure_dump walks `SYS_CONTEXT('userenv', 'current_schema')`,
not the connecting user). The existing `structure_dump_spec` pins
`:data_dictionary` in `before(:all)` so its exact-DDL assertions
still apply to the legacy backend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the use-dbms-metadata-for-structure-dump-v2 branch from e0aaa5c to be13c01 Compare May 3, 2026 14:59
Closes rsim#2513.

Adds a second `structure_dump` implementation built on Oracle's
`DBMS_METADATA.GET_DDL` / `GET_DEPENDENT_DDL` alongside the existing
data-dictionary (`ALL_*`) one. The new backend lives in a separate
file and installs via `prepend DbmsMetadata`, so the legacy path in
`structure_dump.rb` is untouched.

Selectable globally via `OracleEnhancedAdapter.structure_dump_method`:

- `:auto` (default) — `:dbms_metadata` on Oracle 12.1+,
  `:data_dictionary` otherwise. The 12.1 floor is a project-policy
  version gate (the IDENTITY / EDITIONABLE / modern
  SET_TRANSFORM_PARAM era), exposed as `use_dbms_metadata_dump?`.
- `:dbms_metadata` — force the new path; raises `ArgumentError` on
  pre-12.1 (mirrors PR rsim#2576's `identifier_max_length: :long`
  fail-fast policy).
- `:data_dictionary` — force the original implementation. Remains
  explicitly selectable on 12.1+ as well.

Implementation walks `ALL_OBJECTS` per object_type rather than
hand-rolling per-kind queries; this naturally picks up
MATERIALIZED VIEW / TYPE / TRIGGER / SYNONYM / etc. without separate
code paths. FK constraints (REF_CONSTRAINT) are emitted in a single
trailing block; cross-table dependency order is left to the user (or
to a fresh-schema reload) rather than tracked here.

Output is informed by `mysqldump --no-data` / `pg_dump --schema-only`:

- `STORAGE` / `TABLESPACE` / `SEGMENT_ATTRIBUTES` / `EMIT_SCHEMA`
  suppressed via `DBMS_METADATA.SET_TRANSFORM_PARAM` so the dump is
  portable across installations and schemas. Oracle honors these for
  TABLE / INDEX / SEQUENCE.
- `MATERIALIZED VIEW` DDL is an exception: Oracle does not honour the
  same suppression on MV (object_type-specific override does not work
  either), so MV DDL retains physical attributes (PCTFREE, TABLESPACE,
  SEGMENT CREATION, etc.) as Oracle emits them. Since MVs aren't
  creatable through Rails' standard migration helpers anyway, no
  post-processing pass is added — Oracle's output is used as-is.
- `REF_CONSTRAINTS` emitted as separate `ALTER TABLE … ADD
  CONSTRAINT` statements after all tables.
- Inline constraints via Oracle's `CONSTRAINTS=TRUE` default; UNIQUE
  backing indexes that DBMS_METADATA already inlines into the table
  DDL are filtered out so they aren't emitted twice.
- `STATEMENT_TOKEN` separation preserved (Oracle's `SQLTERMINATOR=FALSE`
  default) so `execute_structure_dump` splits as before.
- `primary_key_trigger:` (rsim#2615) row triggers picked up via the
  TRIGGER object_type pass.
- COMMENT ON TABLE / COMMENT ON COLUMN queried directly because
  `GET_DEPENDENT_DDL("COMMENT")` is unreliable.
- `structure_dump_db_stored_code` selects 'PROCEDURE', 'PACKAGE',
  'FUNCTION', 'TYPE' — `GET_DDL("PACKAGE", ...)` already returns
  spec + body, so selecting 'PACKAGE BODY' would emit the body twice.

Specs follow the `pg_dump` / `mysqldump` testing convention: assert
output shape rather than exact byte-level DDL. A schema-option spec
verifies that `:schema` connection-time switching is honored
(structure_dump walks `SYS_CONTEXT('userenv', 'current_schema')`,
not the connecting user). The existing `structure_dump_spec` pins
`:data_dictionary` in `before(:all)` so its exact-DDL assertions
still apply to the legacy backend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yahonda yahonda force-pushed the use-dbms-metadata-for-structure-dump-v2 branch from be13c01 to c567c23 Compare May 3, 2026 15:22
@yahonda yahonda merged commit 9c25e2c into rsim:master May 3, 2026
12 checks passed
@yahonda yahonda deleted the use-dbms-metadata-for-structure-dump-v2 branch May 3, 2026 15:36
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Replaces the catalog-based `resolve_data_source_name` (single
all_objects query + secondary all_synonyms lookup since rsim#2521) with a
`DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private and
public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Implementation:
- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`).
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed"`). Previously `%("#{part}")` would
  re-quote an already-quoted part, producing `""test_Mixed""` and
  raising ORA-01741.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.

Benchmark script (`script/benchmark_describe.rb`) carried over from
the POC reference at rsim#2566.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Replaces the catalog-based `resolve_data_source_name` (single
all_objects query + secondary all_synonyms lookup since rsim#2521) with a
`DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private and
public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Implementation:
- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`).
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed"`). Previously `%("#{part}")` would
  re-quote an already-quoted part, producing `""test_Mixed""` and
  raising ORA-01741.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.

Benchmark script (`script/benchmark_describe.rb`) carried over from
the POC reference at rsim#2566.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Replaces the catalog-based `resolve_data_source_name` (single
all_objects query + secondary all_synonyms lookup since rsim#2521) with a
`DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private and
public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Implementation:
- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`).
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed"`). Previously `%("#{part}")` would
  re-quote an already-quoted part, producing `""test_Mixed""` and
  raising ORA-01741.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.

Benchmark script (`script/benchmark_describe.rb`) carried over from
the POC reference at rsim#2566.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Replaces the catalog-based `resolve_data_source_name` (single
all_objects query + secondary all_synonyms lookup since rsim#2521) with a
`DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private and
public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Implementation:
- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`).
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed"`). Previously `%("#{part}")` would
  re-quote an already-quoted part, producing `""test_Mixed""` and
  raising ORA-01741.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.

Benchmark script (`script/benchmark_describe.rb`) carried over from
the POC reference at rsim#2566.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 3, 2026
Replaces the catalog-based `resolve_data_source_name` (single
all_objects query + secondary all_synonyms lookup since rsim#2521) with a
`DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private and
public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Implementation:
- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`).
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed"`). Previously `%("#{part}")` would
  re-quote an already-quoted part, producing `""test_Mixed""` and
  raising ORA-01741.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.

Benchmark script (`script/benchmark_describe.rb`) carried over from
the POC reference at rsim#2566.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 4, 2026
Replaces the catalog-based `resolve_data_source_name` (single
all_objects query + secondary all_synonyms lookup since rsim#2521) with a
`DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private and
public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Implementation:
- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`).
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed"`). Previously `%("#{part}")` would
  re-quote an already-quoted part, producing `""test_Mixed""` and
  raising ORA-01741.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.

Benchmark script (`script/benchmark_describe.rb`) carried over from
the POC reference at rsim#2566.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 4, 2026
Replaces the catalog-based `resolve_data_source_name` (single
all_objects query + secondary all_synonyms lookup since rsim#2521) with a
`DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private and
public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Implementation:
- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`).
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed"`). Previously `%("#{part}")` would
  re-quote an already-quoted part, producing `""test_Mixed""` and
  raising ORA-01741.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.

Benchmark script (`script/benchmark_describe.rb`) carried over from
the POC reference at rsim#2566.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 4, 2026
Replaces the catalog-based `resolve_data_source_name` (single
all_objects query + secondary all_synonyms lookup since rsim#2521) with a
`DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private and
public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Implementation:
- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`).
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed"`). Previously `%("#{part}")` would
  re-quote an already-quoted part, producing `""test_Mixed""` and
  raising ORA-01741.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.

Benchmark script (`script/benchmark_describe.rb`) carried over from
the POC reference at rsim#2566.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 4, 2026
Replaces the catalog-based `resolve_data_source_name` (single
all_objects query + secondary all_synonyms lookup since rsim#2521) with a
`DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private and
public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Implementation:
- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`).
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed"`). Previously `%("#{part}")` would
  re-quote an already-quoted part, producing `""test_Mixed""` and
  raising ORA-01741.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.

Benchmark script (`script/benchmark_describe.rb`) carried over from
the POC reference at rsim#2566.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 4, 2026
Replaces the catalog-based `resolve_data_source_name` (single
all_objects query + secondary all_synonyms lookup since rsim#2521) with a
`DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private and
public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Implementation:
- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`).
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed"`). Previously `%("#{part}")` would
  re-quote an already-quoted part, producing `""test_Mixed""` and
  raising ORA-01741.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.

Benchmark script (`script/benchmark_describe.rb`) carried over from
the POC reference at rsim#2566.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 4, 2026
Replaces the catalog-based `resolve_data_source_name` (single
`all_objects` query + secondary `all_synonyms` lookup since rsim#2521)
with a `DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private
and public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Numbers measured against an out-of-tree benchmark script (kept on the
POC branch rsim#2566); the gem itself does not ship a benchmark file.

Implementation:

- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`). Both wrap the call in `with_retry` so a
  stale connection is marked dead consistently with the rest of the
  adapter.
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed_Case_Desc"`) and doubles embedded
  `"` characters in unquoted parts (Oracle's `""` escape). Without
  this, `%("#{part}")` would re-quote an already-quoted part,
  producing `""test_Mixed""` and raising ORA-01741.
- `split_dotted_name` is a quote-aware splitter so `"Foo.Bar"`
  (quoted dot) survives intact, including the `""` escape sequence
  inside a quoted identifier.
- Malformed dotted names with empty parts (`schema..table`) are
  rejected with `ArgumentError` before reaching NAME_RESOLVE.
- DB-link names (`name@db_link`) are rejected up-front with
  `ArgumentError` — `data_source_exists?` is not the right entry
  point for resolving remote objects.
- Circular synonyms surface as ORA-00980 from NAME_RESOLVE, wrapped
  as `OracleEnhanced::ConnectionException`. No Ruby-side stack
  overflow, clean error.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.
- The new path runs through an anonymous PL/SQL block (OCI) /
  `CallableStatement` (JDBC), bypassing `select_one`. The
  `sql.active_record` SCHEMA notification is preserved via explicit
  `instrumenter.instrument`, so logging and other subscribers keep
  seeing the describe call.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
yahonda added a commit to yahonda/oracle-enhanced that referenced this pull request May 4, 2026
Replaces the catalog-based `resolve_data_source_name` (single
`all_objects` query + secondary `all_synonyms` lookup since rsim#2521)
with a `DBMS_UTILITY.NAME_RESOLVE` call. NAME_RESOLVE chases private
and public synonyms inside the PL/SQL engine and returns
`[owner, object_name]` in one round trip, so the work does not depend
on `ALL_*` dictionary-view cost — the failure mode that rsim#2521's
secondary `all_synonyms` lookup leaves behind on synonym-heavy
schemas.

Now that PR rsim#2513 has landed `DBMS_METADATA.GET_DDL` in the adapter,
using another `DBMS_*` package is no longer a style break.

Benchmark on Oracle 23ai with a 1000-object fixture (700 tables +
100 views + 100 private synonyms + 100 public synonyms):

| case             | master   | this PR  | speedup |
|------------------|---------:|---------:|--------:|
| tables           |  0.384ms |  0.300ms |   1.3x  |
| views            |  0.364ms |  0.303ms |   1.2x  |
| private synonyms | 13.031ms |  0.447ms |    29x  |
| public synonyms  |  1.723ms |  0.454ms |   3.8x  |
| all mixed        |  0.578ms |  0.265ms |   2.2x  |

Numbers measured against an out-of-tree benchmark script (kept on the
POC branch rsim#2566); the gem itself does not ship a benchmark file.

Implementation:

- OUT-bind plumbing on both the OCI8 path (anonymous PL/SQL block
  with named binds) and the JDBC path (`CallableStatement` +
  `registerOutParameter`). Both wrap the call in `with_retry` so a
  stale connection is marked dead consistently with the rest of the
  adapter.
- `normalize_name_for_name_resolve` preserves case for already-quoted
  identifiers (e.g. `"test_Mixed_Case_Desc"`) and doubles embedded
  `"` characters in unquoted parts (Oracle's `""` escape). Without
  this, `%("#{part}")` would re-quote an already-quoted part,
  producing `""test_Mixed""` and raising ORA-01741.
- `split_dotted_name` is a quote-aware splitter so `"Foo.Bar"`
  (quoted dot) survives intact, including the `""` escape sequence
  inside a quoted identifier.
- Malformed identifiers raise `ArgumentError` up-front instead of
  being silently mangled and rejected by Oracle as an unhelpful
  `ConnectionException`. Three shapes are caught:
  * empty parts (`schema..table`, `.foo`, `foo.`)
  * three or more parts (`schema."table".extra` — NAME_RESOLVE with
    `context = 0` only accepts `schema.object`)
  * half-quoted parts (`"foo`, `foo"` — opens a quoted identifier
    that never closes, or vice versa). Embedded `"` mid-part
    (`Test"x`) is still allowed; it goes through the quote-and-
    double-escape branch.
- DB-link names (`name@db_link`) are rejected up-front with
  `ArgumentError` — `data_source_exists?` is not the right entry
  point for resolving remote objects.
- Circular synonyms surface as ORA-00980 from NAME_RESOLVE, wrapped
  as `OracleEnhanced::ConnectionException`. No Ruby-side stack
  overflow, clean error.
- `resolve_data_source_name` keeps master's contract: raise
  `OracleEnhanced::ConnectionException` for a non-existent name so
  `data_source_exists?`'s `rescue` returns false.
- The new path runs through an anonymous PL/SQL block (OCI) /
  `CallableStatement` (JDBC), bypassing `select_one`. The
  `sql.active_record` SCHEMA notification is preserved via explicit
  `instrumenter.instrument`, so logging and other subscribers keep
  seeing the describe call.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant