89 lines
3.1 KiB
Go
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);
|
|
`
|