mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-17 00:14:13 +00:00
- bump version to 0.122.2 - document schema migration invariants and push notification configuration - add serverless host function aliases and v2 database API documentation - introduce schema roundtrip test to prevent migration drift
238 lines
8.2 KiB
Go
238 lines
8.2 KiB
Go
package migrations_test
|
|
|
|
// roundtrip_test.go is the build-time guard that prevents
|
|
// "binary references column X but X is missing from migrations"
|
|
// drift — the bug that triggered the AnChat-test outage on 2026-05-06.
|
|
//
|
|
// How it works:
|
|
//
|
|
// 1. Open an in-memory SQLite database.
|
|
// 2. Apply EVERY embedded migration in version order.
|
|
// 3. Run a series of "exemplar" SQL operations against the resulting
|
|
// schema. If any operation fails, the test fails — meaning either:
|
|
// a. A migration was deleted / renumbered and the schema regressed
|
|
// b. A new migration was added but isn't reachable via embed.FS
|
|
// c. (Most importantly) a Go file references a column / table /
|
|
// index that no migration creates
|
|
//
|
|
// The exemplars are drawn from the actual SQL strings the platform's
|
|
// Go code executes. Adding a new INSERT/SELECT in the gateway → add the
|
|
// matching exemplar here so drift is caught at `go test` time, not
|
|
// at production deploy.
|
|
//
|
|
// This is generic by design — every platform table participates. Adding
|
|
// a new table doesn't require new test infrastructure, only one new
|
|
// exemplar string.
|
|
|
|
import (
|
|
"database/sql"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DeBrosOfficial/network/migrations"
|
|
"github.com/DeBrosOfficial/network/pkg/rqlite"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// TestSchemaRoundtrip_AllMigrationsApplyClean verifies every embedded
|
|
// migration applies successfully against a fresh database in version
|
|
// order. Failure here means a migration is broken in isolation
|
|
// (syntax error, references a missing prior migration's column, etc.).
|
|
func TestSchemaRoundtrip_AllMigrationsApplyClean(t *testing.T) {
|
|
db := openRoundtripDB(t)
|
|
if err := rqlite.ApplyEmbeddedMigrations(t.Context(), db, migrations.FS, zap.NewNop()); err != nil {
|
|
t.Fatalf("ApplyEmbeddedMigrations failed: %v", err)
|
|
}
|
|
|
|
// Sanity: applied version should equal RequiredVersion.
|
|
applied, err := migrations.AppliedVersion(t.Context(), db)
|
|
if err != nil {
|
|
t.Fatalf("AppliedVersion: %v", err)
|
|
}
|
|
if applied != migrations.RequiredVersion() {
|
|
t.Errorf("applied=%d != required=%d after full roundtrip", applied, migrations.RequiredVersion())
|
|
}
|
|
}
|
|
|
|
// TestSchemaRoundtrip_PlatformExemplars exercises representative SQL
|
|
// statements from the Go codebase against the migrated schema.
|
|
//
|
|
// Each exemplar is a string that should EXECUTE successfully (we don't
|
|
// care about row counts — only that the SQL parses and binds against
|
|
// the schema). Args are placeholders; values can be anything matching
|
|
// the column types.
|
|
//
|
|
// When a Go handler is added that touches a new table or column, add
|
|
// an exemplar here. The diff at review time enforces the contract:
|
|
// "if you write Go that uses column X, an exemplar exercises it,
|
|
// which means migrations must declare X."
|
|
func TestSchemaRoundtrip_PlatformExemplars(t *testing.T) {
|
|
db := openRoundtripDB(t)
|
|
if err := rqlite.ApplyEmbeddedMigrations(t.Context(), db, migrations.FS, zap.NewNop()); err != nil {
|
|
t.Fatalf("ApplyEmbeddedMigrations: %v", err)
|
|
}
|
|
|
|
// Each exemplar is (table, sql, args). The args don't have to satisfy
|
|
// constraints — we use Prepare to validate column references without
|
|
// actually running mutations. Statements that have to execute (because
|
|
// SQLite delays some checks) get marked exec=true.
|
|
type exemplar struct {
|
|
name string
|
|
sql string
|
|
args []any
|
|
exec bool // true: actually execute; false: just Prepare
|
|
}
|
|
exemplars := []exemplar{
|
|
// functions table — bug #214's table, which is why we care.
|
|
// Every column written by the function-store INSERT must be here.
|
|
{
|
|
name: "functions INSERT (full column list incl. ws_*)",
|
|
sql: `INSERT INTO functions (
|
|
id, name, namespace, version, wasm_cid,
|
|
memory_limit_mb, timeout_seconds, is_public,
|
|
retry_count, retry_delay_seconds, dlq_topic,
|
|
status, created_at, updated_at, created_by,
|
|
ws_persistent, ws_idle_timeout_sec, ws_max_frame_bytes, ws_max_inflight_per_conn
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
args: []any{
|
|
"id-1", "fn", "ns", 1, "cid-1",
|
|
64, 30, false,
|
|
0, 5, "",
|
|
"active", 0, 0, "ns",
|
|
false, 0, 0, 0,
|
|
},
|
|
exec: true,
|
|
},
|
|
{
|
|
name: "functions SELECT (full column list)",
|
|
sql: `SELECT id, name, namespace, version, wasm_cid, source_cid,
|
|
ws_persistent, ws_idle_timeout_sec, ws_max_frame_bytes, ws_max_inflight_per_conn,
|
|
memory_limit_mb, timeout_seconds, is_public,
|
|
retry_count, retry_delay_seconds, dlq_topic,
|
|
status, created_at, updated_at, created_by
|
|
FROM functions WHERE namespace = ? AND name = ?`,
|
|
args: []any{"ns", "fn"},
|
|
},
|
|
|
|
// function_invocations — used by the invocation-history view (#211 fix).
|
|
{
|
|
name: "function_invocations INSERT",
|
|
sql: `INSERT INTO function_invocations (
|
|
id, function_id, request_id, trigger_type, caller_wallet,
|
|
input_size, output_size, started_at, completed_at,
|
|
duration_ms, status, error_message, memory_used_mb
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
args: []any{
|
|
"inv-1", "id-1", "req-A", "http", "0xwallet",
|
|
0, 0, 0, 0,
|
|
0, "success", "", 0.0,
|
|
},
|
|
exec: true,
|
|
},
|
|
{
|
|
name: "function_invocations SELECT for GetInvocations",
|
|
sql: `SELECT i.id, i.request_id, i.trigger_type, i.caller_wallet,
|
|
i.input_size, i.output_size, i.started_at, i.completed_at,
|
|
i.duration_ms, i.status, i.error_message, i.memory_used_mb
|
|
FROM function_invocations i
|
|
JOIN functions f ON i.function_id = f.id
|
|
WHERE f.namespace = ? AND f.name = ?
|
|
ORDER BY i.started_at DESC LIMIT ?`,
|
|
args: []any{"ns", "fn", 50},
|
|
},
|
|
|
|
// function_logs — WASM-emitted log lines.
|
|
{
|
|
name: "function_logs INSERT",
|
|
sql: `INSERT INTO function_logs (
|
|
id, function_id, invocation_id, level, message, timestamp
|
|
) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
args: []any{"log-1", "id-1", "inv-1", "info", "hi", 0},
|
|
exec: true,
|
|
},
|
|
|
|
// function_pubsub_triggers — wildcard trigger column rename (plan 03).
|
|
// During the dual-column rolling-upgrade window the Go code writes
|
|
// BOTH `topic` (legacy NOT NULL) and `topic_pattern` (new); this
|
|
// exemplar mirrors the actual INSERT and would catch a future
|
|
// migration that drops one column without a corresponding code change.
|
|
{
|
|
name: "function_pubsub_triggers INSERT (dual topic+topic_pattern)",
|
|
sql: `INSERT INTO function_pubsub_triggers (
|
|
id, function_id, topic, topic_pattern,
|
|
enabled, created_at,
|
|
aggregation_window_ms, aggregation_max_batch_size
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
args: []any{"trig-1", "id-1", "presence:*", "presence:*", true, 0, 0, 0},
|
|
exec: true,
|
|
},
|
|
|
|
// push_devices — created by migration 023; encrypted token storage.
|
|
{
|
|
name: "push_devices INSERT",
|
|
sql: `INSERT INTO push_devices (
|
|
id, namespace, user_id, device_id, provider,
|
|
token_encrypted, platform, app_version,
|
|
created_at, updated_at, last_seen
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
args: []any{
|
|
"dev-1", "ns", "u1", "device-A", "ntfy",
|
|
"enc:...", "ios", "1.0",
|
|
0, 0, 0,
|
|
},
|
|
exec: true,
|
|
},
|
|
|
|
// namespace_publish_seq — sequence counter from plan 08.
|
|
{
|
|
name: "namespace_publish_seq UPSERT",
|
|
sql: `INSERT INTO namespace_publish_seq (namespace, next_seq, updated_at)
|
|
VALUES (?, ?, ?)
|
|
ON CONFLICT(namespace) DO UPDATE SET
|
|
next_seq = next_seq + 1,
|
|
updated_at = excluded.updated_at`,
|
|
args: []any{"ns", 2, 0},
|
|
exec: true,
|
|
},
|
|
}
|
|
|
|
for _, ex := range exemplars {
|
|
t.Run(ex.name, func(t *testing.T) {
|
|
if ex.exec {
|
|
if _, err := db.Exec(ex.sql, ex.args...); err != nil {
|
|
t.Errorf("schema drift: %v\nsql: %s", err, snippet(ex.sql))
|
|
}
|
|
return
|
|
}
|
|
stmt, err := db.Prepare(ex.sql)
|
|
if err != nil {
|
|
t.Errorf("schema drift (Prepare failed): %v\nsql: %s", err, snippet(ex.sql))
|
|
return
|
|
}
|
|
defer func() { _ = stmt.Close() }()
|
|
})
|
|
}
|
|
}
|
|
|
|
// openRoundtripDB returns an in-memory SQLite. Closes automatically on
|
|
// test cleanup.
|
|
func openRoundtripDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite3", ":memory:")
|
|
if err != nil {
|
|
t.Fatalf("open in-memory sqlite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
return db
|
|
}
|
|
|
|
// snippet trims a SQL string to fit on a single error line.
|
|
func snippet(s string) string {
|
|
s = strings.Join(strings.Fields(s), " ")
|
|
if len(s) > 140 {
|
|
return s[:140] + "..."
|
|
}
|
|
return s
|
|
}
|