f2e83e7b9a
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>
103 lines
4.4 KiB
Markdown
103 lines
4.4 KiB
Markdown
---
|
|
id: "01-shared-primitives"
|
|
title: "Shared migration and meta primitives"
|
|
status: done
|
|
wave: 1
|
|
depends_on: []
|
|
plan: "plan.md"
|
|
spec: "../../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`.
|
|
|
|
```go
|
|
// 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.
|
|
|
|
```go
|
|
// 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:**
|
|
1. `db.IsMissingTableError` returns true for a real SQLite missing-table error
|
|
and for Postgres SQLSTATE `42P01`, and false for unrelated errors.
|
|
2. `persistence.WriteKeyIfAbsent` returns `(true, nil)` on first write and
|
|
`(false, nil)` on every later call, leaving the stored value untouched.
|
|
3. No existing caller of `writeKey` or `WriteVersion` changes behaviour.
|
|
|
|
- **Verification:**
|
|
`cd apps/backend && go test ./internal/db/... ./internal/persistence/... && make lint`
|
|
|
|
- **Files likely touched:**
|
|
- `apps/backend/internal/db/errors.go`
|
|
- `apps/backend/internal/db/errors_test.go`
|
|
- `apps/backend/internal/persistence/meta.go`
|
|
- `apps/backend/internal/persistence/meta_test.go`
|
|
|
|
- **Dependencies:** None.
|
|
|
|
- **Parallelism:** parallel-safe with task 07 — disjoint files
|
|
(`internal/db`, `internal/persistence` vs `internal/office`).
|
|
|
|
- **Inputs:** Spec **Activation points** and **Persistence guarantees**; plan
|
|
**Shared primitives** and decision D5;
|
|
`docs/decisions/0027-replayable-schema-migrations.md`; existing classifiers in
|
|
`apps/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 packages `ok` (`internal/db`, `internal/db/dialect`,
|
|
`internal/persistence`); `TestIsMissingTableError` covers SQLite
|
|
"no such table" (direct and wrapped), Postgres SQLSTATE `42P01` (direct and
|
|
wrapped), a Postgres undefined-column error (must be `false`), an unrelated
|
|
error, and `nil` — 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.
|