db.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. /**
  2. * db.ts - Cross-runtime SQLite compatibility layer
  3. *
  4. * Provides a unified Database export that works under both Bun (bun:sqlite)
  5. * and Node.js (better-sqlite3). The APIs are nearly identical — the main
  6. * difference is the import path.
  7. *
  8. * On macOS, Apple's system SQLite is compiled with SQLITE_OMIT_LOAD_EXTENSION,
  9. * which prevents loading native extensions like sqlite-vec. When running under
  10. * Bun we call Database.setCustomSQLite() to swap in Homebrew's full-featured
  11. * SQLite build before creating any database instances.
  12. */
  13. export const isBun = typeof globalThis.Bun !== "undefined";
  14. let _Database: any;
  15. let _sqliteVecLoad: ((db: any) => void) | null;
  16. if (isBun) {
  17. // Dynamic string prevents tsc from resolving bun:sqlite on Node.js builds
  18. const bunSqlite = "bun:" + "sqlite";
  19. const BunDatabase = (await import(/* @vite-ignore */ bunSqlite)).Database;
  20. // See: https://bun.com/docs/runtime/sqlite#setcustomsqlite
  21. if (process.platform === "darwin") {
  22. const homebrewPaths = [
  23. "/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib", // Apple Silicon
  24. "/usr/local/opt/sqlite/lib/libsqlite3.dylib", // Intel
  25. ];
  26. for (const p of homebrewPaths) {
  27. try {
  28. BunDatabase.setCustomSQLite(p);
  29. break;
  30. } catch {}
  31. }
  32. }
  33. _Database = BunDatabase;
  34. // setCustomSQLite may have silently failed — test that extensions actually work.
  35. try {
  36. const { getLoadablePath } = await import("sqlite-vec");
  37. const vecPath = getLoadablePath();
  38. const testDb = new BunDatabase(":memory:");
  39. testDb.loadExtension(vecPath);
  40. testDb.close();
  41. _sqliteVecLoad = (db: any) => db.loadExtension(vecPath);
  42. } catch {
  43. // Vector search won't work, but BM25 and other operations are unaffected.
  44. _sqliteVecLoad = null;
  45. }
  46. } else {
  47. _Database = (await import("better-sqlite3")).default;
  48. const sqliteVec = await import("sqlite-vec");
  49. _sqliteVecLoad = (db: any) => sqliteVec.load(db);
  50. }
  51. /**
  52. * Open a SQLite database. Works with both bun:sqlite and better-sqlite3.
  53. */
  54. export function openDatabase(path: string): Database {
  55. return new _Database(path) as Database;
  56. }
  57. /**
  58. * Common subset of the Database interface used throughout QMD.
  59. */
  60. export interface Database {
  61. exec(sql: string): void;
  62. prepare(sql: string): Statement;
  63. loadExtension(path: string): void;
  64. close(): void;
  65. }
  66. export interface Statement {
  67. run(...params: any[]): { changes: number; lastInsertRowid: number | bigint };
  68. get(...params: any[]): any;
  69. all(...params: any[]): any[];
  70. }
  71. /**
  72. * Load the sqlite-vec extension into a database.
  73. *
  74. * Throws with platform-specific fix instructions when the extension is
  75. * unavailable.
  76. */
  77. export function loadSqliteVec(db: Database): void {
  78. if (!_sqliteVecLoad) {
  79. const hint = isBun && process.platform === "darwin"
  80. ? "On macOS with Bun, install Homebrew SQLite: brew install sqlite\n" +
  81. "Or install qmd with npm instead: npm install -g @tobilu/qmd"
  82. : "Ensure the sqlite-vec native module is installed correctly.";
  83. throw new Error(`sqlite-vec extension is unavailable. ${hint}`);
  84. }
  85. _sqliteVecLoad(db);
  86. }