Skip to content

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

@yahonda

Description

@yahonda

Background

Since #2610 / #2612, add_index :t, :col, unique: true, name: :n emits both a CREATE UNIQUE INDEX and a trailing ALTER TABLE ADD CONSTRAINT n UNIQUE (col) USING INDEX n for non-functional indexes. This adapter-specific behavior was originally introduced because Oracle requires a UNIQUE/PK CONSTRAINT (not just a UNIQUE INDEX) for FK targetability — without it, a add_foreign_key against an indexed-only column hits ORA-02270.

Now that #2701 has landed add_unique_constraint / t.unique_constraint / remove_unique_constraint, users have an explicit DSL for the constraint side. The implicit-constraint creation in add_index has become redundant and at odds with both Rails core's general convention (PostgreSQL / MySQL / SQLite all create only the index) and the principle of least surprise (add_index should add an index, not also a constraint).

Proposal

Three-phase deprecation:

Phase 1 — deprecation warning + global opt-out flag (no Rails core dependency) — delivered in #2709

Emit an ActiveSupport::Deprecation warning when add_index enters the implicit-constraint branch, e.g.:

add_index :t, :col, unique: true creates an implicit named UNIQUE constraint on Oracle. This implicit-constraint behavior will be removed in oracle-enhanced X.0. For an explicit constraint, call add_unique_constraint :t, :col, name: :n. For FK targetability without changing index identity, call both:
add_index :t, :col, unique: true
add_unique_constraint :t, :col

Add a global opt-out for users who already prefer index-only:

# config/initializers/oracle_enhanced.rb
ActiveRecord::ConnectionAdapters::OracleEnhancedAdapter.add_index_unique_creates_constraint = false
  • falseadd_index unique: true only emits CREATE UNIQUE INDEX; no warning
  • true (Phase 1 default) → current behavior + warning
  • nil (default) → equivalent to true

Phase 2 — Migration[N+1] default flip (depends on Rails another-33269)

Following the precedent in #2596, register a MigrationCompatibility::Versioned module so that:

  • Migration[N] and earlier keep current implicit-constraint behavior
  • Migration[N+1] and later default to index-only (matching the global flag's false value)

Schema.define and direct connection.create_table keep the flag's value as their source of truth, since they have no migration-version context.

Phase 3 — flag and implicit-constraint code path removal

After Phase 2 has had time to settle (one major release on the new default), remove the transition machinery entirely:

  • OracleEnhancedAdapter.add_index_unique_creates_constraint cattr_accessor
  • warn_implicit_unique_constraint_deprecation private helper
  • add_inline_unique_constraints post-super drain in create_table
  • The trailing ALTER TABLE ... ADD CONSTRAINT ... USING INDEX emission in add_index
  • The OracleEnhanced.deprecator.silence wrap added to schema_define in spec_helper
  • The MigrationCompatibility module from Phase 2 (no longer needed once the only behavior is "index only")

End state: add_index :col, unique: true only emits CREATE UNIQUE INDEX on Oracle, achieving full parity with the Rails-core PostgreSQL / MySQL / SQLite adapters. Callers who need a constraint reach for add_unique_constraint (which has been the explicit path since #2701).

This phase has no Rails-core dependency; it can ship one major release after Phase 2.

Out of scope

  • Removing the add_inline_unique_constraints post-super loop in create_table until Phase 2 lands. The deprecation warning fires from the same code path; the implementation stays.
  • Auto-rewriting historical migrations.

Related

Acceptance criteria

  • Phase 1 — deprecation warning + global flag, with specs covering on/off/default and that the warning does not fire when add_unique_constraint is called explicitly.
  • Phase 2 — MigrationCompatibility module registering V_<next> that flips the default. Specs covering Migration[N+1]+, Migration[N] and earlier, and Schema.define. (Blocked by another-33269.)
  • Phase 3 — flag, helper, and implicit-constraint code path are removed. add_index unique: true emits only CREATE UNIQUE INDEX. Specs cover the new clean behavior; the schema_define deprecator silence wrap is no longer needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions