feat(routing): add logical group schema foundation

This commit is contained in:
phamnazage-jpg
2026-05-28 15:26:16 +08:00
parent e549549b4d
commit 7f75d8a670
3 changed files with 294 additions and 1 deletions

View File

@@ -0,0 +1,78 @@
CREATE TABLE logical_groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
logical_group_id TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
status TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
route_policy TEXT NOT NULL DEFAULT 'priority',
sticky_mode TEXT NOT NULL DEFAULT 'conversation_preferred',
conversation_ttl_seconds INTEGER NOT NULL DEFAULT 7200,
user_model_ttl_seconds INTEGER NOT NULL DEFAULT 1800,
failover_threshold INTEGER NOT NULL DEFAULT 2,
cooldown_seconds INTEGER NOT NULL DEFAULT 600,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_logical_groups_status ON logical_groups(status);
CREATE TABLE logical_group_models (
id INTEGER PRIMARY KEY AUTOINCREMENT,
logical_group_id TEXT NOT NULL,
public_model TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_logical_group_models_group
FOREIGN KEY (logical_group_id)
REFERENCES logical_groups(logical_group_id)
ON DELETE CASCADE,
CONSTRAINT uq_logical_group_models_group_model
UNIQUE (logical_group_id, public_model)
);
CREATE INDEX idx_logical_group_models_group_id ON logical_group_models(logical_group_id);
CREATE INDEX idx_logical_group_models_status ON logical_group_models(status);
CREATE TABLE logical_group_routes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
route_id TEXT NOT NULL UNIQUE,
logical_group_id TEXT NOT NULL,
name TEXT NOT NULL,
status TEXT NOT NULL,
priority INTEGER NOT NULL,
weight INTEGER NOT NULL DEFAULT 100,
shadow_group_id TEXT NOT NULL,
shadow_host_id TEXT NOT NULL,
upstream_base_url_hint TEXT NOT NULL DEFAULT '',
cooldown_until TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_logical_group_routes_group
FOREIGN KEY (logical_group_id)
REFERENCES logical_groups(logical_group_id)
ON DELETE CASCADE
);
CREATE INDEX idx_logical_group_routes_group_id ON logical_group_routes(logical_group_id);
CREATE INDEX idx_logical_group_routes_shadow_host_id ON logical_group_routes(shadow_host_id);
CREATE INDEX idx_logical_group_routes_status_priority ON logical_group_routes(status, priority);
CREATE TABLE logical_group_route_models (
id INTEGER PRIMARY KEY AUTOINCREMENT,
route_id TEXT NOT NULL,
public_model TEXT NOT NULL,
shadow_model TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_logical_group_route_models_route
FOREIGN KEY (route_id)
REFERENCES logical_group_routes(route_id)
ON DELETE CASCADE,
CONSTRAINT uq_logical_group_route_models_route_model
UNIQUE (route_id, public_model)
);
CREATE INDEX idx_logical_group_route_models_route_id ON logical_group_route_models(route_id);
CREATE INDEX idx_logical_group_route_models_status ON logical_group_route_models(status);

View File

