Skip to content

[BUG]: Upstash cache broken with libSQL driver — Row objects lose numeric indices after JSON roundtrip #5561

@olehreznichenko

Description

@olehreznichenko

Report hasn't been filed before.

  • 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:

// 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:

// libsql/session.js — values()
return (client.execute(stmt)).then(({ rows }) => rows);
// ↑ rows is Array<Row> — 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:

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:

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

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:

 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<Array<Value>> 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions