mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
@@ -25,6 +25,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
- **Dashboard**: remove the dead, unconditional `useLiveRequests()` call from `HomePageClient.tsx` — it crashed the `/home` page in production builds with `ReferenceError: useLiveRequests is not defined` (#4759, #4745) and opened the live-dashboard WebSocket even when Provider Topology was hidden (#4596). The live feed remains owned by the settings-gated `HomeProviderTopologySection` ([#4761](https://github.com/diegosouzapw/OmniRoute/pull/4761) — thanks @diegosouzapw).
|
||||
- **Providers dashboard**: dedupe provider nodes by id when adding a compatible provider (`upsertProviderNodeById`) so the same provider can no longer appear twice and no-op adds don't invalidate the compatible-provider memo ([#4768](https://github.com/diegosouzapw/OmniRoute/pull/4768) — closes #4746, thanks @diegosouzapw).
|
||||
- **Storage VACUUM**: the scheduled VACUUM job now follows the Storage page settings (`scheduledVacuum` / `vacuumHour`) as the single source of truth; the legacy env-flag control path was removed ([#4726](https://github.com/diegosouzapw/OmniRoute/pull/4726) — thanks @rdself).
|
||||
- **Storage SQLite tuning**: `Cache Size` is now a positive KiB setting (for example, `16384`) that applies to SQLite as `PRAGMA cache_size = -16384`; Page Size and Cache Size changes are applied to the live database instead of being persisted only in the settings table.
|
||||
- **Tiers**: no-auth providers are now counted as free, and the free-tier filter returns an empty set instead of falling through to every provider ([#4753](https://github.com/diegosouzapw/OmniRoute/pull/4753) — thanks @megamen32 / @diegosouzapw).
|
||||
- **Combos**: auto-promote `zeroLatencyOptimizationsEnabled` so legacy configs (pre-3.8.33 `fallbackCompressionMode="lite"`) round-trip cleanly on the first GUI edit ([#4774](https://github.com/diegosouzapw/OmniRoute/pull/4774) — thanks @KooshaPari / @diegosouzapw).
|
||||
|
||||
|
||||
@@ -40,9 +40,10 @@ For **single-user, single-instance** deployments (the primary OmniRoute use case
|
||||
```ts
|
||||
// src/lib/db/core.ts
|
||||
db.pragma("journal_mode = WAL");
|
||||
db.pragma("busy_timeout = 5000");
|
||||
db.pragma("busy_timeout = 2000");
|
||||
db.pragma("synchronous = NORMAL");
|
||||
db.pragma("cache_size = -2048");
|
||||
// Settings > System & Storage > Cache Size is applied as KiB.
|
||||
db.pragma("cache_size = -16384");
|
||||
```
|
||||
|
||||
WAL allows **concurrent reads** during writes — important for the dashboard, which queries while requests are being recorded.
|
||||
|
||||
@@ -57,6 +57,7 @@ export const INTENTIONALLY_INTERNAL = new Set([
|
||||
"migrationRunner", // db-internal: importado por db/core.ts (runMigrations ao inicializar o DB)
|
||||
"notion", // intentionally-internal: settings/notion API route + open-sse/mcp-server/tools/notionTools.ts
|
||||
"obsidian", // intentionally-internal: src/lib/obsidianSync.ts + settings/obsidian route + MCP obsidianTools.ts
|
||||
"optimizationSettings", // db-internal: imported by db/core.ts for SQLite PRAGMA application helpers that require the live adapter
|
||||
"pluginMetrics", // DEAD? (production): write path não foi conectado ainda (documentado no cabeçalho do módulo); testado por tests/unit/plugins-metrics.test.ts
|
||||
"prompts", // DEAD? (production): zero callers de produção encontrados; domínio domain/prompts.ts é independente; testado por tests/integration/proxy-pipeline.test.ts
|
||||
"providerNodeSelect", // db-internal: importado só por db/providers.ts (selectProviderNodeForConnection — lógica pura de seleção de provider node split do providers.ts, #4421)
|
||||
|
||||
@@ -1030,18 +1030,18 @@ export default function SystemStorageTab() {
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-text-muted mb-1">
|
||||
Cache Size (KB, negative = % of RAM)
|
||||
</label>
|
||||
<label className="block text-xs text-text-muted mb-1">Cache Size (KB)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
step="1024"
|
||||
value={dbSettings.optimization.cacheSize}
|
||||
onChange={(e) =>
|
||||
setDbSettings({
|
||||
...dbSettings,
|
||||
optimization: {
|
||||
...dbSettings.optimization,
|
||||
cacheSize: parseInt(e.target.value) || -2000,
|
||||
cacheSize: parseInt(e.target.value) || 16384,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ import { runMigrations } from "./migrationRunner";
|
||||
import { runDbHealthCheck } from "./healthCheck";
|
||||
import { resetAllDbModuleState } from "./stateReset";
|
||||
import { parseStoredPayload } from "../logPayloads";
|
||||
import { DEFAULT_DATABASE_SETTINGS, type DatabaseSettings } from "@/types/databaseSettings";
|
||||
import {
|
||||
applyDatabaseOptimizationSettingsForDb,
|
||||
applyStoredDatabaseOptimizationSettings,
|
||||
getAutoVacuumModeForDb,
|
||||
setAutoVacuumForDb,
|
||||
setCacheSizeForDb,
|
||||
setPageSizeForDb,
|
||||
} from "./optimizationSettings";
|
||||
import {
|
||||
buildArtifactRelativePath,
|
||||
writeCallArtifact,
|
||||
@@ -29,6 +38,7 @@ import { invalidateDbCache } from "./readCache";
|
||||
type SqliteDatabase = SqliteAdapter;
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
type CheckpointMode = "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE";
|
||||
type DatabaseOptimizationSettings = DatabaseSettings["optimization"];
|
||||
type PreservedTableSnapshot = {
|
||||
table: string;
|
||||
rowCount: number;
|
||||
@@ -1340,7 +1350,7 @@ export function getDbInstance(): SqliteDatabase {
|
||||
// contended op can no longer freeze the loop past the host watchdog's 6s liveness probe.
|
||||
db.pragma("busy_timeout = 2000");
|
||||
db.pragma("synchronous = NORMAL");
|
||||
db.pragma("cache_size = -2048");
|
||||
db.pragma(`cache_size = -${DEFAULT_DATABASE_SETTINGS.optimization.cacheSize}`);
|
||||
db.exec(SCHEMA_SQL);
|
||||
ensureProviderConnectionsColumns(db);
|
||||
ensureUsageHistoryColumns(db);
|
||||
@@ -1361,6 +1371,8 @@ export function getDbInstance(): SqliteDatabase {
|
||||
|
||||
runMigrations(db, { isNewDb });
|
||||
|
||||
applyStoredDatabaseOptimizationSettings(db);
|
||||
|
||||
offloadLegacyCallLogDetails(db);
|
||||
|
||||
// Auto-migrate from db.json if exists
|
||||
@@ -1731,44 +1743,16 @@ function migrateFromJson(db: SqliteDatabase, jsonPath: string) {
|
||||
|
||||
// ──────────────── Auto-Vacuum Management ────────────────
|
||||
|
||||
export function applyDatabaseOptimizationSettings(settings: DatabaseOptimizationSettings): void {
|
||||
applyDatabaseOptimizationSettingsForDb(getDbInstance(), settings, { applyPersistent: true });
|
||||
}
|
||||
|
||||
export function setAutoVacuum(mode: "NONE" | "FULL" | "INCREMENTAL"): void {
|
||||
const db = getDbInstance();
|
||||
|
||||
const currentMode = db.pragma("auto_vacuum", { simple: true }) as number;
|
||||
const modeMap: Record<string, number> = {
|
||||
NONE: 0,
|
||||
FULL: 1,
|
||||
INCREMENTAL: 2,
|
||||
};
|
||||
|
||||
const targetMode = modeMap[mode];
|
||||
|
||||
if (currentMode === targetMode) {
|
||||
console.log(`[DB] auto_vacuum already set to ${mode}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[DB] Changing auto_vacuum from ${currentMode} to ${mode} (${targetMode})`);
|
||||
|
||||
db.pragma(`auto_vacuum = ${targetMode}`);
|
||||
|
||||
db.exec("VACUUM");
|
||||
|
||||
const newMode = db.pragma("auto_vacuum", { simple: true }) as number;
|
||||
console.log(`[DB] auto_vacuum changed to ${newMode}`);
|
||||
setAutoVacuumForDb(getDbInstance(), mode);
|
||||
}
|
||||
|
||||
export function getAutoVacuumMode(): "NONE" | "FULL" | "INCREMENTAL" {
|
||||
const db = getDbInstance();
|
||||
const mode = db.pragma("auto_vacuum", { simple: true }) as number;
|
||||
|
||||
const modeMap: Record<number, "NONE" | "FULL" | "INCREMENTAL"> = {
|
||||
0: "NONE",
|
||||
1: "FULL",
|
||||
2: "INCREMENTAL",
|
||||
};
|
||||
|
||||
return modeMap[mode] || "NONE";
|
||||
return getAutoVacuumModeForDb(getDbInstance());
|
||||
}
|
||||
|
||||
export function runManualVacuum(): { success: boolean; duration: number; error?: string } {
|
||||
@@ -1790,35 +1774,9 @@ export function runManualVacuum(): { success: boolean; duration: number; error?:
|
||||
}
|
||||
|
||||
export function setPageSize(pageSize: number): void {
|
||||
const db = getDbInstance();
|
||||
const currentPageSize = db.pragma("page_size", { simple: true }) as number;
|
||||
|
||||
if (currentPageSize === pageSize) {
|
||||
console.log(`[DB] page_size already set to ${pageSize}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[DB] Changing page_size from ${currentPageSize} to ${pageSize}`);
|
||||
db.pragma(`page_size = ${pageSize}`);
|
||||
db.exec("VACUUM");
|
||||
|
||||
const newPageSize = db.pragma("page_size", { simple: true }) as number;
|
||||
console.log(`[DB] page_size changed to ${newPageSize}`);
|
||||
setPageSizeForDb(getDbInstance(), pageSize);
|
||||
}
|
||||
|
||||
export function setCacheSize(cacheSizeKb: number): void {
|
||||
const db = getDbInstance();
|
||||
const currentCacheSize = db.pragma("cache_size", { simple: true }) as number;
|
||||
const targetCacheSize = -cacheSizeKb;
|
||||
|
||||
if (currentCacheSize === targetCacheSize) {
|
||||
console.log(`[DB] cache_size already set to ${cacheSizeKb}KB`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[DB] Changing cache_size from ${Math.abs(currentCacheSize)}KB to ${cacheSizeKb}KB`);
|
||||
db.pragma(`cache_size = ${targetCacheSize}`);
|
||||
|
||||
const newCacheSize = db.pragma("cache_size", { simple: true }) as number;
|
||||
console.log(`[DB] cache_size changed to ${Math.abs(newCacheSize)}KB`);
|
||||
setCacheSizeForDb(getDbInstance(), cacheSizeKb);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
||||
import { DEFAULT_DATABASE_SETTINGS, type DatabaseSettings } from "@/types/databaseSettings";
|
||||
|
||||
import { backupDbFile } from "./backup";
|
||||
import { DATA_DIR, SQLITE_FILE, getDbInstance } from "./core";
|
||||
import { DATA_DIR, SQLITE_FILE, applyDatabaseOptimizationSettings, getDbInstance } from "./core";
|
||||
import { invalidateDbCache } from "./readCache";
|
||||
import { getDatabaseStats } from "./stats";
|
||||
import { getState as getVacuumSchedulerState, refreshVacuumScheduler } from "./vacuumScheduler";
|
||||
@@ -95,6 +95,15 @@ function toBooleanSetting(value: unknown): boolean | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeOptimizationSettings(settings: UserDatabaseSettings) {
|
||||
const fallback = DEFAULT_DATABASE_SETTINGS.optimization.cacheSize;
|
||||
const numericCacheSize = Number(settings.optimization.cacheSize);
|
||||
settings.optimization.cacheSize =
|
||||
Number.isFinite(numericCacheSize) && numericCacheSize > 0
|
||||
? Math.min(1000000, Math.floor(numericCacheSize))
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function readNamespace(namespace: string): Record<string, unknown> {
|
||||
const db = getDbInstance();
|
||||
const rows = db
|
||||
@@ -220,6 +229,7 @@ export function getUserDatabaseSettings(): UserDatabaseSettings {
|
||||
mergeTopLevelSections(settings, mainSettings);
|
||||
mergeDatabaseSettingsNamespace(settings, readNamespace(DATABASE_SETTINGS_NAMESPACE));
|
||||
mergeRuntimeLogSettings(settings, mainSettings);
|
||||
normalizeOptimizationSettings(settings);
|
||||
|
||||
return settings;
|
||||
}
|
||||
@@ -259,6 +269,7 @@ export function updateDatabaseSettings(
|
||||
mergeSectionObject(nextSettings, section, updates[section]);
|
||||
}
|
||||
}
|
||||
normalizeOptimizationSettings(nextSettings);
|
||||
|
||||
const db = getDbInstance();
|
||||
const insert = db.prepare(
|
||||
@@ -289,7 +300,10 @@ export function updateDatabaseSettings(
|
||||
|
||||
backupDbFile("pre-write");
|
||||
invalidateDbCache("settings");
|
||||
if (optimizationUpdated) refreshVacuumScheduler();
|
||||
if (optimizationUpdated) {
|
||||
applyDatabaseOptimizationSettings(nextSettings.optimization);
|
||||
refreshVacuumScheduler();
|
||||
}
|
||||
|
||||
return nextSettings;
|
||||
}
|
||||
|
||||
@@ -40,5 +40,5 @@ INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSetting
|
||||
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'autoVacuumMode', '"INCREMENTAL"');
|
||||
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'scheduledVacuum', '"weekly"');
|
||||
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'pageSize', '4096');
|
||||
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'cacheSize', '10000');
|
||||
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'cacheSize', '16384');
|
||||
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('databaseSettings', 'mmapSize', '268435456');
|
||||
|
||||
10
src/lib/db/migrations/104_normalize_database_cache_size.sql
Normal file
10
src/lib/db/migrations/104_normalize_database_cache_size.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- 104_normalize_database_cache_size.sql
|
||||
-- Normalize the old SQLite cache-size default to the new UI contract:
|
||||
-- databaseSettings.optimization.cacheSize is a positive KiB value, e.g. 16384.
|
||||
-- Only legacy defaults are rewritten; custom positive values are preserved.
|
||||
|
||||
UPDATE key_value
|
||||
SET value = '16384'
|
||||
WHERE namespace = 'databaseSettings'
|
||||
AND key IN ('cacheSize', 'optimization.cacheSize')
|
||||
AND TRIM(value) IN ('-2000', '"-2000"', '10000', '"10000"');
|
||||
284
src/lib/db/optimizationSettings.ts
Normal file
284
src/lib/db/optimizationSettings.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
import { DEFAULT_DATABASE_SETTINGS, type DatabaseSettings } from "@/types/databaseSettings";
|
||||
|
||||
import type { SqliteAdapter } from "./adapters/types";
|
||||
|
||||
type SqliteDatabase = SqliteAdapter;
|
||||
type DatabaseOptimizationSettings = DatabaseSettings["optimization"];
|
||||
type AutoVacuumMode = DatabaseOptimizationSettings["autoVacuumMode"];
|
||||
|
||||
const AUTO_VACUUM_MODE_TO_PRAGMA: Record<AutoVacuumMode, number> = {
|
||||
NONE: 0,
|
||||
FULL: 1,
|
||||
INCREMENTAL: 2,
|
||||
};
|
||||
|
||||
const PRAGMA_TO_AUTO_VACUUM_MODE: Record<number, AutoVacuumMode> = {
|
||||
0: "NONE",
|
||||
1: "FULL",
|
||||
2: "INCREMENTAL",
|
||||
};
|
||||
|
||||
function parseKeyValueJson(raw: string | null | undefined): unknown {
|
||||
if (raw === null || raw === undefined) return undefined;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function normalizeAutoVacuumMode(value: unknown, fallback: AutoVacuumMode): AutoVacuumMode {
|
||||
return typeof value === "string" && value in AUTO_VACUUM_MODE_TO_PRAGMA
|
||||
? (value as AutoVacuumMode)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function normalizePageSizeBytes(value: unknown, fallback: number): number {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
const pageSize = Math.floor(numeric);
|
||||
if (pageSize < 512 || pageSize > 65536 || pageSize % 512 !== 0) return fallback;
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
function normalizeVacuumHour(value: unknown, fallback: number): number {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
return Math.min(23, Math.max(0, Math.floor(numeric)));
|
||||
}
|
||||
|
||||
function normalizeStoredCacheSizeKb(value: unknown, fallback: number): number {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) return fallback;
|
||||
const cacheSizeKb = Math.floor(numeric);
|
||||
if (cacheSizeKb < 1 || cacheSizeKb > 1000000) return fallback;
|
||||
return cacheSizeKb;
|
||||
}
|
||||
|
||||
function requireCacheSizeKb(value: number): number {
|
||||
if (!Number.isInteger(value) || value < 1 || value > 1000000) {
|
||||
throw new Error("cache_size must be a positive KiB value between 1 and 1000000");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function mergeOptimizationSettings(
|
||||
target: DatabaseOptimizationSettings,
|
||||
value: unknown
|
||||
): DatabaseOptimizationSettings {
|
||||
if (!isRecord(value)) return target;
|
||||
return {
|
||||
...target,
|
||||
autoVacuumMode: normalizeAutoVacuumMode(value.autoVacuumMode, target.autoVacuumMode),
|
||||
scheduledVacuum:
|
||||
typeof value.scheduledVacuum === "string" &&
|
||||
["never", "daily", "weekly", "monthly"].includes(value.scheduledVacuum)
|
||||
? (value.scheduledVacuum as DatabaseOptimizationSettings["scheduledVacuum"])
|
||||
: target.scheduledVacuum,
|
||||
vacuumHour: normalizeVacuumHour(value.vacuumHour, target.vacuumHour),
|
||||
pageSize: normalizePageSizeBytes(value.pageSize, target.pageSize),
|
||||
cacheSize: normalizeStoredCacheSizeKb(value.cacheSize, target.cacheSize),
|
||||
optimizeOnStartup:
|
||||
typeof value.optimizeOnStartup === "boolean"
|
||||
? value.optimizeOnStartup
|
||||
: target.optimizeOnStartup,
|
||||
};
|
||||
}
|
||||
|
||||
function readDatabaseOptimizationSettings(db: SqliteDatabase): DatabaseOptimizationSettings {
|
||||
let settings: DatabaseOptimizationSettings = { ...DEFAULT_DATABASE_SETTINGS.optimization };
|
||||
|
||||
try {
|
||||
const rows = db
|
||||
.prepare("SELECT namespace, key, value FROM key_value WHERE namespace IN (?, ?)")
|
||||
.all("settings", "databaseSettings") as Array<{
|
||||
namespace: string;
|
||||
key: string;
|
||||
value: string | null;
|
||||
}>;
|
||||
const byNamespace: Record<string, Record<string, unknown>> = {
|
||||
settings: {},
|
||||
databaseSettings: {},
|
||||
};
|
||||
for (const row of rows) {
|
||||
byNamespace[row.namespace] ??= {};
|
||||
byNamespace[row.namespace][row.key] = parseKeyValueJson(row.value);
|
||||
}
|
||||
|
||||
const mainSettings = byNamespace.settings ?? {};
|
||||
const databaseSettingsValue = mainSettings.databaseSettings;
|
||||
if (isRecord(databaseSettingsValue)) {
|
||||
settings = mergeOptimizationSettings(settings, databaseSettingsValue.optimization);
|
||||
}
|
||||
settings = mergeOptimizationSettings(settings, mainSettings.optimization);
|
||||
|
||||
const databaseSettings = byNamespace.databaseSettings ?? {};
|
||||
const optimizeOnStartup =
|
||||
databaseSettings["optimization.optimizeOnStartup"] ?? databaseSettings.optimizeOnStartup;
|
||||
settings = mergeOptimizationSettings(settings, databaseSettings.optimization);
|
||||
settings = {
|
||||
...settings,
|
||||
autoVacuumMode: normalizeAutoVacuumMode(
|
||||
databaseSettings["optimization.autoVacuumMode"] ?? databaseSettings.autoVacuumMode,
|
||||
settings.autoVacuumMode
|
||||
),
|
||||
pageSize: normalizePageSizeBytes(
|
||||
databaseSettings["optimization.pageSize"] ?? databaseSettings.pageSize,
|
||||
settings.pageSize
|
||||
),
|
||||
cacheSize: normalizeStoredCacheSizeKb(
|
||||
databaseSettings["optimization.cacheSize"] ?? databaseSettings.cacheSize,
|
||||
settings.cacheSize
|
||||
),
|
||||
optimizeOnStartup:
|
||||
typeof optimizeOnStartup === "boolean" ? optimizeOnStartup : settings.optimizeOnStartup,
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[DB] Failed to read database optimization settings; using defaults: ${message}`);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
export function setCacheSizeForDb(db: SqliteDatabase, cacheSizeKb: number): void {
|
||||
const normalizedCacheSizeKb = requireCacheSizeKb(cacheSizeKb);
|
||||
const currentCacheSize = db.pragma("cache_size", { simple: true }) as number;
|
||||
const targetCacheSize = -normalizedCacheSizeKb;
|
||||
|
||||
if (currentCacheSize === targetCacheSize) {
|
||||
console.log(`[DB] cache_size already set to ${normalizedCacheSizeKb}KB`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[DB] Changing cache_size from ${Math.abs(currentCacheSize)}KB to ${normalizedCacheSizeKb}KB`
|
||||
);
|
||||
db.pragma(`cache_size = ${targetCacheSize}`);
|
||||
|
||||
const newCacheSize = db.pragma("cache_size", { simple: true }) as number;
|
||||
if (newCacheSize !== targetCacheSize) {
|
||||
throw new Error(
|
||||
`cache_size change did not take effect (expected ${targetCacheSize}, got ${newCacheSize})`
|
||||
);
|
||||
}
|
||||
console.log(`[DB] cache_size changed to ${Math.abs(newCacheSize)}KB`);
|
||||
}
|
||||
|
||||
function applyPersistentOptimizationPragmas(
|
||||
db: SqliteDatabase,
|
||||
settings: DatabaseOptimizationSettings
|
||||
): void {
|
||||
const targetAutoVacuum = AUTO_VACUUM_MODE_TO_PRAGMA[settings.autoVacuumMode];
|
||||
const targetPageSize = normalizePageSizeBytes(
|
||||
settings.pageSize,
|
||||
DEFAULT_DATABASE_SETTINGS.optimization.pageSize
|
||||
);
|
||||
const currentAutoVacuum = db.pragma("auto_vacuum", { simple: true }) as number;
|
||||
const currentPageSize = db.pragma("page_size", { simple: true }) as number;
|
||||
|
||||
if (currentAutoVacuum === targetAutoVacuum && currentPageSize === targetPageSize) return;
|
||||
|
||||
const originalJournalMode = String(
|
||||
db.pragma("journal_mode", { simple: true }) ?? ""
|
||||
).toUpperCase();
|
||||
const shouldRestoreWal = originalJournalMode === "WAL";
|
||||
|
||||
console.log(
|
||||
`[DB] Applying persistent optimization settings ` +
|
||||
`(auto_vacuum ${currentAutoVacuum}->${targetAutoVacuum}, ` +
|
||||
`page_size ${currentPageSize}->${targetPageSize})`
|
||||
);
|
||||
|
||||
try {
|
||||
if (shouldRestoreWal) db.pragma("journal_mode = DELETE");
|
||||
db.pragma(`auto_vacuum = ${targetAutoVacuum}`);
|
||||
db.pragma(`page_size = ${targetPageSize}`);
|
||||
db.exec("VACUUM");
|
||||
} finally {
|
||||
if (shouldRestoreWal) {
|
||||
try {
|
||||
db.pragma("journal_mode = WAL");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(`[DB] Failed to restore WAL mode after optimization settings: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newAutoVacuum = db.pragma("auto_vacuum", { simple: true }) as number;
|
||||
const newPageSize = db.pragma("page_size", { simple: true }) as number;
|
||||
if (newAutoVacuum !== targetAutoVacuum || newPageSize !== targetPageSize) {
|
||||
throw new Error(
|
||||
`database optimization settings did not take effect ` +
|
||||
`(auto_vacuum expected ${targetAutoVacuum}, got ${newAutoVacuum}; ` +
|
||||
`page_size expected ${targetPageSize}, got ${newPageSize})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function applyDatabaseOptimizationSettingsForDb(
|
||||
db: SqliteDatabase,
|
||||
settings: DatabaseOptimizationSettings,
|
||||
options: { applyPersistent: boolean }
|
||||
): void {
|
||||
if (options.applyPersistent) applyPersistentOptimizationPragmas(db, settings);
|
||||
setCacheSizeForDb(
|
||||
db,
|
||||
normalizeStoredCacheSizeKb(settings.cacheSize, DEFAULT_DATABASE_SETTINGS.optimization.cacheSize)
|
||||
);
|
||||
}
|
||||
|
||||
export function applyStoredDatabaseOptimizationSettings(db: SqliteDatabase): void {
|
||||
const settings = readDatabaseOptimizationSettings(db);
|
||||
// Startup can happen concurrently in test workers and clustered hosts. Only
|
||||
// restore connection-local settings here; page_size/auto_vacuum require VACUUM
|
||||
// and are applied synchronously when the Storage settings are saved.
|
||||
applyDatabaseOptimizationSettingsForDb(db, settings, {
|
||||
applyPersistent: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function setAutoVacuumForDb(db: SqliteDatabase, mode: AutoVacuumMode): void {
|
||||
const currentMode = db.pragma("auto_vacuum", { simple: true }) as number;
|
||||
const targetMode = AUTO_VACUUM_MODE_TO_PRAGMA[mode];
|
||||
|
||||
if (currentMode === targetMode) {
|
||||
console.log(`[DB] auto_vacuum already set to ${mode}`);
|
||||
return;
|
||||
}
|
||||
|
||||
applyPersistentOptimizationPragmas(db, {
|
||||
...DEFAULT_DATABASE_SETTINGS.optimization,
|
||||
autoVacuumMode: mode,
|
||||
pageSize: db.pragma("page_size", { simple: true }) as number,
|
||||
});
|
||||
}
|
||||
|
||||
export function getAutoVacuumModeForDb(db: SqliteDatabase): AutoVacuumMode {
|
||||
const mode = db.pragma("auto_vacuum", { simple: true }) as number;
|
||||
return PRAGMA_TO_AUTO_VACUUM_MODE[mode] || "NONE";
|
||||
}
|
||||
|
||||
export function setPageSizeForDb(db: SqliteDatabase, pageSize: number): void {
|
||||
const currentPageSize = db.pragma("page_size", { simple: true }) as number;
|
||||
const targetPageSize = normalizePageSizeBytes(
|
||||
pageSize,
|
||||
DEFAULT_DATABASE_SETTINGS.optimization.pageSize
|
||||
);
|
||||
|
||||
if (currentPageSize === targetPageSize) {
|
||||
console.log(`[DB] page_size already set to ${targetPageSize}`);
|
||||
return;
|
||||
}
|
||||
|
||||
applyPersistentOptimizationPragmas(db, {
|
||||
...DEFAULT_DATABASE_SETTINGS.optimization,
|
||||
autoVacuumMode: getAutoVacuumModeForDb(db),
|
||||
pageSize: targetPageSize,
|
||||
});
|
||||
}
|
||||
@@ -400,7 +400,7 @@ export const databaseSettingsSchema = z
|
||||
.or(z.literal("monthly")),
|
||||
vacuumHour: z.number().int().min(0).max(23),
|
||||
pageSize: z.number().multipleOf(512).min(512).max(65536),
|
||||
cacheSize: z.number().int().min(-1000000).max(1000000),
|
||||
cacheSize: z.number().int().positive().max(1000000),
|
||||
optimizeOnStartup: z.boolean(),
|
||||
}),
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ export const DEFAULT_DATABASE_SETTINGS: Omit<DatabaseSettings, "location" | "sta
|
||||
scheduledVacuum: "weekly",
|
||||
vacuumHour: 2,
|
||||
pageSize: 4096,
|
||||
cacheSize: -2000,
|
||||
cacheSize: 16384,
|
||||
optimizeOnStartup: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -121,7 +121,7 @@ test("INTENTIONALLY_INTERNAL is exported from check-db-rules.mjs", () => {
|
||||
assert.ok(INTENTIONALLY_INTERNAL.size > 0, "INTENTIONALLY_INTERNAL must not be empty");
|
||||
});
|
||||
|
||||
test("INTENTIONALLY_INTERNAL contains the expected 29 audited modules", () => {
|
||||
test("INTENTIONALLY_INTERNAL contains the expected 30 audited modules", () => {
|
||||
const expected = [
|
||||
"_rowTypes",
|
||||
"accessTokens",
|
||||
@@ -141,6 +141,7 @@ test("INTENTIONALLY_INTERNAL contains the expected 29 audited modules", () => {
|
||||
"migrationRunner",
|
||||
"notion",
|
||||
"obsidian",
|
||||
"optimizationSettings",
|
||||
"pluginMetrics",
|
||||
"prompts",
|
||||
"providerNodeSelect",
|
||||
|
||||
@@ -146,6 +146,81 @@ test("database log settings mirror the runtime pipeline toggle", async () => {
|
||||
assert.equal(databaseSettings.getUserDatabaseSettings().logs.callLogPipelineEnabled, true);
|
||||
});
|
||||
|
||||
test("database optimization settings apply SQLite cache size immediately", () => {
|
||||
const current = databaseSettings.getUserDatabaseSettings();
|
||||
|
||||
databaseSettings.updateDatabaseSettings({
|
||||
optimization: {
|
||||
...current.optimization,
|
||||
autoVacuumMode: core.getAutoVacuumMode(),
|
||||
pageSize: 4096,
|
||||
cacheSize: 16384,
|
||||
},
|
||||
});
|
||||
|
||||
const db = core.getDbInstance();
|
||||
const stored = db
|
||||
.prepare(
|
||||
"SELECT value FROM key_value WHERE namespace = 'databaseSettings' AND key = 'optimization.cacheSize'"
|
||||
)
|
||||
.get() as { value: string } | undefined;
|
||||
|
||||
assert.equal(db.pragma("cache_size", { simple: true }), -16384);
|
||||
assert.equal(JSON.parse(stored?.value ?? "null"), 16384);
|
||||
assert.equal(databaseSettings.getUserDatabaseSettings().optimization.cacheSize, 16384);
|
||||
});
|
||||
|
||||
test("database optimization settings apply SQLite page size immediately", () => {
|
||||
const current = databaseSettings.getUserDatabaseSettings();
|
||||
|
||||
databaseSettings.updateDatabaseSettings({
|
||||
optimization: {
|
||||
...current.optimization,
|
||||
autoVacuumMode: core.getAutoVacuumMode(),
|
||||
pageSize: 8192,
|
||||
cacheSize: 16384,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(core.getDbInstance().pragma("page_size", { simple: true }), 8192);
|
||||
assert.equal(databaseSettings.getUserDatabaseSettings().optimization.pageSize, 8192);
|
||||
});
|
||||
|
||||
test("database optimization cache size is applied when the DB is reopened", () => {
|
||||
const db = core.getDbInstance();
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('databaseSettings', ?, ?)"
|
||||
).run("optimization.cacheSize", JSON.stringify(32768));
|
||||
|
||||
core.resetDbInstance();
|
||||
const reopened = core.getDbInstance();
|
||||
|
||||
assert.equal(reopened.pragma("cache_size", { simple: true }), -32768);
|
||||
});
|
||||
|
||||
test("database optimization rejects negative cache size through the API", async () => {
|
||||
const current = databaseSettings.getUserDatabaseSettings();
|
||||
const response = await databaseSettingsRoute.PATCH(
|
||||
makeJsonRequest("PATCH", {
|
||||
optimization: {
|
||||
...current.optimization,
|
||||
cacheSize: -2000,
|
||||
},
|
||||
}) as never
|
||||
);
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
});
|
||||
|
||||
test("database settings reader normalizes legacy negative cache size to the positive default", () => {
|
||||
const db = core.getDbInstance();
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('databaseSettings', ?, ?)"
|
||||
).run("optimization.cacheSize", JSON.stringify(-2000));
|
||||
|
||||
assert.equal(databaseSettings.getUserDatabaseSettings().optimization.cacheSize, 16384);
|
||||
});
|
||||
|
||||
test("purgeDetailedLogs deletes request_detail_logs", async () => {
|
||||
const db = core.getDbInstance();
|
||||
db.prepare("INSERT INTO request_detail_logs (id, timestamp, duration_ms) VALUES (?, ?, ?)").run(
|
||||
|
||||
Reference in New Issue
Block a user