Skip to content

[SQLite-kit] Keep column UNIQUE when the same column also has a plain index - #6063

Open
soya-miyoshi wants to merge 1 commit into
drizzle-team:betafrom
soya-miyoshi:fix/sqlite-unique-dropped-by-plain-index
Open

[SQLite-kit] Keep column UNIQUE when the same column also has a plain index#6063
soya-miyoshi wants to merge 1 commit into
drizzle-team:betafrom
soya-miyoshi:fix/sqlite-unique-dropped-by-plain-index

Conversation

@soya-miyoshi

Copy link
Copy Markdown

Fixes #6060

Problem

In the SQLite schema serializer, a column declared with .unique() silently loses its UNIQUE constraint when the same column also has a plain, non-unique index() declared on it. Nothing is emitted in its place — no inline UNIQUE, no CREATE UNIQUE INDEX — so uniqueness disappears from the generated DDL and duplicate rows become insertable.

This is most visible on the push table-recreate path (__new_<table> + INSERT ... SELECT + DROP + RENAME), where an existing UNIQUE guarantee is dropped without any prompt or warning. It affects generate too, since both share the serializer.

A sibling .unique() column without a plain index keeps its constraint, which isolates the trigger to the .unique() + index() combination on the same column — a very common pattern (unique business key + a covering query index).

Root cause

fromDrizzleSchema in drizzle-kit/src/dialects/sqlite/drizzle.ts computes a hasUniqueIndex flag whose purpose is to avoid emitting an inline UNIQUE on top of a column that is already covered by a uniqueIndex() (which would duplicate the constraint). The predicate, however, never checks whether the matched index is actually unique — it matches any single-column index on that column. So a plain index() suppresses the inline UNIQUE just as a uniqueIndex() would, but without providing any replacement enforcement.

Fix

Ignore non-unique indexes in that lookup:

const hasUniqueIndex = Boolean(it.config.indexes.find((item) => {
  const i = item.config;
  if (!i.unique) return false; // a plain index enforces nothing — must not suppress UNIQUE
  const column = i.columns.length === 1 ? i.columns[0] : null;
  return column && !is(column, SQL) && column.name === name;
}));

Deduplication against a real uniqueIndex() is unchanged; only plain indexes stop suppressing the constraint.

Tests

Added coverage in drizzle-kit/tests/sqlite/sqlite-constraints.test.ts for the bug and its neighbors:

  • .unique() + plain index() on the same column → keeps UNIQUE
  • .unique() + real uniqueIndex() on the same column → still deduplicated (no double constraint)
  • sibling .unique() column without an index → unaffected

Full file passes:

✓ tests/sqlite/sqlite-constraints.test.ts (59 tests)
Test Files  1 passed (1)
     Tests  59 passed (59)

Notes

  • Targeted at beta (1.0.0-rc.4), which is where the reported code path (src/dialects/sqlite/drizzle.ts) lives.

In the SQLite schema serializer, a column declared with `.unique()` loses its
UNIQUE constraint when the same column also has a plain, non-unique `index()`.
Nothing is emitted in its place, so uniqueness silently disappears from the
generated DDL and duplicates become insertable.

The `hasUniqueIndex` check exists to avoid emitting an inline UNIQUE on top of
a unique index that already enforces the same thing. The predicate, however,
never looks at whether the index is unique — it matches any single-column index
on that column, so a plain index suppresses the constraint too.

Fixed by ignoring non-unique indexes in that lookup. Deduplication against a
real `uniqueIndex()` is unchanged.

Affects both `generate` and `push` (they share this serializer); the issue was
reported against the `push` table-recreate path.

Fixes drizzle-team#6060
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