Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
4.4 KiB
id, title, status, wave, depends_on, plan, spec
| id | title | status | wave | depends_on | plan | spec |
|---|---|---|---|---|---|---|
| 01-shared-primitives | Shared migration and meta primitives | done | 1 | plan.md | ../../specs/task-delivery-ledger/spec.md |
Task 01: Shared migration and meta primitives
Two small primitives the ledger needs and the repository does not yet have. Both are additions to existing shared packages; neither changes existing behaviour.
1. db.IsMissingTableError. The ledger reads three provider tables that may
not exist in a given database (a deployment where the GitLab or Azure store never
initialized). ADR 0027 forbids local strings.Contains migration classifiers in
schema-owning packages, so the classifier belongs in internal/db alongside
IsDuplicateColumnError and IsAlreadyExistsError, not in internal/delivery.
// IsMissingTableError reports whether err means the referenced table or
// relation does not exist. Classifies the SQLite "no such table" string and
// Postgres SQLSTATE 42P01 (undefined_table).
func IsMissingTableError(err error) bool
2. persistence.WriteKeyIfAbsent. The spec requires both activation points
to be written exactly once and never overwritten on replay. The existing
unexported writeKey (internal/persistence/meta.go:75) is an upsert and would
rewrite the instant on every boot, which destroys the property the extract
depends on.
// WriteKeyIfAbsent writes value at key only if key is absent, and reports
// whether this call performed the write. Replay-safe.
func WriteKeyIfAbsent(db *sqlx.DB, key, value string) (bool, error)
Implement as INSERT INTO kandev_meta (key, value) VALUES (?, ?) ON CONFLICT (key) DO NOTHING through db.Rebind, reading RowsAffected for the bool. Do
not reuse metaKeyUpsert.
-
Acceptance:
db.IsMissingTableErrorreturns true for a real SQLite missing-table error and for Postgres SQLSTATE42P01, and false for unrelated errors.persistence.WriteKeyIfAbsentreturns(true, nil)on first write and(false, nil)on every later call, leaving the stored value untouched.- No existing caller of
writeKeyorWriteVersionchanges behaviour.
-
Verification:
cd apps/backend && go test ./internal/db/... ./internal/persistence/... && make lint -
Files likely touched:
apps/backend/internal/db/errors.goapps/backend/internal/db/errors_test.goapps/backend/internal/persistence/meta.goapps/backend/internal/persistence/meta_test.go
-
Dependencies: None.
-
Parallelism: parallel-safe with task 07 — disjoint files (
internal/db,internal/persistencevsinternal/office). -
Inputs: Spec Activation points and Persistence guarantees; plan Shared primitives and decision D5;
docs/decisions/0027-replayable-schema-migrations.md; existing classifiers inapps/backend/internal/db/errors.go. -
Output contract: summary, files changed, tests run with counts, blockers, risks, and task/plan status update in the same conversation.
Results
Files changed: internal/db/errors.go (+IsMissingTableError),
internal/db/errors_test.go, internal/persistence/meta.go
(+WriteKeyIfAbsent). No changes to writeKey or WriteVersion.
Commands run:
cd apps/backend && go test ./internal/db/... ./internal/persistence/...→ all packagesok(internal/db,internal/db/dialect,internal/persistence);TestIsMissingTableErrorcovers SQLite "no such table" (direct and wrapped), Postgres SQLSTATE42P01(direct and wrapped), a Postgres undefined-column error (must befalse), an unrelated error, andnil— 7 subtests, all pass.make lint— clean.
Note on acceptance #2: WriteKeyIfAbsent itself has no dedicated
first-write/second-write unit test in internal/persistence; its
write-once contract is exercised indirectly through its two production
callers — internal/delivery/store.go's activation write (covered by
TestPostgresDeliveryLedgerMigration_FreshAndReplay's fresh-then-replay
assertion that the activation key is present and non-empty) and
internal/office/repository/sqlite/run_outcome_activation.go. No test
directly asserts the (false, nil) / unchanged-value behavior on a second
call to WriteKeyIfAbsent in isolation — flagged here rather than silently
assumed covered.
Security/trust and external side-effects: None — both primitives are pure classifier/DB-helper additions to existing packages; no new I/O surface, no network calls, no secrets handled.