Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmd/agentsview/pg.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,20 @@ func runPGServe(args []string) {
)
defer stop()

// Attempt to apply any missing schema migrations before
// the compatibility check. This handles upgrades (e.g.
// new tables like tool_result_events) without requiring a
// manual schema drop. If the PG role is read-only the
// migration is skipped and the compat check reports what
// is missing.
if err := postgres.EnsureSchema(
ctx, store.DB(), pgCfg.Schema,
); err != nil {
if !postgres.IsReadOnlyError(err) {
fatal("pg serve: schema migration failed: %v", err)
}
}

if err := postgres.CheckSchemaCompat(
ctx, store.DB(),
); err != nil {
Expand Down
94 changes: 94 additions & 0 deletions internal/postgres/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,100 @@ func TestEnsureSchemaIdempotent(t *testing.T) {
}
}

func TestEnsureSchemaMigratesLegacySchema(t *testing.T) {
pgURL := testPGURL(t)
cleanPGSchema(t, pgURL)
t.Cleanup(func() { cleanPGSchema(t, pgURL) })

pg, err := Open(pgURL, "agentsview", true)
if err != nil {
t.Fatalf("connecting to pg: %v", err)
}
defer pg.Close()

ctx := context.Background()

// Simulate a 0.16.x schema: create the schema and core
// tables but omit tool_result_events.
if _, err := pg.ExecContext(ctx,
"CREATE SCHEMA IF NOT EXISTS agentsview",
); err != nil {
t.Fatalf("creating schema: %v", err)
}
legacyDDL := `
CREATE TABLE IF NOT EXISTS sync_metadata (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
machine TEXT NOT NULL,
project TEXT NOT NULL,
agent TEXT NOT NULL,
first_message TEXT,
display_name TEXT,
created_at TIMESTAMPTZ,
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ,
message_count INT NOT NULL DEFAULT 0,
user_message_count INT NOT NULL DEFAULT 0,
parent_session_id TEXT,
relationship_type TEXT NOT NULL DEFAULT '',
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS messages (
session_id TEXT NOT NULL,
ordinal INT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
timestamp TIMESTAMPTZ,
has_thinking BOOLEAN NOT NULL DEFAULT FALSE,
has_tool_use BOOLEAN NOT NULL DEFAULT FALSE,
content_length INT NOT NULL DEFAULT 0,
is_system BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (session_id, ordinal),
FOREIGN KEY (session_id)
REFERENCES sessions(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS tool_calls (
id BIGSERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
tool_name TEXT NOT NULL,
category TEXT NOT NULL,
call_index INT NOT NULL DEFAULT 0,
tool_use_id TEXT NOT NULL DEFAULT '',
input_json TEXT,
skill_name TEXT,
result_content_length INT,
result_content TEXT,
subagent_session_id TEXT,
message_ordinal INT NOT NULL,
FOREIGN KEY (session_id)
REFERENCES sessions(id) ON DELETE CASCADE
);`
if _, err := pg.ExecContext(ctx, legacyDDL); err != nil {
t.Fatalf("creating legacy tables: %v", err)
}

// Verify tool_result_events does not exist yet.
if err := CheckSchemaCompat(ctx, pg); err == nil {
t.Fatal("expected CheckSchemaCompat to fail on legacy schema")
}

// Run EnsureSchema — should create the missing table.
if err := EnsureSchema(ctx, pg, "agentsview"); err != nil {
t.Fatalf("EnsureSchema on legacy schema: %v", err)
}

// Now the compat check should pass.
if err := CheckSchemaCompat(ctx, pg); err != nil {
t.Fatalf(
"CheckSchemaCompat after migration: %v", err,
)
}
}

func TestPushSingleSession(t *testing.T) {
pgURL := testPGURL(t)
cleanPGSchema(t, pgURL)
Expand Down