mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 10:52:17 +03:00
Merged, with one column-reconciliation gap closed. The fail-closed reasoning is right and the comments carry it well: a stored snapshot whose cues were replaced by `[redacted-video-transcript]` must not be rehydrated as continuation history, because forwarding placeholder text upstream as if it were the client's real turn is worse than making the client resend. Treating it exactly like `previous_response_not_found` means no new client-visible behaviour to document. Migration 173 does not collide — the tip runs to 172. **What I added:** `video_content_removed` to `ensureCallLogsColumns` in `src/lib/db/schemaColumns.ts`, plus a case in `tests/unit/db-schema-columns-split.test.ts`. `resolvePreviousResponseState` now SELECTs that column on every `previous_response_id` lookup. Migration 173 creates it, but this repo carries a separate reconciliation path for lineages that skipped a migration — and on such a database the SELECT would throw `no such column: video_content_removed` instead of failing closed. That is the same hole #12470 closed for `provider_connections.last_ping_at` earlier today, so the pattern was fresh. Verified red-then-green: stubbing the new reconciliation out drops the suite to 8/9; restored, 9/9. Validated on `release/v3.8.51`: `responses-continuation-store`, `save-call-log-persistence`, `video-bridge-log-redaction` and `db-schema-columns-split` all green (54 focused tests, 0 failures). `typecheck:core` and `lint` clean. The integration run logs `[DB] Added call_logs.video_content_removed column`, which is the reconciliation firing on a fresh test database.
317 lines
14 KiB
TypeScript
317 lines
14 KiB
TypeScript
/**
|
|
* db/schemaColumns.ts — idempotent schema-column reconciliation + table introspection.
|
|
*
|
|
* Extracted from db/core.ts (god-file decomposition): the helpers that bring older SQLite
|
|
* files up to the current column set (ALTER TABLE … ADD COLUMN, guarded by PRAGMA
|
|
* table_info) plus the small introspection utilities they build on. Each takes the db
|
|
* handle explicitly — no module state — so they live as a co-located leaf that core.ts
|
|
* calls during getDbInstance() bootstrap. Behavior-preserving move.
|
|
*/
|
|
|
|
import type { SqliteAdapter } from "./adapters/types";
|
|
|
|
type SqliteDatabase = SqliteAdapter;
|
|
|
|
export function ensureProviderConnectionsColumns(db: SqliteDatabase) {
|
|
try {
|
|
const columns = db.prepare("PRAGMA table_info(provider_connections)").all() as Array<{
|
|
name?: string;
|
|
}>;
|
|
const columnNames = new Set(columns.map((column) => String(column.name ?? "")));
|
|
for (const [column, type] of [
|
|
["auth_type", "TEXT"],
|
|
["name", "TEXT"],
|
|
["email", "TEXT"],
|
|
["display_name", "TEXT"],
|
|
["provider_specific_data", "TEXT"],
|
|
["rate_limit_protection", "INTEGER DEFAULT 0"],
|
|
["last_used_at", "TEXT"],
|
|
["default_model", "TEXT"], // legacy-schema hole; later data migrations read it
|
|
["last_ping_at", "TEXT"], // added by 123_quota_auto_ping; back-filled here for divergent lineages
|
|
["last_pinged_reset_key", "TEXT"], // added by 123_quota_auto_ping; back-filled here for divergent lineages
|
|
]) {
|
|
if (!columnNames.has(column)) {
|
|
db.exec(`ALTER TABLE provider_connections ADD COLUMN ${column} ${type}`);
|
|
console.log(`[DB] Added provider_connections.${column} column`);
|
|
}
|
|
}
|
|
if (!columnNames.has("group")) {
|
|
db.exec('ALTER TABLE provider_connections ADD COLUMN "group" TEXT');
|
|
console.log('[DB] Added provider_connections."group" column');
|
|
}
|
|
if (!columnNames.has("max_concurrent")) {
|
|
db.exec("ALTER TABLE provider_connections ADD COLUMN max_concurrent INTEGER");
|
|
console.log("[DB] Added provider_connections.max_concurrent column");
|
|
}
|
|
if (!columnNames.has("proxy_enabled")) {
|
|
db.exec(
|
|
"ALTER TABLE provider_connections ADD COLUMN proxy_enabled INTEGER NOT NULL DEFAULT 1"
|
|
);
|
|
console.log("[DB] Added provider_connections.proxy_enabled column");
|
|
}
|
|
if (!columnNames.has("per_key_proxy_enabled")) {
|
|
db.exec(
|
|
"ALTER TABLE provider_connections ADD COLUMN per_key_proxy_enabled INTEGER NOT NULL DEFAULT 0"
|
|
);
|
|
console.log("[DB] Added provider_connections.per_key_proxy_enabled column");
|
|
}
|
|
if (!columnNames.has("quota_visible")) {
|
|
db.exec(
|
|
"ALTER TABLE provider_connections ADD COLUMN quota_visible INTEGER NOT NULL DEFAULT 1"
|
|
);
|
|
console.log("[DB] Added provider_connections.quota_visible column");
|
|
}
|
|
if (!columnNames.has("quota_window_thresholds_json")) {
|
|
db.exec("ALTER TABLE provider_connections ADD COLUMN quota_window_thresholds_json TEXT");
|
|
console.log("[DB] Added provider_connections.quota_window_thresholds_json column");
|
|
}
|
|
if (!columnNames.has("rate_limit_overrides_json")) {
|
|
db.exec("ALTER TABLE provider_connections ADD COLUMN rate_limit_overrides_json TEXT");
|
|
console.log("[DB] Added provider_connections.rate_limit_overrides_json column");
|
|
}
|
|
// `refresh_token` is part of 001_initial_schema.sql, but `CREATE TABLE IF NOT EXISTS`
|
|
// is a no-op on a pre-existing legacy table that predates it — heal it defensively
|
|
// before the index below relies on it, or a very old DB fails startup entirely.
|
|
if (!columnNames.has("refresh_token")) {
|
|
db.exec("ALTER TABLE provider_connections ADD COLUMN refresh_token TEXT");
|
|
console.log("[DB] Added provider_connections.refresh_token column");
|
|
}
|
|
db.exec(
|
|
"CREATE INDEX IF NOT EXISTS idx_pc_max_concurrent ON provider_connections(provider, max_concurrent)"
|
|
);
|
|
db.exec(
|
|
"CREATE INDEX IF NOT EXISTS idx_pc_auth_active_refresh ON provider_connections(auth_type, is_active, refresh_token)"
|
|
);
|
|
db.exec(
|
|
"CREATE INDEX IF NOT EXISTS idx_pc_provider_auth_type ON provider_connections(provider, auth_type)"
|
|
);
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.warn("[DB] Failed to verify provider_connections schema:", message);
|
|
}
|
|
}
|
|
|
|
export function ensureUsageHistoryAccountIndex(db: SqliteDatabase) {
|
|
try {
|
|
db.exec("CREATE INDEX IF NOT EXISTS idx_uh_account_key ON usage_history(account_key)");
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.warn("[DB] Failed to verify usage_history account index:", message);
|
|
}
|
|
}
|
|
|
|
export function ensureUsageHistoryColumns(db: SqliteDatabase) {
|
|
try {
|
|
const columns = db.prepare("PRAGMA table_info(usage_history)").all() as Array<{
|
|
name?: string;
|
|
}>;
|
|
const columnNames = new Set(columns.map((column) => String(column.name ?? "")));
|
|
|
|
if (!columnNames.has("success")) {
|
|
db.exec("ALTER TABLE usage_history ADD COLUMN success INTEGER DEFAULT 1");
|
|
console.log("[DB] Added usage_history.success column");
|
|
}
|
|
if (!columnNames.has("latency_ms")) {
|
|
db.exec("ALTER TABLE usage_history ADD COLUMN latency_ms INTEGER DEFAULT 0");
|
|
console.log("[DB] Added usage_history.latency_ms column");
|
|
}
|
|
if (!columnNames.has("ttft_ms")) {
|
|
db.exec("ALTER TABLE usage_history ADD COLUMN ttft_ms INTEGER DEFAULT 0");
|
|
console.log("[DB] Added usage_history.ttft_ms column");
|
|
}
|
|
if (!columnNames.has("error_code")) {
|
|
db.exec("ALTER TABLE usage_history ADD COLUMN error_code TEXT");
|
|
console.log("[DB] Added usage_history.error_code column");
|
|
}
|
|
if (!columnNames.has("service_tier")) {
|
|
db.exec("ALTER TABLE usage_history ADD COLUMN service_tier TEXT DEFAULT 'standard'");
|
|
console.log("[DB] Added usage_history.service_tier column");
|
|
}
|
|
db.exec("CREATE INDEX IF NOT EXISTS idx_uh_service_tier ON usage_history(service_tier)");
|
|
if (!columnNames.has("combo_strategy")) {
|
|
db.exec("ALTER TABLE usage_history ADD COLUMN combo_strategy TEXT DEFAULT 'direct'");
|
|
console.log("[DB] Added usage_history.combo_strategy column");
|
|
}
|
|
db.exec("CREATE INDEX IF NOT EXISTS idx_uh_combo_strategy ON usage_history(combo_strategy)");
|
|
if (!columnNames.has("account_key")) {
|
|
db.exec("ALTER TABLE usage_history ADD COLUMN account_key TEXT");
|
|
console.log("[DB] Added usage_history.account_key column");
|
|
}
|
|
if (!columnNames.has("account_label")) {
|
|
db.exec("ALTER TABLE usage_history ADD COLUMN account_label TEXT");
|
|
console.log("[DB] Added usage_history.account_label column");
|
|
}
|
|
if (!columnNames.has("account_label_priority")) {
|
|
db.exec("ALTER TABLE usage_history ADD COLUMN account_label_priority INTEGER DEFAULT 0");
|
|
console.log("[DB] Added usage_history.account_label_priority column");
|
|
}
|
|
db.exec(
|
|
"CREATE INDEX IF NOT EXISTS idx_uh_provider_model_timestamp ON usage_history(provider, model, timestamp)"
|
|
);
|
|
db.exec(
|
|
`CREATE INDEX IF NOT EXISTS idx_uh_dedup ON usage_history(
|
|
timestamp,
|
|
COALESCE(provider, ''),
|
|
COALESCE(model, ''),
|
|
COALESCE(connection_id, ''),
|
|
COALESCE(api_key_id, ''),
|
|
tokens_input,
|
|
tokens_output
|
|
)`
|
|
);
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.warn("[DB] Failed to verify usage_history schema:", message);
|
|
}
|
|
}
|
|
|
|
export function ensureCallLogsColumns(db: SqliteDatabase) {
|
|
try {
|
|
const columns = db.prepare("PRAGMA table_info(call_logs)").all() as Array<{
|
|
name?: string;
|
|
}>;
|
|
const columnNames = new Set(columns.map((column) => String(column.name ?? "")));
|
|
|
|
if (!columnNames.has("artifact_relpath")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN artifact_relpath TEXT");
|
|
console.log("[DB] Added call_logs.artifact_relpath column");
|
|
}
|
|
if (!columnNames.has("has_pipeline_details")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN has_pipeline_details INTEGER DEFAULT 0");
|
|
console.log("[DB] Added call_logs.has_pipeline_details column");
|
|
}
|
|
if (!columnNames.has("requested_model")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN requested_model TEXT DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.requested_model column");
|
|
}
|
|
if (!columnNames.has("request_type")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN request_type TEXT DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.request_type column");
|
|
}
|
|
if (!columnNames.has("tokens_cache_read")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN tokens_cache_read INTEGER DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.tokens_cache_read column");
|
|
}
|
|
if (!columnNames.has("tokens_cache_creation")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN tokens_cache_creation INTEGER DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.tokens_cache_creation column");
|
|
}
|
|
if (!columnNames.has("tokens_reasoning")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN tokens_reasoning INTEGER DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.tokens_reasoning column");
|
|
}
|
|
if (!columnNames.has("cache_source")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN cache_source TEXT DEFAULT 'upstream'");
|
|
console.log("[DB] Added call_logs.cache_source column");
|
|
}
|
|
if (!columnNames.has("combo_step_id")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN combo_step_id TEXT DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.combo_step_id column");
|
|
}
|
|
if (!columnNames.has("combo_execution_key")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN combo_execution_key TEXT DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.combo_execution_key column");
|
|
}
|
|
if (!columnNames.has("error_summary")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN error_summary TEXT DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.error_summary column");
|
|
}
|
|
if (!columnNames.has("detail_state")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN detail_state TEXT DEFAULT 'none'");
|
|
console.log("[DB] Added call_logs.detail_state column");
|
|
}
|
|
if (!columnNames.has("artifact_size_bytes")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN artifact_size_bytes INTEGER DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.artifact_size_bytes column");
|
|
}
|
|
if (!columnNames.has("artifact_sha256")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN artifact_sha256 TEXT DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.artifact_sha256 column");
|
|
}
|
|
if (!columnNames.has("has_request_body")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN has_request_body INTEGER DEFAULT 0");
|
|
console.log("[DB] Added call_logs.has_request_body column");
|
|
}
|
|
if (!columnNames.has("has_response_body")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN has_response_body INTEGER DEFAULT 0");
|
|
console.log("[DB] Added call_logs.has_response_body column");
|
|
}
|
|
if (!columnNames.has("request_summary")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN request_summary TEXT DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.request_summary column");
|
|
}
|
|
// added by 173_call_logs_video_content_removed; back-filled here because
|
|
// resolvePreviousResponseState SELECTs it on every continuation lookup — a
|
|
// lineage that skipped the migration would throw "no such column" there
|
|
// rather than fail closed. Same hole #12470 closed for provider_connections.
|
|
if (!columnNames.has("video_content_removed")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN video_content_removed INTEGER NOT NULL DEFAULT 0");
|
|
console.log("[DB] Added call_logs.video_content_removed column");
|
|
}
|
|
if (!columnNames.has("correlation_id")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN correlation_id TEXT DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.correlation_id column");
|
|
}
|
|
if (!columnNames.has("model_pinned")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN model_pinned INTEGER DEFAULT 0");
|
|
console.log("[DB] Added call_logs.model_pinned column");
|
|
}
|
|
if (!columnNames.has("session_tag")) {
|
|
db.exec("ALTER TABLE call_logs ADD COLUMN session_tag TEXT DEFAULT NULL");
|
|
console.log("[DB] Added call_logs.session_tag column");
|
|
}
|
|
|
|
db.exec(
|
|
"CREATE INDEX IF NOT EXISTS idx_call_logs_requested_model ON call_logs(requested_model)"
|
|
);
|
|
db.exec("CREATE INDEX IF NOT EXISTS idx_call_logs_request_type ON call_logs(request_type)");
|
|
db.exec(
|
|
"CREATE INDEX IF NOT EXISTS idx_cl_combo_target ON call_logs(combo_name, combo_execution_key, timestamp)"
|
|
);
|
|
db.exec("CREATE INDEX IF NOT EXISTS idx_cl_correlation_id ON call_logs(correlation_id)");
|
|
db.exec("CREATE INDEX IF NOT EXISTS idx_cl_session_tag ON call_logs(session_tag)");
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.warn("[DB] Failed to verify call_logs schema:", message);
|
|
}
|
|
}
|
|
|
|
export function ensureProxyLogsColumns(db: SqliteDatabase) {
|
|
try {
|
|
const columns = db.prepare("PRAGMA table_info(proxy_logs)").all() as Array<{
|
|
name?: string;
|
|
}>;
|
|
const columnNames = new Set(columns.map((column) => String(column.name ?? "")));
|
|
if (!columnNames.has("egress_ip")) {
|
|
db.exec("ALTER TABLE proxy_logs ADD COLUMN egress_ip TEXT");
|
|
console.log("[DB] Added proxy_logs.egress_ip column");
|
|
}
|
|
} catch (error: unknown) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.warn("[DB] Failed to verify proxy_logs schema:", message);
|
|
}
|
|
}
|
|
|
|
export function hasColumn(db: SqliteDatabase, tableName: string, columnName: string): boolean {
|
|
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ name?: string }>;
|
|
return rows.some((row) => row.name === columnName);
|
|
}
|
|
|
|
export function hasTable(db: SqliteDatabase, tableName: string): boolean {
|
|
return Boolean(
|
|
db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName)
|
|
);
|
|
}
|
|
|
|
export function quoteIdentifier(identifier: string): string {
|
|
return `"${identifier.replace(/"/g, '""')}"`;
|
|
}
|
|
|
|
export function getTableColumns(db: SqliteDatabase, tableName: string): string[] {
|
|
return (
|
|
db.prepare(`PRAGMA table_info(${quoteIdentifier(tableName)})`).all() as Array<{ name?: string }>
|
|
)
|
|
.map((column) => String(column.name ?? ""))
|
|
.filter((column) => column.length > 0);
|
|
}
|