Files
rvbox/internal/client/spool/migrations.go
T

89 lines
3.1 KiB
Go

package spool
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
)
type migration struct {
version uint32
sql string
}
var migrations = []migration{{version: 1, sql: schemaV1}}
func applyMigrations(ctx context.Context, db *sql.DB) error {
if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
version INTEGER PRIMARY KEY CHECK(version > 0), checksum TEXT NOT NULL, applied_at INTEGER NOT NULL
) STRICT`); err != nil {
return fmt.Errorf("create client spool migration table: %w", err)
}
for _, current := range migrations {
checksumBytes := sha256.Sum256([]byte(current.sql))
checksum := hex.EncodeToString(checksumBytes[:])
var stored string
err := db.QueryRowContext(ctx, `SELECT checksum FROM schema_migrations WHERE version = ?`, current.version).Scan(&stored)
if err == nil {
if stored != checksum {
return fmt.Errorf("client spool migration %d checksum mismatch", current.version)
}
continue
}
if err != sql.ErrNoRows {
return err
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
if _, err = tx.ExecContext(ctx, current.sql); err == nil {
_, err = tx.ExecContext(ctx, `INSERT INTO schema_migrations(version, checksum, applied_at) VALUES (?, ?, unixepoch())`, current.version, checksum)
}
if err != nil {
_ = tx.Rollback()
return fmt.Errorf("apply client spool migration %d: %w", current.version, err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit client spool migration %d: %w", current.version, err)
}
}
return nil
}
const schemaV1 = `
CREATE TABLE commands (
issue_uuid BLOB PRIMARY KEY CHECK(length(issue_uuid) = 16),
immutable_sha256 BLOB NOT NULL CHECK(length(immutable_sha256) = 32),
command_revision INTEGER NOT NULL CHECK(command_revision > 0),
phase INTEGER NOT NULL CHECK(phase BETWEEN 1 AND 11),
terminal INTEGER NOT NULL DEFAULT 0 CHECK(terminal IN (0, 1)),
next_local_ordinal INTEGER NOT NULL DEFAULT 1 CHECK(next_local_ordinal > 0),
next_event_seq INTEGER NOT NULL DEFAULT 1 CHECK(next_event_seq > 0),
last_server_ack INTEGER NOT NULL DEFAULT 0 CHECK(last_server_ack >= 0),
accepted_at INTEGER NOT NULL
) STRICT;
CREATE TABLE events (
issue_uuid BLOB NOT NULL REFERENCES commands(issue_uuid) ON DELETE CASCADE,
local_ordinal INTEGER NOT NULL CHECK(local_ordinal > 0),
event_seq INTEGER CHECK(event_seq IS NULL OR event_seq > 0),
event_kind INTEGER NOT NULL CHECK(event_kind > 0),
compression INTEGER NOT NULL CHECK(compression IN (1, 2)),
raw_bytes INTEGER NOT NULL CHECK(raw_bytes >= 0),
payload BLOB NOT NULL,
payload_sha256 BLOB NOT NULL CHECK(length(payload_sha256) = 32),
created_at INTEGER NOT NULL,
PRIMARY KEY(issue_uuid, local_ordinal),
UNIQUE(issue_uuid, event_seq)
) STRICT, WITHOUT ROWID;
CREATE INDEX events_send_window ON events(issue_uuid, event_seq, local_ordinal);
CREATE TABLE command_tombstones (
issue_uuid BLOB PRIMARY KEY CHECK(length(issue_uuid) = 16),
immutable_sha256 BLOB NOT NULL CHECK(length(immutable_sha256) = 32),
acknowledged_at INTEGER NOT NULL
) STRICT;
CREATE INDEX tombstones_fifo ON command_tombstones(acknowledged_at, issue_uuid);
`