drizzle-team / drizzle-team/drizzle-orm
[BUG]: Upstash cache broken with libSQL driver — Row objects lose numeric indices after JSON roundtrip
- 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?
0.45.2
### What version of `drizzle-kit` are you using?
0.31.10
### Other packages
@libsql/client@0.17.2, @upstash/redis@1.37.0
### Describe the Bug
When using `upstashCache()` with the `libsql` driver (either with `global: true` or explicit `.$withCache()`), queries succeed on the **first request** (cache miss → DB hit → result stored in cache), but **crash on the second request** (cache hit) with:
```
SyntaxError: "undefined" is not valid JSON
```
This happens because libSQL's Row objects have a special structure that doesn't survive JSON serialization through Redis.
### Root Cause
LibSQL's `Row` objects are **plain objects with both named keys AND numeric indices + `length`**:
```js
// libSQL Row (from @libsql/client execute())
Row {
0: '{}', // numeric index
1: 1705312200, // numeric index
baseline: '{}', // named key
created_at: 1705312200, // named key
length: 2 // array-like length
}
```
Drizzle's `values()` method in `libsql/session.js` caches these Row objects via `queryWithCache`:
```js
// libsql/session.js — values()
return (client.execute(stmt)).then(({ rows }) => rows);
// ↑ rows is Array — each Row has numeric + named keys
```
When the upstash cache stores this via `HSET`, `@upstash/redis` serializes with `JSON.stringify`. Since Row is **not** an Array instance (`Array.isArray(row) === false`), `JSON.stringify` serializes it as a **plain object with only named keys**:
```js
JSON.stringify(row)
// → '{"baseline":"{}","created_at":1705312200}'
// ❌ Numeric indices (0, 1) and length are LOST
```
On cache hit, the deserialized row is a plain object **without numeric indices**. Then `mapAllResult` calls:
```js
Array.prototype.slice.call(row).map(v => normalizeFieldValue(v))
// → [] (empty array!) because the object has no numeric indices or length
```
Then `mapResultRow` reads `row[0]` → `undefined`, and the column decoder calls `JSON.parse(undefined)` → 💥
### Proof
```js
const { createClient } = require('@libsql/client');
const c = createClient({ url: 'file:test.db' });
await c.execute('CREATE TABLE t(id TEXT, data TEXT)');
await c.execute("INSERT INTO t VALUES('x', '{\"a\":1}')");
const { rows } = await c.execute('SELECT * FROM t');
const row = rows[0];
console.log(Array.isArray(row)); // false
console.log(JSON.stringify(row)); // {"id":"x","data":"{\"a\":1}"}
console.log(Array.prototype.slice.call(row)); // ['x', '{"a":1}'] ✅
const parsed = JSON.parse(JSON.stringify(row));
console.log(Array.prototype.slice.call(parsed)); // [] ❌ EMPTY
```
### Steps to Reproduce
1. Set up a libSQL/Turso database with Drizzle ORM
2. Configure `upstashCache()` with valid Upstash Redis credentials
3. Add `.$withCache()` to any `db.select()` query (or use `global: true`)
4. Execute the query — **works** (cache miss, fetches from DB)
5. Execute the same query again — **crashes** (cache hit, Row deserialization broken)
### Expected Behavior
Cached queries should return the same results as uncached queries. The cache layer should properly serialize/deserialize libSQL Row objects so that `Array.prototype.slice.call(row)` works after a JSON roundtrip.
### Suggested Fix
In `libsql/session.js`, the `values()` method should convert Row objects to plain arrays **before** passing them to `queryWithCache`:
```diff
async values(placeholderValues) {
// ...
return await this.queryWithCache(this.query.sql, params, async () => {
const stmt = { sql: this.query.sql, args: params };
return (this.tx ? this.tx.execute(stmt) : this.client.execute(stmt))
- .then(({ rows }) => rows);
+ .then(({ rows }) => rows.map(row => Array.prototype.slice.call(row)));
});
}
```
This ensures the cached data is a plain `Array>` that survives JSON roundtrip, matching what `mapAllResult` expects.
### Environment
- **Database**: Turso (libSQL HTTP) + local SQLite via `@libsql/client`
- **Driver**: `drizzle-orm/libsql`
- **Cache**: `drizzle-orm/cache/upstash`
- **Runtime**: Node.js v24
- **NOT a monorepo issue** — reproducible in a standalone project
Contributor guide
Assessment
This issue has not been assessed yet.