You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
SELECT col1, col2 FROM <strict-doc-collection> WHERE … executed via the pgwire extended-query protocol (the default for most drivers) returns rows where every column value is NULL or the row object is [{}], even when the underlying rows exist and the same query via the simple-query protocol returns the correct data.
This is a silent failure — the client receives rows (not an error), the rows look well-formed (correct count), but every field is null. Dedup logic, update paths, cache lookups, and any read-modify-write pattern that uses a SELECT result on a STRICT doc collection quietly misbehaves.
Symptom seen by client
Two production workaround comments from a downstream app, verbatim:
nodedb-memory-store.ts:507-511
NodeDB 0.0.4 quirk: tagged-template SELECT on TYPE DOCUMENT STRICT
collections returns empty row objects ([{}]). Use simple-query
protocol (sql.unsafe) to get the `result`-wrapped JSON shape that
unwrap() can parse correctly. Without this, entity dedup was
returning null on every lookup → new entity row per mention.
nodedb-memory-store.ts:754-756
NodeDB 0.0.4: tagged-template SELECT on TYPE DOCUMENT STRICT collections
silently returns rows with all columns null. Use sql.unsafe (simple-query)
+ esc() quoting. Same pattern as findEntityByExactName fix.
The "entity dedup was returning null on every lookup → new entity row per mention" line is the business-impact tell: any SELECT … WHERE canonical_name = $1 LIMIT 1 over a STRICT doc collection returned an empty-body row, the app treated it as "not found", and inserted a duplicate. A caller can't tell the difference between "row doesn't exist" and "row exists but all fields decoded to null".
Schema shape that triggers it (from the downstream app's schema setup):
CREATE COLLECTION entities TYPE DOCUMENT STRICT (
id STRING PRIMARY KEY,
tenant_id STRING NOT NULL,
user_id STRING NOT NULL,
canonical_name STRING NOT NULL,
aliases STRING,
embedding VECTOR(1024),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
Query that manifests (extended protocol — postgres.js tagged template):
Same query via simple-query protocol (sql.unsafe(...)) returns the actual row contents wrapped in the { result: "..." } envelope — correct data.
Scope
Only TYPE DOCUMENT STRICT collections are reported affected; regular CREATE COLLECTION (schemaless) works via either protocol, per the downstream app's split — it uses tagged template for schemaless tables without issue.
Affects any pgwire driver that defaults to extended protocol (postgres.js, psycopg, JDBC, pgx), unless the client explicitly opts out of PREPARE (prepare: false in postgres.js).
which routes to execute_planned_sql_inner in routing/mod.rs:87. The simple-query path instead goes through sql_exec.rs:286-307 and ends up in execute_planned_sqlplus the DDL dispatcher at line 286. The row-encoding layer (envelope-wrapped JSON result vs. typed columns) differs between the two paths, and somewhere in the typed-column encoder for STRICT-doc scans the fields are not populated — or the result schema inferred at Parse time (prepared/parser.rs:63-76) doesn't match the actual columns decoded at Execute time.
Why this matters
Silent. Silent. Silent. No error, no warning — the driver reports N rows and the application reads N null-valued records.
Corrupts application state. The downstream app was creating a new entity row for every mention because findEntityByExactName returned null. Over days, entity count blows up with duplicates and the LLM's memory graph degrades.
CREATE COLLECTION t TYPE DOCUMENT STRICT (
id STRING PRIMARY KEY,
name STRING
);
INSERT INTO t (id, name) VALUES ('a', 'alice');
// postgres.js with DEFAULT (extended protocol) — buggyconstrows=awaitsql\`SELECTid,nameFROMtWHEREname= \${'alice'}LIMIT1\`;console.log(rows);// expected: [{ id: 'a', name: 'alice' }], actual: [{}] or [{ id: null, name: null }]// Same query via simple-query — correctconstrows2=awaitsql.unsafe(\"SELECTid,nameFROMtWHEREname='alice'LIMIT1\");console.log(rows2);// [{ id: 'a', name: 'alice' }] (possibly wrapped in { result: \"...\" })
Notes
Verified via two independent workaround comments in production code at the same downstream client (see lines 507-511 and 754-756 of nodedb-memory-store.ts in the app).
SELECT col1, col2 FROM <strict-doc-collection> WHERE …executed via the pgwire extended-query protocol (the default for most drivers) returns rows where every column value is NULL or the row object is[{}], even when the underlying rows exist and the same query via the simple-query protocol returns the correct data.This is a silent failure — the client receives rows (not an error), the rows look well-formed (correct count), but every field is null. Dedup logic, update paths, cache lookups, and any read-modify-write pattern that uses a SELECT result on a STRICT doc collection quietly misbehaves.
Symptom seen by client
Two production workaround comments from a downstream app, verbatim:
The "entity dedup was returning null on every lookup → new entity row per mention" line is the business-impact tell: any
SELECT … WHERE canonical_name = $1 LIMIT 1over a STRICT doc collection returned an empty-body row, the app treated it as "not found", and inserted a duplicate. A caller can't tell the difference between "row doesn't exist" and "row exists but all fields decoded to null".Schema shape that triggers it (from the downstream app's schema setup):
Query that manifests (extended protocol — postgres.js tagged template):
Same query via simple-query protocol (
sql.unsafe(...)) returns the actual row contents wrapped in the{ result: "..." }envelope — correct data.Scope
CREATE COLLECTION(schemaless) works via either protocol, per the downstream app's split — it uses tagged template for schemaless tables without issue.PREPARE(prepare: falsein postgres.js).Likely root cause (for triage)
The extended path goes through
prepared/execute.rs:46-49:which routes to
execute_planned_sql_innerinrouting/mod.rs:87. The simple-query path instead goes throughsql_exec.rs:286-307and ends up inexecute_planned_sqlplus the DDL dispatcher at line 286. The row-encoding layer (envelope-wrapped JSONresultvs. typed columns) differs between the two paths, and somewhere in the typed-column encoder for STRICT-doc scans the fields are not populated — or the result schema inferred at Parse time (prepared/parser.rs:63-76) doesn't match the actual columns decoded at Execute time.Why this matters
findEntityByExactNamereturned null. Over days, entity count blows up with duplicates and the LLM's memory graph degrades.prepare: falseworkaround globally (see also pgwire extended-query protocol rejects DSL statements (SEARCH / GRAPH / UPSERT / MATCH) — forces simple-query + string interpolation #43), which is also why this bug is not trivially side-stepped.Repro
Notes