drizzle-team / drizzle-team/drizzle-orm

[BUG]: SQLite pull crashes on any expression index, because .on() doesn't filter the keys the line above it does

Open
#6,237 0 comments 0 reactions 1 assignee Claimed by @RomanNabukhotnyi View on GitHub
bug bug/fixed-in-beta db/sqlite drizzle/kit
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

better-sqlite3@13.0.3, node:sqlite (Node 24.17.0)

### Describe the Bug

`pull` fails on any SQLite index over an expression. In `createTableIndexes` (`dialects/sqlite/typescript.ts`) the generated-name line filters expression keys out and the `.on()` line below it doesn't:

```ts
const columnNames = it.columns.filter((c) => !c.isExpression).map((c) => c.value);
...
statement += `.on(${it.columns.map((it) => `table.${withCasing(it.value, casing)}`).join(', ')})`;
```

`value` is null for an expression key. `introspect.ts` builds the column as `{ value: it.column, isExpression: it.cid === -2 }`, and `ii.name` from `pragma_index_info` is NULL exactly when `cid` is -2, so `withCasing(null)` throws. The row type says `value: string`, which is why tsc doesn't see it.

Smallest case:

```sql
CREATE TABLE t (id INTEGER PRIMARY KEY, code TEXT NOT NULL);
CREATE INDEX i ON t (lower(code));
```

```
[✓] 1 tables fetched
[✓] 2 columns fetched
[✓] 1 indexes fetched
[✓] 0 foreign keys fetched
[✓] 0 check constraints fetched
[✓] 0 views fetched
Failed to connect to or introspect the SQLite database
```

The real error, from logging the `catch` that produces that last line:

```
TypeError: Cannot read properties of null (reading 'camelCase')
at withCasing$2 (bin.cjs:155408:58)
at bin.cjs:155593:56
at Array.map ()
at createTableIndexes$2 (bin.cjs:155593:35)
at ddlToTypeScript$2 (bin.cjs:155457:18)
at handle$7 (bin.cjs:155688:14)
```

Which shapes hit it, same table:

| index | result |
| --- | --- |
| `CREATE INDEX i ON t (lower(code))` | crash |
| `CREATE UNIQUE INDEX i ON t (lower(code))` | crash |
| `CREATE INDEX i ON t (lower(code), id)` | crash |
| `CREATE UNIQUE INDEX i ON t (lower(code), id)` | `unexpected unique index 'i' with expression value: ...` |

Only the last row is #6159, and #6171 fixes that one in `fromDatabase`. The other three never reach that loop.

Filtering expression keys out of `.on()` stops the crash, but it drops the index to its plain columns, and to nothing at all for the first two. `IndexColumn` is `SQLiteColumn | SQL`, so the shape that round-trips is:

```ts
index("i").on(sql`lower(code)`)
```

The expression text isn't in the row to write that with, though. `pragma_index_info` gives the null and nothing else, so it means reading the index's own DDL, which is the same correlated subquery on `sqlite_master` that #6236 is adding for the partial `WHERE`.

Separate, but it's what made this expensive to find: the `catch` around SQLite pull rewrites any untyped error into `Failed to connect to or introspect the SQLite database` and lists `better-sqlite3`, `bun`, `@libsql/client`, `@tursodatabase/database`, `node:sqlite`. The driver had connected and printed six fetch lines directly above. Letting the error through, or a flag that does, would save the bisect.

Same on 1.0.0-rc.5-ab785fc. `0.31.10` doesn't crash, but writes `index("i").on()` with no arguments, which doesn't typecheck against `on(...columns: [IndexColumn, ...IndexColumn[]])`, so the null wants handling on that line either way. The same null reaching the casing function is open for MySQL as #4499 and #5546 (PR #5548). A guard in `withCasing` covers the crash in every dialect, but each one finds the expression text somewhere different, so keeping the index is separate work.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.