Stabilize cross-platform local runtime
This commit is contained in:
@@ -38,6 +38,7 @@ async function checkSQLiteDatabaseHealth(): Promise<{
|
||||
const sqlite = getSQLiteClient();
|
||||
|
||||
const connected = await sqlite.testConnection();
|
||||
const vectorCapability = sqlite.getVectorCapability();
|
||||
if (!connected) {
|
||||
return {
|
||||
connected: false,
|
||||
@@ -47,7 +48,7 @@ async function checkSQLiteDatabaseHealth(): Promise<{
|
||||
};
|
||||
}
|
||||
|
||||
const vectorExtension = await sqlite.checkVectorExtension();
|
||||
const vectorExtension = vectorCapability.available;
|
||||
|
||||
// Check if main tables exist
|
||||
const tables = await sqlite.checkTables();
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { DatabaseError } from '@/types/database';
|
||||
import {
|
||||
ensureDatabaseDirectory,
|
||||
getDatabasePath,
|
||||
getVecExtensionPath,
|
||||
loadVecExtension,
|
||||
type VectorCapability,
|
||||
} from './sqlite-runtime';
|
||||
|
||||
export interface SQLiteConfig {
|
||||
dbPath: string;
|
||||
@@ -19,27 +24,26 @@ class SQLiteClient {
|
||||
private db: Database.Database;
|
||||
private config: SQLiteConfig;
|
||||
private readonly readOnly: boolean;
|
||||
private readonly vectorCapability: VectorCapability;
|
||||
|
||||
private constructor() {
|
||||
this.config = this.getSQLiteConfig();
|
||||
this.readOnly = process.env.SQLITE_READONLY === 'true';
|
||||
|
||||
// Initialize database connection
|
||||
const dbDirectory = path.dirname(this.config.dbPath);
|
||||
if (!this.readOnly && !fs.existsSync(dbDirectory)) {
|
||||
fs.mkdirSync(dbDirectory, { recursive: true });
|
||||
if (!this.readOnly) {
|
||||
ensureDatabaseDirectory(this.config.dbPath);
|
||||
}
|
||||
this.db = this.readOnly
|
||||
? new Database(this.config.dbPath, { readonly: true, fileMustExist: true })
|
||||
: new Database(this.config.dbPath);
|
||||
|
||||
// Load sqlite-vec extension
|
||||
try {
|
||||
this.db.loadExtension(this.config.vecExtensionPath);
|
||||
this.vectorCapability = loadVecExtension(this.db, this.config.vecExtensionPath);
|
||||
if (this.vectorCapability.available) {
|
||||
console.log('SQLite vector extension loaded successfully');
|
||||
} catch (error) {
|
||||
// Do not fail hard — allow the app to run without vector features
|
||||
console.error('Warning: Failed to load vector extension:', error);
|
||||
} else {
|
||||
console.warn(`Warning: ${this.vectorCapability.reason}`);
|
||||
}
|
||||
|
||||
// Configure SQLite settings
|
||||
@@ -58,8 +62,10 @@ class SQLiteClient {
|
||||
this.db.pragma('busy_timeout = 5000');
|
||||
|
||||
// Ensure vector virtual tables are present and healthy
|
||||
this.ensureVectorTables();
|
||||
this.healVectorTablesIfCorrupt();
|
||||
if (this.vectorCapability.available) {
|
||||
this.ensureVectorTables();
|
||||
this.healVectorTablesIfCorrupt();
|
||||
}
|
||||
|
||||
// Ensure logging schema (rename memory->logs if needed, create triggers/views)
|
||||
this.ensureLoggingAndMemorySchema();
|
||||
@@ -69,17 +75,9 @@ class SQLiteClient {
|
||||
}
|
||||
|
||||
private getSQLiteConfig(): SQLiteConfig {
|
||||
const dbPath = process.env.SQLITE_DB_PATH || path.join(
|
||||
process.env.HOME || '~',
|
||||
'Library/Application Support/RA-H/db/rah.sqlite'
|
||||
);
|
||||
|
||||
const vecExtensionPath = process.env.SQLITE_VEC_EXTENSION_PATH ||
|
||||
'./vendor/sqlite-extensions/vec0.dylib';
|
||||
|
||||
return {
|
||||
dbPath,
|
||||
vecExtensionPath
|
||||
dbPath: getDatabasePath(),
|
||||
vecExtensionPath: getVecExtensionPath(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,7 +132,9 @@ class SQLiteClient {
|
||||
} as DatabaseError;
|
||||
}
|
||||
// Proactively validate/repair vec vtables before any write transaction
|
||||
this.healVectorTablesIfCorrupt();
|
||||
if (this.vectorCapability.available) {
|
||||
this.healVectorTablesIfCorrupt();
|
||||
}
|
||||
const txn = this.db.transaction(callback);
|
||||
try {
|
||||
return txn();
|
||||
@@ -154,13 +154,11 @@ class SQLiteClient {
|
||||
}
|
||||
|
||||
public async checkVectorExtension(): Promise<boolean> {
|
||||
try {
|
||||
const result = this.query('SELECT vec_version() as version');
|
||||
return result.rows.length > 0;
|
||||
} catch (error) {
|
||||
console.error('Vector extension check failed:', error);
|
||||
return false;
|
||||
}
|
||||
return this.vectorCapability.available;
|
||||
}
|
||||
|
||||
public getVectorCapability(): VectorCapability {
|
||||
return this.vectorCapability;
|
||||
}
|
||||
|
||||
public async checkTables(): Promise<string[]> {
|
||||
@@ -176,6 +174,10 @@ class SQLiteClient {
|
||||
}
|
||||
|
||||
public ensureVectorExtensions(): void {
|
||||
if (!this.vectorCapability.available) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Test for vec_nodes and vec_chunks; create them if missing
|
||||
const hasVecNodes = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get('vec_nodes');
|
||||
@@ -205,7 +207,7 @@ class SQLiteClient {
|
||||
}
|
||||
|
||||
private ensureVectorTables(): void {
|
||||
if (this.readOnly) {
|
||||
if (this.readOnly || !this.vectorCapability.available) {
|
||||
return;
|
||||
}
|
||||
// Wrapper to keep existing public API stable
|
||||
@@ -739,7 +741,7 @@ class SQLiteClient {
|
||||
}
|
||||
|
||||
private healVectorTablesIfCorrupt(): void {
|
||||
if (this.readOnly) {
|
||||
if (this.readOnly || !this.vectorCapability.available) {
|
||||
return;
|
||||
}
|
||||
// Attempt lightweight reads to detect CORRUPT_VTAB; if detected, drop/recreate vtables
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
export type VectorCapability =
|
||||
| {
|
||||
available: true;
|
||||
backend: 'sqlite-vec';
|
||||
extensionPath: string;
|
||||
detail: string;
|
||||
}
|
||||
| {
|
||||
available: false;
|
||||
backend: 'none';
|
||||
extensionPath: string;
|
||||
reason: string;
|
||||
};
|
||||
|
||||
const VECTOR_CAPABILITY_KEY = '__rahVectorCapability';
|
||||
|
||||
function getHomeDirectory(): string {
|
||||
return os.homedir() || process.env.HOME || '~';
|
||||
}
|
||||
|
||||
export function getDefaultDbPath(): string {
|
||||
const homeDir = getHomeDirectory();
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
||||
return path.join(appData, 'RA-H', 'db', 'rah.sqlite');
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
return path.join(homeDir, 'Library', 'Application Support', 'RA-H', 'db', 'rah.sqlite');
|
||||
}
|
||||
|
||||
return path.join(
|
||||
process.env.XDG_DATA_HOME || path.join(homeDir, '.local', 'share'),
|
||||
'RA-H',
|
||||
'db',
|
||||
'rah.sqlite'
|
||||
);
|
||||
}
|
||||
|
||||
export function getDatabasePath(): string {
|
||||
return process.env.SQLITE_DB_PATH || getDefaultDbPath();
|
||||
}
|
||||
|
||||
export function getDefaultVecExtensionPath(): string {
|
||||
const extension = process.platform === 'darwin'
|
||||
? 'dylib'
|
||||
: process.platform === 'win32'
|
||||
? 'dll'
|
||||
: 'so';
|
||||
|
||||
return path.join(process.cwd(), 'vendor', 'sqlite-extensions', `vec0.${extension}`);
|
||||
}
|
||||
|
||||
export function getVecExtensionPath(): string {
|
||||
return process.env.SQLITE_VEC_EXTENSION_PATH || getDefaultVecExtensionPath();
|
||||
}
|
||||
|
||||
export function ensureDatabaseDirectory(dbPath: string): void {
|
||||
const dbDirectory = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dbDirectory)) {
|
||||
fs.mkdirSync(dbDirectory, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function buildVectorFailureReason(error: unknown, extensionPath: string): string {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return `sqlite-vec failed to load from ${extensionPath}. On Windows, install vec0.dll or switch to Qdrant. Original error: ${message}`;
|
||||
}
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
return `sqlite-vec failed to load from ${extensionPath}. Alpine/musl builds are unsupported; use Qdrant there. Original error: ${message}`;
|
||||
}
|
||||
|
||||
return `sqlite-vec failed to load from ${extensionPath}. Original error: ${message}`;
|
||||
}
|
||||
|
||||
export function loadVecExtension(db: Database.Database, extensionPath = getVecExtensionPath()): VectorCapability {
|
||||
try {
|
||||
db.loadExtension(extensionPath);
|
||||
const capability: VectorCapability = {
|
||||
available: true,
|
||||
backend: 'sqlite-vec',
|
||||
extensionPath,
|
||||
detail: 'sqlite-vec extension loaded successfully',
|
||||
};
|
||||
(db as Database.Database & Record<string, unknown>)[VECTOR_CAPABILITY_KEY] = capability;
|
||||
return capability;
|
||||
} catch (error) {
|
||||
const capability: VectorCapability = {
|
||||
available: false,
|
||||
backend: 'none',
|
||||
extensionPath,
|
||||
reason: buildVectorFailureReason(error, extensionPath),
|
||||
};
|
||||
(db as Database.Database & Record<string, unknown>)[VECTOR_CAPABILITY_KEY] = capability;
|
||||
return capability;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDbVectorCapability(db: Database.Database): VectorCapability {
|
||||
const capability = (db as Database.Database & Record<string, unknown>)[VECTOR_CAPABILITY_KEY];
|
||||
if (capability && typeof capability === 'object' && 'available' in capability) {
|
||||
return capability as VectorCapability;
|
||||
}
|
||||
|
||||
return {
|
||||
available: false,
|
||||
backend: 'none',
|
||||
extensionPath: getVecExtensionPath(),
|
||||
reason: 'sqlite-vec capability was not initialized for this database connection',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user