drizzle-team / drizzle-team/drizzle-orm
[BUG]: SQLite push table recreate silently drops UNIQUE from a column that also has a plain index
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
### Report hasn't been filed before.
- [x] I have verified that the bug I'm about to report hasn't been filed before.
### What version of `drizzle-orm` are you using?
1.0.0-rc.4
### What version of `drizzle-kit` are you using?
1.0.0-rc.4
### Other packages
@libsql/client@0.17.4
### Describe the Bug
When `drizzle-kit push` (SQLite) recreates a table (`__new_` + `INSERT INTO ... SELECT` + `DROP` + `RENAME`), a column declared `.unique()` **loses its UNIQUE constraint if that same column also has a plain (non-unique) `index()`** declared on it. The emitted `CREATE TABLE __new_...` omits the inline `UNIQUE` for that column and no `CREATE UNIQUE INDEX` is emitted either — uniqueness is silently destroyed. The plan applies **without any prompt or warning**.
A sibling `.unique()` column *without* a plain index keeps its constraint, which isolates the trigger to the `.unique()` + `index()` combination.
**Reproduction**
1. Create the database out-of-band (simulates an existing production DB):
```sql
-- sqlite3 a.db
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sku TEXT NOT NULL UNIQUE,
batch_id INTEGER NOT NULL UNIQUE,
label TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
CREATE INDEX orders_sku_idx ON orders(sku);
INSERT INTO orders (sku, batch_id, label) VALUES ('SKU-1', 100, 'first'), ('SKU-2', 200, 'second');
```
2. `schema.ts` (the missing `created_at` default is only there to trigger a table recreate — any recreate trigger reproduces it):
```ts
import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
export const orders = sqliteTable(
'orders',
{
id: integer('id').primaryKey({ autoIncrement: true }),
sku: text('sku').notNull().unique(),
batchId: integer('batch_id').notNull().unique(),
label: text('label'),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
},
(t) => [index('orders_sku_idx').on(t.sku)]
);
```
3. `drizzle.config.ts`:
```ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './schema.ts',
out: './out',
dialect: 'sqlite',
dbCredentials: { url: 'file:a.db' },
});
```
4. `drizzle-kit push --verbose` — emitted SQL (applied with no prompt):
```sql
PRAGMA foreign_keys=OFF;
CREATE TABLE `__new_orders` (
`id` integer PRIMARY KEY AUTOINCREMENT,
`sku` text NOT NULL, -- UNIQUE is GONE
`batch_id` integer NOT NULL UNIQUE, -- kept (no plain index on this one)
`label` text,
`created_at` integer NOT NULL
);
INSERT INTO `__new_orders`(`id`, `sku`, `batch_id`, `label`, `created_at`) SELECT `id`, `sku`, `batch_id`, `label`, `created_at` FROM `orders`;
DROP TABLE `orders`;
ALTER TABLE `__new_orders` RENAME TO `orders`;
PRAGMA foreign_keys=ON;
CREATE INDEX `orders_sku_idx` ON `orders` (`sku`);
```
5. Proof of silent integrity loss:
```sql
INSERT INTO orders (sku, batch_id, created_at) VALUES ('SKU-1', 999, 0);
-- succeeds; SELECT count(*) FROM orders WHERE sku='SKU-1' → 2
```
**Expected**: the recreated table keeps `sku` UNIQUE (inline or via a unique index), or at minimum push warns before removing a uniqueness guarantee.
**Actual**: uniqueness silently removed; duplicate rows accepted afterwards. On a real production database this destroys unique constraints across every table that pairs `.unique()` with a query index on the same column (a very common pattern: unique business key + covering index).
Possibly related (but distinct — those are re-add/duplicate-statement issues, not silent loss): #4152, #3574, #2888, #5955.
Contributor guide
Assessment
This issue has not been assessed yet.