v2.0.0: claims model - signed MMUR2 votes in a truth DB, merged view in a rebuildable cache DB; POST /api/claims, claims and merge routes, unsigned writes answer 410
Build and Push Docker Image / build (push) Successful in 5m1s
Build and Push Docker Image / build (push) Successful in 5m1s
This commit is contained in:
@@ -1,70 +1,76 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/metazla/meta-core/internal/kv"
|
||||
)
|
||||
|
||||
// SchemaVersion is the key-layout version this build of meta-core expects.
|
||||
// Distinct from the snapshot manifest version (which is on-disk format);
|
||||
// this number describes the record layout — primarily, "are roots UUIDs or
|
||||
// content-hash tokens, and do reverse-index keys cid:* exist?"
|
||||
// SchemaVersion is the truth-database layout this build of meta-core expects.
|
||||
//
|
||||
// The Redis → SQLite move did not change the record layout (file:<rid>/<field>,
|
||||
// cid:<cid>), so it did not bump this: the one-time migration copied records
|
||||
// verbatim.
|
||||
// 2 — v1.x: one kv table holding records, the CID index and UDL cells
|
||||
// 3 — v2.0.0: the claims model (truth: principal, claim, cid_group, meta)
|
||||
//
|
||||
// Bump only when a layout change can't be detected from the data itself or
|
||||
// healed by the write path. Each bump is a flag day in alpha: operators wipe
|
||||
// and restart.
|
||||
const SchemaVersion = 2
|
||||
// Bump only when a layout change can't be detected from the data itself. Each
|
||||
// bump is a flag day: there is no migration code in meta-core (the one-off
|
||||
// v2 → v3 conversion of watch.nsl.sh is a manual tool, tools/claims-migrate).
|
||||
const SchemaVersion = 3
|
||||
|
||||
// SchemaVersionKey is the sentinel under which we record SchemaVersion.
|
||||
// Checked on every meta-core boot.
|
||||
const SchemaVersionKey = "meta-core:schema-version"
|
||||
// SchemaVersionKey is the truth `meta` row recording SchemaVersion.
|
||||
const SchemaVersionKey = "schema-version"
|
||||
|
||||
// EnsureSchemaVersion verifies the store matches the expected schema version.
|
||||
// Called once at startup, before any other reads/writes.
|
||||
// EnsureSchemaVersion verifies the truth database matches the expected
|
||||
// version. Called once at startup, before Start.
|
||||
//
|
||||
// Behavior:
|
||||
// - sentinel present and matches expected → OK
|
||||
// - sentinel missing AND no roots → fresh store, stamp sentinel
|
||||
// - sentinel missing AND roots exist → legacy data, refuse
|
||||
// - sentinel present but mismatched → refuse, point operator at clean:all
|
||||
//
|
||||
// Refusal is hard-fail; the caller is expected to log.Fatalf and exit.
|
||||
// The error message names the wipe remediation because in alpha there
|
||||
// is no automated migration — operators bring up a clean store.
|
||||
// - a v1.x store (the old kv table, which the default path still points at)
|
||||
// → refuse
|
||||
// - sentinel present and matching → OK
|
||||
// - sentinel missing and no claims → fresh store, stamp it
|
||||
// - sentinel missing with claims, or mismatched → refuse
|
||||
func (c *Client) EnsureSchemaVersion(expected int) error {
|
||||
return c.update(func(tx *kv.Tx) error {
|
||||
ctx := tx.Context()
|
||||
have, ok, err := tx.Get(ctx, SchemaVersionKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get schema version: %w", err)
|
||||
c.mu.RLock()
|
||||
truth := c.truth
|
||||
c.mu.RUnlock()
|
||||
if truth == nil {
|
||||
return fmt.Errorf("not connected")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), opTimeout)
|
||||
defer cancel()
|
||||
return truth.Update(ctx, func(tx *kv.Tx) error {
|
||||
var name string
|
||||
err := tx.QueryRow(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'kv'`).Scan(&name)
|
||||
if err == nil {
|
||||
return fmt.Errorf(
|
||||
"%s holds a meta-core v1.x store (schema version 2, the kv table). v2.0.0 "+
|
||||
"does not migrate it: convert it with tools/claims-migrate, or move it "+
|
||||
"away to start empty", truth.Path())
|
||||
}
|
||||
if !ok {
|
||||
n, err := tx.Count(ctx, rootsNS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("count roots: %w", err)
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
var have string
|
||||
err = tx.QueryRow(`SELECT value FROM meta WHERE name = ?`, SchemaVersionKey).Scan(&have)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
var n int64
|
||||
if err := tx.QueryRow(`SELECT count(*) FROM claim`).Scan(&n); err != nil {
|
||||
return err
|
||||
}
|
||||
if n > 0 {
|
||||
return fmt.Errorf(
|
||||
"the store has %d indexed roots but no %q sentinel — this is alpha "+
|
||||
"data from before the UUID-rooted schema landed. Delete the "+
|
||||
"database file to wipe and restart, or downgrade meta-core to a compatible build",
|
||||
n, SchemaVersionKey)
|
||||
return fmt.Errorf("truth database has %d claims but no %q sentinel; refusing to guess its layout", n, SchemaVersionKey)
|
||||
}
|
||||
return tx.Put(SchemaVersionKey, strconv.Itoa(expected))
|
||||
_, err := tx.Exec(`INSERT INTO meta(name, value) VALUES(?, ?)`, SchemaVersionKey, strconv.Itoa(expected))
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("get schema version: %w", err)
|
||||
}
|
||||
if have == strconv.Itoa(expected) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(
|
||||
"store schema version is %q, this build of meta-core expects %d. "+
|
||||
"Delete the database file to wipe and restart, or use a meta-core "+
|
||||
"build that matches the existing data",
|
||||
have, expected)
|
||||
return fmt.Errorf("truth schema version is %q, this build of meta-core expects %d", have, expected)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user