@@ -102,6 +102,26 @@ func TestTableExists(t *testing.T) {
} }
} }
func TestOpenAppliesLogicalRoutingTables(t *testing.T) {
store := openTestDB(t)
db := store.SQLDB()
for _, table := range []string{
"logical_groups",
"logical_group_models",
"logical_group_routes",
"logical_group_route_models",
} {
found, err := tableExists(context.Background(), db, table)
if err != nil {
t.Fatalf("tableExists(%q) error = %v", table, err)
}
if !found {
t.Fatalf("tableExists(%q) = false, want true", table)
}
}
}
func TestDetectLegacy0001Schema(t *testing.T) { func TestDetectLegacy0001Schema(t *testing.T) {
store := openTestDB(t) store := openTestDB(t)
db := store.SQLDB() db := store.SQLDB()

View File

@@ -133,7 +133,15 @@ func TestStoreAppliesLatestMigration(t *testing.T) {
store := openTestStore(t) store := openTestStore(t)
defer closeTestStore(t, store) defer closeTestStore(t, store)
for _, table := range []string{"import_runs", "import_run_items", "import_run_item_events"} { for _, table := range []string{
"import_runs",
"import_run_items",
"import_run_item_events",
"logical_groups",
"logical_group_models",
"logical_group_routes",
"logical_group_route_models",
} {
if !tableExists(t, store.SQLDB(), table) { if !tableExists(t, store.SQLDB(), table) {
t.Fatalf("table %q does not exist after latest migration", table) t.Fatalf("table %q does not exist after latest migration", table)
} }
@@ -165,6 +173,185 @@ func TestStoreAppliesLatestMigration(t *testing.T) {
t.Fatalf("column %q missing from import_run_items", column) t.Fatalf("column %q missing from import_run_items", column)
} }
} }
for _, column := range []string{
"logical_group_id",
"display_name",
"route_policy",
"sticky_mode",
"conversation_ttl_seconds",
"user_model_ttl_seconds",
"failover_threshold",
"cooldown_seconds",
} {
if !tableColumnExists(t, store.SQLDB(), "logical_groups", column) {
t.Fatalf("column %q missing from logical_groups", column)
}
}
for _, column := range []string{
"logical_group_id",
"public_model",
"status",
} {
if !tableColumnExists(t, store.SQLDB(), "logical_group_models", column) {
t.Fatalf("column %q missing from logical_group_models", column)
}
}
for _, column := range []string{
"route_id",
"logical_group_id",
"priority",
"weight",
"shadow_group_id",
"shadow_host_id",
"upstream_base_url_hint",
"cooldown_until",
} {
if !tableColumnExists(t, store.SQLDB(), "logical_group_routes", column) {
t.Fatalf("column %q missing from logical_group_routes", column)
}
}
for _, column := range []string{
"route_id",
"public_model",
"shadow_model",
"status",
} {
if !tableColumnExists(t, store.SQLDB(), "logical_group_route_models", column) {
t.Fatalf("column %q missing from logical_group_route_models", column)
}
}
}
func TestStoreInitEnforcesLogicalRoutingConstraints(t *testing.T) {
ctx := context.Background()
store := openTestStore(t)
defer closeTestStore(t, store)
db := store.SQLDB()
mustExecSQL(t, db, `
INSERT INTO logical_groups (
logical_group_id,
display_name,
status
) VALUES (?, ?, ?)`,
"gpt-shared",
"GPT Shared",
"active",
)
if _, err := db.ExecContext(ctx, `
INSERT INTO logical_groups (
logical_group_id,
display_name,
status
) VALUES (?, ?, ?)`,
"gpt-shared",
"GPT Shared Duplicate",
"active",
); err == nil {
t.Fatal("duplicate logical_group_id insert error = nil, want unique constraint failure")
}
mustExecSQL(t, db, `
INSERT INTO logical_group_models (
logical_group_id,
public_model
) VALUES (?, ?)`,
"gpt-shared",
"gpt-5.4",
)
if _, err := db.ExecContext(ctx, `
INSERT INTO logical_group_models (
logical_group_id,
public_model
) VALUES (?, ?)`,
"gpt-shared",
"gpt-5.4",
); err == nil {
t.Fatal("duplicate logical_group_models insert error = nil, want unique constraint failure")
}
if _, err := db.ExecContext(ctx, `
INSERT INTO logical_group_routes (
route_id,
logical_group_id,
name,
status,
priority,
shadow_group_id,
shadow_host_id
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
"route-missing-group",
"missing-group",
"Missing Group Route",
"active",
10,
"shadow-group-a",
"shadow-host-a",
); err == nil {
t.Fatal("logical_group_routes missing group error = nil, want foreign key failure")
}
mustExecSQL(t, db, `
INSERT INTO logical_group_routes (
route_id,
logical_group_id,
name,
status,
priority,
shadow_group_id,
shadow_host_id
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
"route-asxs",
"gpt-shared",
"ASXS",
"active",
10,
"gpt-shared__asxs",
"remote43",
)
if _, err := db.ExecContext(ctx, `
INSERT INTO logical_group_route_models (
route_id,
public_model,
shadow_model
) VALUES (?, ?, ?)`,
"missing-route",
"gpt-5.4",
"gpt-5.4",
); err == nil {
t.Fatal("logical_group_route_models missing route error = nil, want foreign key failure")
}
mustExecSQL(t, db, `
INSERT INTO logical_group_route_models (
route_id,
public_model,
shadow_model
) VALUES (?, ?, ?)`,
"route-asxs",
"gpt-5.4",
"gpt-5.4",
)
if _, err := db.ExecContext(ctx, `
INSERT INTO logical_group_route_models (
route_id,
public_model,
shadow_model
) VALUES (?, ?, ?)`,
"route-asxs",
"gpt-5.4",
"gpt-5.4-alt",
); err == nil {
t.Fatal("duplicate logical_group_route_models insert error = nil, want unique constraint failure")
}
} }
func TestStoreInitBackfillsLedgerForCompletePreLedgerSchema(t *testing.T) { func TestStoreInitBackfillsLedgerForCompletePreLedgerSchema(t *testing.T) {
@@ -303,6 +490,14 @@ func mustExec(t *testing.T, db *sql.DB, statement string) {
} }
} }
func mustExecSQL(t *testing.T, db *sql.DB, statement string, args ...any) {
t.Helper()
if _, err := db.ExecContext(context.Background(), statement, args...); err != nil {
t.Fatalf("ExecContext() error = %v", err)
}
}
func closeTestStore(t *testing.T, store *sqlite.DB) { func closeTestStore(t *testing.T, store *sqlite.DB) {
t.Helper() t.Helper()