86 lines
3.4 KiB
Go
86 lines
3.4 KiB
Go
package store
|
|
|
|
// bankbasis.go: the settled basis (v19) — what this book has already decided, and the fingerprint of the
|
|
// evidence each decision was taken on. Dumb storage like stopmemory.go and ruby.go: the fingerprint, the
|
|
// predicate and every rule about WHEN a row may be served live in the pipeline; this layer writes rows and
|
|
// reads them back.
|
|
//
|
|
// ⚠ IT REPLACES, WHERE ITS NEIGHBOUR ONLY ACCUMULATES. bank_stop_presented has no delete on purpose —
|
|
// trimming it re-arms a stop the owner has already been shown. This table is the opposite: a term decided
|
|
// again must overwrite its row, or the predicate keeps comparing against evidence the book has moved past
|
|
// and serves an answer that was right two purchases ago. Same shape, deliberately different guarantee.
|
|
|
|
// BankBasisRow is one decided surface as the store holds it.
|
|
type BankBasisRow struct {
|
|
SrcKey string
|
|
FPVersion string
|
|
FPWhole string
|
|
FPCanon string
|
|
Dst string
|
|
Type string
|
|
Gender string
|
|
}
|
|
|
|
// BankBasisForBook returns the book's settled basis keyed by the normalized source key. An empty table is
|
|
// an empty map: nothing has been decided yet, which is the state of every book before its first purchase.
|
|
func (s *Store) BankBasisForBook(bookID string) (map[string]BankBasisRow, error) {
|
|
ctx, cancel := opContext()
|
|
defer cancel()
|
|
rows, err := s.r.QueryContext(ctx, `
|
|
SELECT src_key, fp_version, fp_whole, fp_canon, dst, type, gender
|
|
FROM bank_basis WHERE book_id = ?`, bookID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
out := map[string]BankBasisRow{}
|
|
for rows.Next() {
|
|
var r BankBasisRow
|
|
if err := rows.Scan(&r.SrcKey, &r.FPVersion, &r.FPWhole, &r.FPCanon, &r.Dst, &r.Type, &r.Gender); err != nil {
|
|
return nil, err
|
|
}
|
|
out[r.SrcKey] = r
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// PutBankBasis writes the run's decisions, in ONE transaction, replacing whatever the book held for the
|
|
// same surfaces.
|
|
//
|
|
// ⛔ A ROW WITH NO RENDERING IS REFUSED RATHER THAN STORED. The basis exists to hand an answer back, and an
|
|
// empty one handed back is the silent regression the whole mechanism is built against: the emission would
|
|
// fall to the raw first-chunk guess at status `auto` and the run would look cheaper while the bank got
|
|
// poorer. Refusing here means the predicate cannot serve emptiness even if a caller upstream stops
|
|
// checking — the guarantee lives at the write, where a caller cannot forget it.
|
|
func (s *Store) PutBankBasis(bookID string, rows []BankBasisRow) error {
|
|
if len(rows) == 0 {
|
|
return nil
|
|
}
|
|
ctx, cancel := opContext()
|
|
defer cancel()
|
|
tx, err := s.w.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
for _, r := range rows {
|
|
if r.SrcKey == "" || r.Dst == "" || r.FPWhole == "" || r.FPVersion == "" {
|
|
continue
|
|
}
|
|
if _, err := tx.ExecContext(ctx, `
|
|
INSERT INTO bank_basis (book_id, src_key, fp_version, fp_whole, fp_canon, dst, type, gender, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
|
ON CONFLICT(book_id, src_key) DO UPDATE SET
|
|
fp_version = excluded.fp_version,
|
|
fp_whole = excluded.fp_whole,
|
|
fp_canon = excluded.fp_canon,
|
|
dst = excluded.dst,
|
|
type = excluded.type,
|
|
gender = excluded.gender,
|
|
updated_at = excluded.updated_at`,
|
|
bookID, r.SrcKey, r.FPVersion, r.FPWhole, r.FPCanon, r.Dst, r.Type, r.Gender); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|