Skip to content

fix(db): serialize non-binary JS values for Postgres — mirror SQLite toBindable - #592

Open
diogenxs wants to merge 1 commit into
CoreBunch:mainfrom
lazybirds-cloud:lazybirds/pg
Open

diogenxs wants to merge 1 commit into
CoreBunch:mainfrom
lazybirds-cloud:lazybirds/pg

Conversation

@diogenxs

@diogenxs diogenxs commented Oct 1, 2026 •

Copy link
Copy Markdown

Fixes #593.

Problem

On Postgres, Bun SQL binds plain objects to text parameters via String(), storing literal '[object Object]' in every *_json column. The SQLite adapter's toBindable JSON.stringify's the same values, so the two dialects silently diverge: a PG deployment writes garbage to ai_messages.content_json, conversation settings, plugin records — and replaying that content into a provider request fails with a confusing downstream 500.

Root cause

docs/reference/database-dialects.md Rule 2 documents both adapters as auto-serializing JSON-shaped writes. The SQLite adapter does it in toBindable; the Postgres side relied on Bun.sql parameter binding — which binds a plain object to a text column via String().

Related upstream: oven-sh/bun#29010 (prepare:false jsonb variant) — but this fix is needed independently: for a text parameter Bun can't infer a JSON contract, so the adapter must own the serialization, exactly like the SQLite path already does.

Fix

Mirror SQLite's toBindable in the PG wrapper (tagged templates + unsafe() params):

  • plain object/array → JSON.stringify (the actual bug)
  • Date → ISO 8601 string (Bun PG text-format binding is locale toString())
  • Uint8Array/Buffer → pass through untouched — Bun binds binary natively to bytea (verified round-trip); stringifying would corrupt AES-GCM credential ciphertext/iv and break every decrypt
  • null/undefined/primitives: unchanged

Verification

End-to-end on Postgres 16 with a live provider credential:

  • before: content_json = '[object Object]'; replaying the conversation 500s the provider call
  • after: content_json = [{"kind":"text","text":"…"}]; multi-turn agent chats (toolCall/toolResult blocks) persist and replay correctly; credential decrypt works (binary passthrough); setup/login/roles/users/onboarding unaffected
  • full suite green locally; the only failures are canvas/DOM tests that also fail on pristine main in my environment, unrelated to this change

Found while running Instatic on shared Postgres at LazyBirds (one database per site, provisioned automatically). Happy to add a PG regression test — pointing me at the preferred harness for DB-backed tests would help.

@diogenxs
diogenxs force-pushed the lazybirds/pg branch 2 times, most recently from 82013cc to 2719644 Compare October 1, 2026 18:44
…indable

Bun.sql binds plain objects to text parameters via String() → '[object
Object]', so every *_json column write stored garbage on Postgres while
the SQLite adapter JSON.stringify'd the same values.
@diogenxs

diogenxs commented Oct 1, 2026

Copy link
Copy Markdown
Author

Filed the underlying report as #593. Rebased onto current main (the adapter gained ownsPool/close() — conflict resolved, serializer untouched) and the full test suite runs green locally; the only failures I see are the canvas/DOM tests that also fail on pristine main in my environment, nothing related to this change.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Postgres — *_json columns store '[object Object]'; AI chat history corrupts and provider calls fail

1 participant