From fa0df451d5b3138bfaa0ab1e0747df68c17c9143 Mon Sep 17 00:00:00 2001 From: William Gill Date: Fri, 17 Apr 2026 09:45:37 -0500 Subject: [PATCH] fix: coerce nil Origins to empty slice at pg store boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs exposed by the integration suite: 1. (production) pins.origins is NOT NULL DEFAULT '{}', but pgx serialises a Go nil []string as SQL NULL — so every Create/Replace whose caller omitted origins (an optional field per the IPFS Pinning Service spec) was 500ing on the NOT NULL constraint. The openapi/pin-service paths pass origins through verbatim, so any client POST without "origins" hit this. Normalise nil -> []string{} at the store boundary in both pinStore.Create and pinStore.Replace. 2. (test I introduced last commit) SET LOCAL does not accept bound parameters; the RLS integration test was getting a 42601 syntax error. Switch to SELECT set_config('anchorage.org_id', $1, true), which is parameterisable and keeps the value out of the SQL string. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/pkg/store/postgres/store.go | 12 +++++++++++- test/rls_integration_test.go | 5 ++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/internal/pkg/store/postgres/store.go b/internal/pkg/store/postgres/store.go index ad5e1cd..bac7966 100644 --- a/internal/pkg/store/postgres/store.go +++ b/internal/pkg/store/postgres/store.go @@ -422,9 +422,16 @@ func (p pinStore) Create(ctx context.Context, pn *store.Pin) error { if err != nil { return fmt.Errorf("marshal meta: %w", err) } + // pins.origins is NOT NULL DEFAULT '{}'. pgx serialises a nil + // []string as SQL NULL, which would fail the constraint, so coerce + // nil to an empty slice at the boundary. + origins := pn.Origins + if origins == nil { + origins = []string{} + } _, err = p.s.queries().CreatePin(ctx, sqlc.CreatePinParams{ RequestID: pn.RequestID, OrgID: pn.OrgID, Cid: pn.CID, - Name: pn.Name, Meta: meta, Origins: pn.Origins, + Name: pn.Name, Meta: meta, Origins: origins, }) if isUniqueViolation(err) { return store.ErrConflict @@ -550,6 +557,9 @@ func (p pinStore) Replace(ctx context.Context, orgID ids.OrgID, rid ids.PinID, n if err != nil { return nil, err } + if origins == nil { + origins = []string{} + } row, err := p.s.queries().ReplacePin(ctx, sqlc.ReplacePinParams{ RequestID: rid, OrgID: orgID, Cid: newCID, Name: name, Meta: metaBytes, Origins: origins, }) diff --git a/test/rls_integration_test.go b/test/rls_integration_test.go index 373b87a..4365693 100644 --- a/test/rls_integration_test.go +++ b/test/rls_integration_test.go @@ -80,7 +80,10 @@ func txWithOrg(ctx context.Context, pool *pgxpool.Pool, org string, fn func(cont } defer func() { _ = tx.Rollback(ctx) }() if org != "" { - if _, err := tx.Exec(ctx, "SET LOCAL anchorage.org_id = $1", org); err != nil { + // SET LOCAL does not accept bound parameters; use set_config, + // which does, so the org id is never string-concatenated into + // SQL. + if _, err := tx.Exec(ctx, "SELECT set_config('anchorage.org_id', $1, true)", org); err != nil { return err } }