client.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import Database from 'better-sqlite3';
  2. import * as fs from 'fs';
  3. import * as path from 'path';
  4. import * as crypto from 'crypto';
  5. export class DatabaseClient {
  6. private static instance: DatabaseClient;
  7. private db: Database.Database;
  8. private dbPath: string;
  9. private constructor() {
  10. // Get database path from environment or use default
  11. this.dbPath = process.env.DIFF_EVALS_DB_PATH || path.join(__dirname, '../evals.db');
  12. // Ensure directory exists
  13. const dbDir = path.dirname(this.dbPath);
  14. if (!fs.existsSync(dbDir)) {
  15. fs.mkdirSync(dbDir, { recursive: true });
  16. }
  17. // Initialize database connection
  18. this.db = new Database(this.dbPath);
  19. // Enable WAL mode for concurrent access
  20. this.db.pragma('journal_mode = WAL');
  21. // Enable foreign key constraints
  22. this.db.pragma('foreign_keys = ON');
  23. // Initialize schema if needed
  24. this.initializeSchema();
  25. }
  26. static getInstance(): DatabaseClient {
  27. if (!DatabaseClient.instance) {
  28. DatabaseClient.instance = new DatabaseClient();
  29. }
  30. return DatabaseClient.instance;
  31. }
  32. private initializeSchema(): void {
  33. // Check if tables exist by trying to query one of them
  34. try {
  35. this.db.prepare('SELECT COUNT(*) FROM system_prompts LIMIT 1').get();
  36. // If we get here, tables exist
  37. return;
  38. } catch (error) {
  39. // Tables don't exist, create them
  40. console.log('Initializing database schema...');
  41. this.createTables();
  42. }
  43. }
  44. private createTables(): void {
  45. const schemaPath = path.join(__dirname, 'schema.sql');
  46. const schema = fs.readFileSync(schemaPath, 'utf8');
  47. // Execute the entire schema as one block
  48. this.db.transaction(() => {
  49. this.db.exec(schema);
  50. })();
  51. console.log('Database schema initialized successfully');
  52. }
  53. getDatabase(): Database.Database {
  54. return this.db;
  55. }
  56. getDatabasePath(): string {
  57. return this.dbPath;
  58. }
  59. // Utility method to generate SHA-256 hash
  60. static generateHash(content: string): string {
  61. return crypto.createHash('sha256').update(content).digest('hex');
  62. }
  63. // Utility method to generate UUID-like ID
  64. static generateId(): string {
  65. return crypto.randomUUID();
  66. }
  67. // Transaction wrapper
  68. transaction<T>(fn: () => T): T {
  69. return this.db.transaction(fn)();
  70. }
  71. // Close database connection (for cleanup)
  72. close(): void {
  73. if (this.db) {
  74. this.db.close();
  75. }
  76. }
  77. // Get database info
  78. getInfo(): { path: string; size: number; tables: string[] } {
  79. const stats = fs.statSync(this.dbPath);
  80. const tables = this.db
  81. .prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
  82. .all()
  83. .map((row: any) => row.name);
  84. return {
  85. path: this.dbPath,
  86. size: stats.size,
  87. tables
  88. };
  89. }
  90. // Vacuum database (cleanup and optimize)
  91. vacuum(): void {
  92. this.db.exec('VACUUM');
  93. }
  94. // Get database statistics
  95. getStats(): { [tableName: string]: number } {
  96. const tables = ['system_prompts', 'processing_functions', 'files', 'runs', 'cases', 'results'];
  97. const stats: { [tableName: string]: number } = {};
  98. for (const table of tables) {
  99. try {
  100. const result = this.db.prepare(`SELECT COUNT(*) as count FROM ${table}`).get() as { count: number };
  101. stats[table] = result.count;
  102. } catch (error) {
  103. stats[table] = 0;
  104. }
  105. }
  106. return stats;
  107. }
  108. }
  109. // Export singleton instance getter
  110. export const getDatabase = () => DatabaseClient.getInstance();