fix: coerce nil Origins to empty slice at pg store boundary
Test / Build & Unit Tests (push) Successful in 4m58s
Test / Lint (push) Successful in 27s
Test / Integration Tests (push) Failing after 2m19s
Security / Vulnerability Check (push) Successful in 1m12s

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 09:45:37 -05:00
co-authored by Claude Opus 4.7
parent 10326c6ea9
commit fa0df451d5
2 changed files with 15 additions and 2 deletions
+11 -1
View File
@@ -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,
})
+4 -1
View File
@@ -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
}
}