Skip to content

Phase 2: flip add_index unique: true default in Migration[8.2]+ [DRAFT — depends on Rails per-adapter-migration-compatibility]#2710

Draft
yahonda wants to merge 1 commit into
rsim:masterfrom
yahonda:deprecate-implicit-unique-constraint-2702-phase2
Draft

Phase 2: flip add_index unique: true default in Migration[8.2]+ [DRAFT — depends on Rails per-adapter-migration-compatibility]#2710
yahonda wants to merge 1 commit into
rsim:masterfrom
yahonda:deprecate-implicit-unique-constraint-2702-phase2

Conversation

@yahonda
Copy link
Copy Markdown
Collaborator

@yahonda yahonda commented May 7, 2026

Summary

Closes #2702 (Phase 2).

This PR is a draft because it depends on the unmerged Rails extension point AbstractAdapter#migration_compatibility_module_for(migration_class) plus the Migration::Compatibility::Versioned convention from yahonda/rails per-adapter-migration-compatibility — same dependency as #2596. Once that lands on rails/rails#main:

  1. Switch Gemfile back to rails/rails#main
  2. Mark this PR ready for review

In Phase 2 of the implicit-UNIQUE-CONSTRAINT deprecation, the global default flips from "implicit constraint + warning" (Phase 1, shipped in #2709) to "create the unique index only", matching Rails-core PostgreSQL / MySQL / SQLite. Existing migrations declared at Migration[8.1] or earlier opt back into the legacy implicit-constraint behavior via a MigrationCompatibility::V8_1 module so they keep working unchanged.

Behavior matrix

Caller Phase 1 (current master) Phase 2 (this PR)
Migration[8.2] (Current) — add_index unique: true implicit constraint + warning index only, no warning
Migration[8.1] and earlier — add_index unique: true implicit constraint + warning implicit constraint + warning (preserved by V8_1)
Schema.define { add_index unique: true } implicit constraint + warning index only, no warning
Direct connection.add_index unique: true implicit constraint + warning index only, no warning
OracleEnhancedAdapter.add_index_unique_creates_constraint = true (explicit) — any caller implicit constraint + warning implicit constraint + warning (explicit opt-in to legacy)
Functional unique index — any caller index only (no warning) index only (no warning) — unchanged

Changes

Production

  • OracleEnhancedAdapter.add_index_unique_creates_constraint default flips from true to false. Documentation updated to reflect the Phase 2 default and the explicit opt-in for legacy behavior.
  • New lib/active_record/connection_adapters/oracle_enhanced/migration_compatibility.rb:
    • MigrationCompatibility::V8_1 module wraps add_index and create_table so Migration[8.1] and earlier migrations re-enable the implicit-constraint path.
    • Thread-local key (:__oracle_enhanced_implicit_unique_constraint) is encapsulated as a private_constant of MigrationCompatibility, with with_implicit_unique_constraint_enabled / implicit_unique_constraint_enabled? accessors. Phase 3 deletes the whole module — key, accessors, and V8_1 — in one piece without leaving observable state behind in Thread.current.
  • OracleEnhancedAdapter#migration_compatibility_module_for(migration_class) returns MigrationCompatibility.module_for(migration_class), mirroring the PG / MySQL / SQLite adapters in per-adapter-migration-compatibility.
  • schema_statements.rb introduces an implicit_unique_constraint_active? private helper that ORs the global flag with the migration-version thread-local, used by both add_index and add_inline_unique_constraints.

Specs

  • New migration_compatibility_spec.rb covering:
    • Migration[8.2] (current) — no implicit constraint, no warning, both add_index and inline t.index paths
    • Migration[8.1] (legacy) — implicit constraint + warning captured via expect { ... }.to output(...).to_stderr, both paths
    • Explicit add_index_unique_creates_constraint = true — overrides the Migration[8.2] default
  • Mixed-strategy update of existing specs:
    • Legacy SQL emission tests (should add unique constraint only to the index where it was defined, should emit CREATE UNIQUE INDEX and ADD CONSTRAINT for inline t.index unique: true, produces the same SQL whether unique index is defined inline or via explicit add_index, the DBMS_METADATA path's does not emit a standalone CREATE INDEX for a UNIQUE constraint's backing index, and emits ALTER INDEX ... INVISIBLE for an INVISIBLE constraint-backed index) → wrapped in a new with_implicit_unique_constraint_enabled helper and labeled "(legacy implicit-constraint path)". These specs intentionally exercise the legacy code path; the helper makes that intent explicit.
    • Orthogonal-feature tests (remove_index, add_unique_constraint, schema dumper, and structure_dump's appends NOVALIDATE for foreign keys added with validate: false) that just needed an index + constraint pair as setup are migrated to add_unique_constraint. Forward-compatible: Phase 3 doesn't need to touch them.
    • The drops both the index and the same-name implicit constraint when add_index unique: true created them spec for remove_index keeps the legacy add_index unique: true setup via the helper, because that pairing is precisely what's being tested. (Per "let Oracle handle constraint-managed indexes" — this scenario only happens for the legacy path where the index is created explicitly via CREATE UNIQUE INDEX, not auto-created behind a constraint.)
  • spec_helper.rb adds ImplicitUniqueConstraintHelper#with_implicit_unique_constraint_enabled, included globally so any spec can opt-in to legacy.

Out of scope

  • remove_index against an index that was auto-created behind an add_unique_constraint still raises ORA-01418 because Oracle's automatic cleanup drops the index together with the constraint, then remove_index tries to drop the (already-gone) index. Per the design principle "let Oracle handle the index it auto-creates behind a constraint", the recovery path for that case is remove_unique_constraint :t, name: :n, not remove_index. Worth a friendly error in a follow-up issue if it bites users in practice.
  • Phase 3 (delete the flag, the helper, the V8_1 module, and the implicit-constraint code path) ships one major release after Phase 2 settles. Tracked in Deprecate implicit UNIQUE CONSTRAINT creation by add_index :col, unique: true #2702.

Test plan

  • bundle exec rspec spec/active_record/connection_adapters/oracle_enhanced/migration_compatibility_spec.rb (5 examples)
  • bundle exec rspec spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb -e "implicit constraint deprecation" (6 examples) — Phase 1 specs continue to pass; the explicit-flag-true ones still test the legacy path now
  • CI=1 bundle exec rspec spec/active_record/connection_adapters/oracle_enhanced/schema_statements_spec.rb spec/active_record/connection_adapters/oracle_enhanced/schema_dumper_spec.rb spec/active_record/connection_adapters/oracle_enhanced/structure_dump_spec.rb spec/active_record/connection_adapters/oracle_enhanced/migration_compatibility_spec.rb — 226 examples, 0 failures, 2 pre-existing pending
  • bundle exec rubocop — clean
  • Switch Gemfile back to rails/rails#main once per-adapter-migration-compatibility lands
  • Mark PR ready for review and run CI on full matrix

Related

🤖 Generated with Claude Code

@yahonda yahonda force-pushed the deprecate-implicit-unique-constraint-2702-phase2 branch from fd16113 to 9c33a57 Compare May 7, 2026 05:44
@yahonda yahonda force-pushed the deprecate-implicit-unique-constraint-2702-phase2 branch from 9c33a57 to 13dde32 Compare May 10, 2026 09:29
@yahonda yahonda force-pushed the deprecate-implicit-unique-constraint-2702-phase2 branch from 13dde32 to cca3385 Compare May 19, 2026 14:44
@yahonda yahonda changed the title Phase 2: flip add_index unique: true default in Migration[8.2]+ [DRAFT — depends on Rails another-33269] Phase 2: flip add_index unique: true default in Migration[8.2]+ [DRAFT — depends on Rails per-adapter-migration-compatibility] May 19, 2026
Closes rsim#2702 (Phase 2). Depends on Rails core extension yahonda/rails
branch:per-adapter-migration-compatibility (`migration_compatibility_module_for`).

In Phase 2 of the implicit-UNIQUE-CONSTRAINT deprecation, the global
default flips from "implicit constraint + warning" (Phase 1) to "create
the unique index only", matching Rails-core PostgreSQL/MySQL/SQLite.
Existing migrations declared at `Migration[8.1]` or earlier opt back
into the legacy implicit-constraint behavior via a
`MigrationCompatibility::V8_1` module so they keep working unchanged.

* `OracleEnhancedAdapter.add_index_unique_creates_constraint` default
  flips from `true` to `false`. Set to `true` explicitly to force the
  legacy behavior project-wide.
* New `OracleEnhanced::MigrationCompatibility::V8_1` module overrides
  `add_index` and `create_table` to set a private thread-local that the
  adapter consults, opting old migrations back into the implicit-
  constraint path. The thread-local is encapsulated as a private
  constant of `MigrationCompatibility` so Phase 3 can remove the entire
  module — key included — without leaving observable state behind in
  `Thread.current`.
* New `migration_compatibility_module_for(migration_class)` on
  `OracleEnhancedAdapter` wires the compat module into Rails' Migration
  dispatch.
* Gemfile pins `activerecord` to
  `yahonda/rails branch:per-adapter-migration-compatibility` for the
  duration of Phase 2 (will be reverted to `rails/rails main` once the
  branch is merged).

Specs:

* New `migration_compatibility_spec.rb` covering Migration[8.2] (no
  implicit constraint, no warning), Migration[8.1] (implicit constraint
  + warning, both add_index and inline t.index paths), and explicit
  global flag overriding the migration default.
* Existing legacy SQL emission specs in `schema_statements_spec.rb`
  that assert on the implicit constraint output (`should add unique
  constraint only to the index where it was defined`, `should emit
  CREATE UNIQUE INDEX and ADD CONSTRAINT for inline t.index unique:
  true`, `produces the same SQL whether unique index is defined inline
  or via explicit add_index`) are wrapped in
  `with_implicit_unique_constraint_enabled` and labeled "(legacy
  implicit-constraint path)". The helper is a thin wrapper around the
  global flag, defined in `spec/spec_helper.rb`.
* Orthogonal-feature specs in `schema_statements_spec.rb` and
  `schema_dumper_spec.rb` that needed an index+constraint pair as
  setup are migrated to `add_unique_constraint`. The remaining
  remove_index spec that documents the legacy add_index → remove_index
  pairing keeps the legacy setup via the helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@yahonda yahonda force-pushed the deprecate-implicit-unique-constraint-2702-phase2 branch from cca3385 to bcdd815 Compare May 19, 2026 15:46
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.

Deprecate implicit UNIQUE CONSTRAINT creation by add_index :col, unique: true

1 participant