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
false → add_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.
Background
Since #2610 / #2612,
add_index :t, :col, unique: true, name: :nemits both aCREATE UNIQUE INDEXand a trailingALTER TABLE ADD CONSTRAINT n UNIQUE (col) USING INDEX nfor 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, aadd_foreign_keyagainst an indexed-only column hitsORA-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 inadd_indexhas 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_indexshould 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::Deprecationwarning whenadd_indexenters the implicit-constraint branch, e.g.:Add a global opt-out for users who already prefer index-only:
false→add_index unique: trueonly emitsCREATE UNIQUE INDEX; no warningtrue(Phase 1 default) → current behavior + warningnil(default) → equivalent totruePhase 2 —
Migration[N+1]default flip (depends on Railsanother-33269)Following the precedent in #2596, register a
MigrationCompatibility::Versionedmodule so that:Migration[N]and earlier keep current implicit-constraint behaviorMigration[N+1]and later default to index-only (matching the global flag'sfalsevalue)Schema.defineand directconnection.create_tablekeep 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_constraintcattr_accessorwarn_implicit_unique_constraint_deprecationprivate helperadd_inline_unique_constraintspost-superdrain increate_tableALTER TABLE ... ADD CONSTRAINT ... USING INDEXemission inadd_indexOracleEnhanced.deprecator.silencewrap added toschema_definein spec_helperMigrationCompatibilitymodule from Phase 2 (no longer needed once the only behavior is "index only")End state:
add_index :col, unique: trueonly emitsCREATE UNIQUE INDEXon Oracle, achieving full parity with the Rails-core PostgreSQL / MySQL / SQLite adapters. Callers who need a constraint reach foradd_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
add_inline_unique_constraintspost-superloop increate_tableuntil Phase 2 lands. The deprecation warning fires from the same code path; the implementation stays.Related
add_unique_constraint(Closes Add add_unique_constraint / remove_unique_constraint / t.unique_constraint matching PostgreSQL adapter #2613). Released the explicit-constraint DSL.Migration::Compatibility::Versionedprecedent (depends on yahonda/railsanother-33269).Acceptance criteria
add_unique_constraintis called explicitly.MigrationCompatibilitymodule registeringV_<next>that flips the default. Specs coveringMigration[N+1]+,Migration[N]and earlier, andSchema.define. (Blocked byanother-33269.)add_index unique: trueemits onlyCREATE UNIQUE INDEX. Specs cover the new clean behavior; theschema_definedeprecator silence wrap is no longer needed.