Skip to content

driver-sql (PG): JSON-field values are bound without JSON.stringify — non-empty arrays and bare strings 500, empty array silently stored as {} #10995

Description

@os-zhuang

Found live on the cloud control plane (cloud.objectos.ai, Postgres/Neon via driver-sql) while auditing browser console errors. Filed here per contract-first: the mechanism is the driver's, every PG-backed deployment inherits it.

Reproduction matrix — one PATCH, only value changes

sys_user_preference.value is a JSON-typed field. PATCH /api/v1/data/sys_user_preference/:id with:

body valueresult
{"a":1} (object)200
42 (number)200
{"items":[1,2]} (array nested in object)200
[{"type":"app",…}] (non-empty array)500 DATABASE_ERROR
"x" (string)500 DATABASE_ERROR
[] (empty array)200 — but silently stored as {} (see mechanism)

POST behaves identically (a create with an array value 500s).

Mechanism (inferred from the signature — verify at the driver)

The matrix is the classic node-postgres fingerprint for passing JSON-field values to the driver without JSON.stringify:

  • JS object → node-pg serialises to JSON text → valid jsonb ✓
  • JS array → node-pg converts to a Postgres array literal ({…}) → invalid JSON text → 500; except [], whose literal {} happens to be valid JSON — so an empty array is accepted and silently stored as an empty object. That last row is data corruption, not an error, and it will outlive any fix that only handles the 500s.
  • JS stringx → passed raw → not valid JSON text ("x" would be) → 500; a numeric string/number passes because 42 is valid JSON.

Expected: the SQL driver's PG path must JSON.stringify every JSON-field binding (insert AND update), exactly as it presumably does for objects via node-pg's default object handling — relying on node-pg's per-type defaults is the bug.

User-visible impact, today, on every control-plane deployment

The Console's "recent items" preference (ui.recent) is naturally a list. Every navigation writes it → every write 500s → a red DATABASE_ERROR in the browser console on every console visit, recents never persist, and the server logs a 5xx per page view. Both prod and staging control planes are PG, so this is not an environment quirk. (Tenant environments on Turso/SQLite take a different driver path, which is presumably why the seed/data suites never caught it.)

Note for the fix

  • The 500 half needs the stringify; the []{} half needs a regression test of its own — it is the case that stays wrong silently after the crash is fixed.
  • Scalar JSON values ("x", true, null) are legal JSON documents and should round-trip; a fix that only handles arrays re-fails the string row of the matrix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:enginepriority:p0Critical: blocker, must ship before MVP

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions