fix(memory-plugin): URGENT — emit JSON null for nil metadata/propagation (closes #1794 prod regression) #1798
@@ -349,8 +349,23 @@ func scanMemory(row interface {
|
||||
}
|
||||
|
||||
func marshalMetadata(m map[string]interface{}) ([]byte, error) {
|
||||
// Returning Go nil here previously made lib/pq send the parameter
|
||||
// as a bytea-typed NULL placeholder that PostgreSQL refused to
|
||||
// implicitly cast into the jsonb column. The resulting INSERT
|
||||
// failed with `pq: invalid input syntax for type json`, which
|
||||
// surfaced first as "scan namespace" errors from the QueryRow
|
||||
// path (the row never gets created so RETURNING produces no rows;
|
||||
// the QueryRow error propagates through Scan). Caught 2026-05-24
|
||||
// during the Phase A2 backfill — UpsertNamespace failed on every
|
||||
// call, then the client's circuit breaker opened and ALL plugin
|
||||
// writes started failing. The same hazard exists on the
|
||||
// memory_records.propagation column (also marshalled via this
|
||||
// helper). Sending the JSON literal `null` keeps the parameter
|
||||
// typed as a valid jsonb value regardless of pq's auto-conversion
|
||||
// heuristics — semantically identical to NULL for our consumers
|
||||
// (they treat empty propagation/metadata as absent).
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
return []byte(`null`), nil
|
||||
}
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
|
||||
@@ -17,12 +17,19 @@ import (
|
||||
// --- marshalMetadata corner cases ---
|
||||
|
||||
func TestMarshalMetadata_Nil(t *testing.T) {
|
||||
// Post-fix (2026-05-24): nil input must return the JSON literal
|
||||
// `null` (4 bytes), NOT Go nil. Returning Go nil made lib/pq send
|
||||
// a bytea NULL parameter that PostgreSQL refused to implicitly
|
||||
// cast into the target jsonb column, breaking every UpsertNamespace
|
||||
// and CommitMemory call with `pq: invalid input syntax for type
|
||||
// json`. The Phase A2 backfill exposed it (production saw every
|
||||
// POST /memories return 500 immediately post-tenant-recycle).
|
||||
got, err := marshalMetadata(nil)
|
||||
if err != nil {
|
||||
t.Errorf("err = %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Errorf("got = %v, want nil", got)
|
||||
if string(got) != "null" {
|
||||
t.Errorf("got = %q (%v), want %q", string(got), got, "null")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user