drizzle-team / drizzle-team/drizzle-orm

[BUG]: SQLite push glues the table's FOREIGN KEY clause onto a generated-column ALTER ADD, breaking all inserts

Open
#6,061 2 comments 0 reactions 0 assignees View on GitHub
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) rebuilds a **generated column** via `ALTER TABLE ... DROP COLUMN` + `ALTER TABLE ... ADD` (e.g. because the live column is `STORED` and the schema declares it virtual, or the expression text differs), the emitted `ADD` statement **appends the table's foreign-key clause to the generated column**:

```sql
ALTER TABLE `line_items` DROP COLUMN `amount`;
ALTER TABLE `line_items` ADD `amount` real GENERATED ALWAYS AS (CAST(amount_cents AS REAL) / 100.0) VIRTUAL REFERENCES customers(id) ON DELETE CASCADE;
```

The `REFERENCES customers(id) ON DELETE CASCADE` belongs to a *different* column (`customer_id`). SQLite accepts the statement, creating a nonsense FK from the computed `amount` value to `customers.id`. From that moment **every INSERT into the table fails** with `FOREIGN KEY constraint failed` (the computed price is not a customer id), and `PRAGMA foreign_key_check` flags all existing rows.

**Reproduction**

1. Create the database out-of-band (simulates an existing production DB with a STORED generated column):

```sql
-- sqlite3 b.db
CREATE TABLE customers (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL);
CREATE TABLE line_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
amount_cents INTEGER NOT NULL DEFAULT 0,
amount REAL GENERATED ALWAYS AS (CAST(amount_cents AS REAL) / 100.0) STORED
);
INSERT INTO customers (name) VALUES ('acme');
INSERT INTO line_items (customer_id, amount_cents) VALUES (1, 1250);
```

2. `schema.ts` (declares the generated column virtual — the default — which triggers the drop/re-add):

```ts
import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';

export const customers = sqliteTable('customers', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
});

export const lineItems = sqliteTable('line_items', {
id: integer('id').primaryKey({ autoIncrement: true }),
customerId: integer('customer_id')
.notNull()
.references(() => customers.id, { onDelete: 'cascade' }),
amountCents: integer('amount_cents').notNull().default(0),
amount: real('amount').generatedAlwaysAs(sql`CAST(amount_cents AS REAL) / 100.0`),
});
```

3. `drizzle.config.ts`: `{ schema: './schema.ts', out: './out', dialect: 'sqlite', dbCredentials: { url: 'file:b.db' } }`

4. `drizzle-kit push --verbose` emits and applies (no prompt) the two statements shown above.

5. Post-apply state:

```
PRAGMA foreign_key_list(line_items);
-- (0, 0, 'customers', 'amount', 'id', 'NO ACTION', 'CASCADE', 'NONE') ← bogus FK on the generated column
-- (1, 0, 'customers', 'customer_id', 'id', 'NO ACTION', 'CASCADE', 'NONE')

INSERT INTO line_items (customer_id, amount_cents) VALUES (1, 33300);
-- Error: FOREIGN KEY constraint failed

PRAGMA foreign_key_check(line_items);
-- ('line_items', 1, 'customers', 0) ← existing rows now violate
```

6. A **second** `push` then has to recreate the whole table to remove the FK it just created (self-inflicted churn):

```sql
CREATE TABLE `__new_line_items` (... CONSTRAINT `fk_line_items_customer_id_customers_id_fk` FOREIGN KEY (`customer_id`) ...);
INSERT INTO `__new_line_items`(...) SELECT ... FROM `line_items`;
DROP TABLE `line_items`; ALTER TABLE `__new_line_items` RENAME TO `line_items`;
```

**Expected**: the generated-column `ADD` statement carries only the generated-column definition; the unrelated FK clause is never serialized onto it.

**Actual**: corrupt FK is created and the table's write path is broken until a subsequent full recreate.

Possibly related: #4147 (FK + column added together produces incorrect migration).

Contributor guide

Open the contributing guide

Research direction

Start with the drizzle-kit SQLite push path exercised by the provided schema.ts and drizzle.config.ts, focusing on the generated-column ALTER TABLE DROP/ADD sequence. Run the supplied reproduction with drizzle-kit push --verbose, inspect the emitted ADD statement and PRAGMA foreign_key_list(line_items), and confirm that the unrelated customer_id foreign key is absent and inserts succeed.

Written by the indexing model from the issue text.

Assessment

Tech stack
sqlite, typescript
Domain
database
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.