@pineliner/odb-client 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,7 @@
1
+ import * as __WEBPACK_EXTERNAL_MODULE_bun_sqlite__ from "bun:sqlite";
2
+ import * as __WEBPACK_EXTERNAL_MODULE__libsql_client__ from "@libsql/client";
3
+ import * as __WEBPACK_EXTERNAL_MODULE_node_sql_parser__ from "node-sql-parser";
4
+ import * as __WEBPACK_EXTERNAL_MODULE_node_fs__ from "node:fs";
1
5
  // Core types for the ODBLite client
2
6
  // Error types
3
7
  class ODBLiteError extends Error {
@@ -858,19 +862,20 @@ ODBLiteClient.join;
858
862
  * Create a new database
859
863
  *
860
864
  * @param name - Database name (should be unique)
861
- * @param nodeId - ID of the node to host the database
865
+ * @param nodeId - ID of the node to host the database (optional - server will select if null)
862
866
  * @returns Created database object with hash
863
867
  */ async createDatabase(name, nodeId) {
868
+ const body = {
869
+ name
870
+ };
871
+ if (nodeId) body.nodeId = nodeId;
864
872
  const response = await fetch(`${this.apiUrl}/api/tenant/databases`, {
865
873
  method: 'POST',
866
874
  headers: {
867
875
  Authorization: `Bearer ${this.apiKey}`,
868
876
  'Content-Type': 'application/json'
869
877
  },
870
- body: JSON.stringify({
871
- name,
872
- nodeId
873
- })
878
+ body: JSON.stringify(body)
874
879
  });
875
880
  const result = await response.json();
876
881
  if (!result.success) throw new Error(result.error || 'Failed to create database');
@@ -998,10 +1003,734 @@ ODBLiteClient.join;
998
1003
  this.databaseCache.set(cacheKey, hash);
999
1004
  }
1000
1005
  }
1006
+ /**
1007
+ * Bun SQLite adapter for DatabaseManager
1008
+ * Wraps bun:sqlite with Connection interface
1009
+ */ class BunSQLiteAdapter {
1010
+ type = 'bun-sqlite';
1011
+ config;
1012
+ constructor(config){
1013
+ this.config = config;
1014
+ }
1015
+ async connect(config) {
1016
+ const db = new __WEBPACK_EXTERNAL_MODULE_bun_sqlite__.Database(config.databasePath, {
1017
+ readonly: config.readonly,
1018
+ create: config.create ?? true
1019
+ });
1020
+ return new BunSQLiteConnection(db);
1021
+ }
1022
+ async disconnect(tenantId) {
1023
+ // bun:sqlite doesn't require explicit disconnection
1024
+ // File handles are cleaned up by GC
1025
+ }
1026
+ async isHealthy() {
1027
+ try {
1028
+ const db = new __WEBPACK_EXTERNAL_MODULE_bun_sqlite__.Database(this.config.databasePath, {
1029
+ readonly: true,
1030
+ create: false
1031
+ });
1032
+ db.query('SELECT 1').get();
1033
+ db.close();
1034
+ return true;
1035
+ } catch {
1036
+ return false;
1037
+ }
1038
+ }
1039
+ }
1040
+ /**
1041
+ * Connection implementation for Bun SQLite
1042
+ */ class BunSQLiteConnection {
1043
+ db;
1044
+ inTransaction = false;
1045
+ constructor(db){
1046
+ this.db = db;
1047
+ }
1048
+ /**
1049
+ * Template tag query (postgres.js-like)
1050
+ */ async query(strings, ...values) {
1051
+ // Build SQL from template
1052
+ const sql = strings.reduce((acc, str, i)=>acc + str + (i < values.length ? '?' : ''), '');
1053
+ return this.execute(sql, values);
1054
+ }
1055
+ /**
1056
+ * Execute SQL with parameters
1057
+ */ async execute(sql, params = []) {
1058
+ try {
1059
+ const stmt = this.db.query(sql);
1060
+ const isSelect = sql.trim().toUpperCase().startsWith('SELECT');
1061
+ if (isSelect) {
1062
+ const rows = stmt.all(...params);
1063
+ return {
1064
+ rows: rows,
1065
+ rowsAffected: 0
1066
+ };
1067
+ }
1068
+ {
1069
+ const result = stmt.run(...params);
1070
+ return {
1071
+ rows: [],
1072
+ rowsAffected: result.changes || 0,
1073
+ lastInsertRowid: result.lastInsertRowid
1074
+ };
1075
+ }
1076
+ } catch (error) {
1077
+ throw new Error(`SQL execution failed: ${error.message}`);
1078
+ }
1079
+ }
1080
+ /**
1081
+ * Prepare statement for repeated execution
1082
+ */ prepare(sql) {
1083
+ const stmt = this.db.query(sql);
1084
+ return {
1085
+ execute: async (params = [])=>{
1086
+ const isSelect = sql.trim().toUpperCase().startsWith('SELECT');
1087
+ if (isSelect) {
1088
+ const rows = stmt.all(...params);
1089
+ return {
1090
+ rows: rows,
1091
+ rowsAffected: 0
1092
+ };
1093
+ }
1094
+ {
1095
+ const result = stmt.run(...params);
1096
+ return {
1097
+ rows: [],
1098
+ rowsAffected: result.changes || 0,
1099
+ lastInsertRowid: result.lastInsertRowid
1100
+ };
1101
+ }
1102
+ },
1103
+ all: async (params = [])=>stmt.all(...params),
1104
+ get: async (params = [])=>stmt.get(...params)
1105
+ };
1106
+ }
1107
+ /**
1108
+ * Execute function in transaction
1109
+ */ async transaction(fn) {
1110
+ if (this.inTransaction) // Nested transaction - just execute the function
1111
+ return fn(this);
1112
+ this.inTransaction = true;
1113
+ try {
1114
+ await this.execute('BEGIN');
1115
+ const result = await fn(this);
1116
+ await this.execute('COMMIT');
1117
+ return result;
1118
+ } catch (error) {
1119
+ await this.execute('ROLLBACK');
1120
+ throw error;
1121
+ } finally{
1122
+ this.inTransaction = false;
1123
+ }
1124
+ }
1125
+ /**
1126
+ * Close connection
1127
+ */ async close() {
1128
+ this.db.close();
1129
+ }
1130
+ }
1131
+ /**
1132
+ * LibSQL adapter for DatabaseManager
1133
+ * Wraps @libsql/client with Connection interface
1134
+ */ class LibSQLAdapter {
1135
+ type = 'libsql';
1136
+ config;
1137
+ constructor(config){
1138
+ this.config = config;
1139
+ }
1140
+ async connect(config) {
1141
+ const client = (0, __WEBPACK_EXTERNAL_MODULE__libsql_client__.createClient)({
1142
+ url: config.url,
1143
+ authToken: config.authToken,
1144
+ encryptionKey: config.encryptionKey
1145
+ });
1146
+ return new LibSQLConnection(client);
1147
+ }
1148
+ async disconnect(tenantId) {
1149
+ // LibSQL clients don't require explicit disconnection
1150
+ // Connections are pooled internally
1151
+ }
1152
+ async isHealthy() {
1153
+ try {
1154
+ const client = (0, __WEBPACK_EXTERNAL_MODULE__libsql_client__.createClient)({
1155
+ url: this.config.url,
1156
+ authToken: this.config.authToken
1157
+ });
1158
+ await client.execute('SELECT 1');
1159
+ return true;
1160
+ } catch {
1161
+ return false;
1162
+ }
1163
+ }
1164
+ }
1165
+ /**
1166
+ * Connection implementation for LibSQL
1167
+ */ class LibSQLConnection {
1168
+ client;
1169
+ txClient;
1170
+ constructor(client){
1171
+ this.client = client;
1172
+ }
1173
+ /**
1174
+ * Template tag query (postgres.js-like)
1175
+ */ async query(strings, ...values) {
1176
+ // Build SQL from template
1177
+ const sql = strings.reduce((acc, str, i)=>acc + str + (i < values.length ? '?' : ''), '');
1178
+ return this.execute(sql, values);
1179
+ }
1180
+ /**
1181
+ * Execute SQL with parameters
1182
+ * Supports both formats: execute(sql, params) and execute({sql, args})
1183
+ */ async execute(sql, params = []) {
1184
+ try {
1185
+ const target = this.txClient || this.client;
1186
+ // Support both execute(sql, params) and execute({sql, args}) formats
1187
+ let query;
1188
+ query = 'string' == typeof sql ? {
1189
+ sql,
1190
+ args: params
1191
+ } : {
1192
+ sql: sql.sql,
1193
+ args: sql.args || []
1194
+ };
1195
+ const result = await target.execute(query);
1196
+ return {
1197
+ rows: result.rows,
1198
+ rowsAffected: Number(result.rowsAffected),
1199
+ lastInsertRowid: result.lastInsertRowid ? BigInt(result.lastInsertRowid.toString()) : void 0
1200
+ };
1201
+ } catch (error) {
1202
+ throw new Error(`SQL execution failed: ${error.message}`);
1203
+ }
1204
+ }
1205
+ /**
1206
+ * Prepare statement for repeated execution
1207
+ */ prepare(sql) {
1208
+ return {
1209
+ execute: async (params = [])=>this.execute(sql, params),
1210
+ all: async (params = [])=>{
1211
+ const result = await this.execute(sql, params);
1212
+ return result.rows;
1213
+ },
1214
+ get: async (params = [])=>{
1215
+ const result = await this.execute(sql, params);
1216
+ return result.rows[0] || null;
1217
+ }
1218
+ };
1219
+ }
1220
+ /**
1221
+ * Execute function in transaction
1222
+ */ async transaction(fn) {
1223
+ if (this.txClient) // Nested transaction - just execute the function
1224
+ return fn(this);
1225
+ // Use manual BEGIN/COMMIT/ROLLBACK for simplicity
1226
+ try {
1227
+ await this.execute('BEGIN');
1228
+ const result = await fn(this);
1229
+ await this.execute('COMMIT');
1230
+ return result;
1231
+ } catch (error) {
1232
+ await this.execute('ROLLBACK');
1233
+ throw error;
1234
+ }
1235
+ }
1236
+ /**
1237
+ * Close connection
1238
+ */ async close() {
1239
+ // LibSQL client.close() if available in future versions
1240
+ // For now, connections are managed by the client pool
1241
+ }
1242
+ }
1243
+ /**
1244
+ * ODB-Lite adapter for DatabaseManager
1245
+ * Wraps ServiceClient and ODBLiteClient with Connection interface
1246
+ */ class ODBLiteAdapter {
1247
+ type = 'odblite';
1248
+ config;
1249
+ serviceClient;
1250
+ constructor(config){
1251
+ this.config = config;
1252
+ this.serviceClient = new ServiceClient({
1253
+ baseUrl: config.serviceUrl,
1254
+ apiKey: config.apiKey
1255
+ });
1256
+ }
1257
+ async connect(config) {
1258
+ const databaseName = config.databaseName || 'default';
1259
+ // Ensure database exists
1260
+ const dbInfo = await this.serviceClient.getDatabaseByName(databaseName);
1261
+ if (!dbInfo) // Create database - pass nodeId from config if provided, otherwise server selects
1262
+ await this.serviceClient.createDatabase(databaseName, this.config.nodeId);
1263
+ // Get fresh database info
1264
+ const db = await this.serviceClient.getDatabaseByName(databaseName);
1265
+ if (!db) throw new Error(`Database ${databaseName} not found after creation`);
1266
+ // Create ODBLiteClient for this database
1267
+ const client = new ODBLiteClient({
1268
+ baseUrl: this.config.serviceUrl,
1269
+ apiKey: this.config.apiKey,
1270
+ databaseId: db.hash
1271
+ });
1272
+ return new ODBLiteConnection(client, this.serviceClient, databaseName);
1273
+ }
1274
+ async disconnect(tenantId) {
1275
+ if (tenantId) {
1276
+ const prefix = 'pipeline_tenant_' // TODO: make configurable
1277
+ ;
1278
+ const databaseName = `${prefix}${tenantId}`;
1279
+ try {
1280
+ await this.serviceClient.deleteDatabase(databaseName);
1281
+ } catch (error) {
1282
+ console.warn(`Failed to delete database ${databaseName}:`, error.message);
1283
+ }
1284
+ }
1285
+ }
1286
+ async isHealthy() {
1287
+ try {
1288
+ await this.serviceClient.listDatabases();
1289
+ return true;
1290
+ } catch {
1291
+ return false;
1292
+ }
1293
+ }
1294
+ }
1295
+ /**
1296
+ * Connection implementation for ODB-Lite
1297
+ */ class ODBLiteConnection {
1298
+ client;
1299
+ serviceClient;
1300
+ databaseName;
1301
+ inTransaction = false;
1302
+ constructor(client, serviceClient, databaseName){
1303
+ this.client = client;
1304
+ this.serviceClient = serviceClient;
1305
+ this.databaseName = databaseName;
1306
+ }
1307
+ /**
1308
+ * Template tag query (postgres.js-like)
1309
+ */ async query(strings, ...values) {
1310
+ // ODBLiteClient.sql is a function that returns a Promise
1311
+ const result = await this.client.sql(strings, ...values);
1312
+ return {
1313
+ rows: result.rows,
1314
+ rowsAffected: result.rowsAffected || 0
1315
+ };
1316
+ }
1317
+ /**
1318
+ * Execute SQL with parameters
1319
+ */ async execute(sql, params = []) {
1320
+ try {
1321
+ const result = await this.client.sql.execute(sql, params);
1322
+ return {
1323
+ rows: result.rows,
1324
+ rowsAffected: result.rowsAffected || 0
1325
+ };
1326
+ } catch (error) {
1327
+ throw new Error(`SQL execution failed: ${error.message}`);
1328
+ }
1329
+ }
1330
+ /**
1331
+ * Prepare statement for repeated execution
1332
+ */ prepare(sql) {
1333
+ return {
1334
+ execute: async (params = [])=>this.execute(sql, params),
1335
+ all: async (params = [])=>{
1336
+ const result = await this.execute(sql, params);
1337
+ return result.rows;
1338
+ },
1339
+ get: async (params = [])=>{
1340
+ const result = await this.execute(sql, params);
1341
+ return result.rows[0] || null;
1342
+ }
1343
+ };
1344
+ }
1345
+ /**
1346
+ * Execute function in transaction
1347
+ */ async transaction(fn) {
1348
+ if (this.inTransaction) // Nested transaction - just execute the function
1349
+ return fn(this);
1350
+ this.inTransaction = true;
1351
+ try {
1352
+ await this.execute('BEGIN');
1353
+ const result = await fn(this);
1354
+ await this.execute('COMMIT');
1355
+ return result;
1356
+ } catch (error) {
1357
+ await this.execute('ROLLBACK');
1358
+ throw error;
1359
+ } finally{
1360
+ this.inTransaction = false;
1361
+ }
1362
+ }
1363
+ /**
1364
+ * Close connection
1365
+ */ async close() {
1366
+ // ODBLiteClient connections are stateless HTTP requests
1367
+ // No explicit close needed
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Parse SQL file into statements, separating PRAGMA from regular SQL
1372
+ * Uses node-sql-parser for robust SQL parsing
1373
+ */ function parseSQL(sqlContent, options = {}) {
1374
+ const separatePragma = options.separatePragma ?? true;
1375
+ // Use node-sql-parser to properly split SQL statements
1376
+ const statements = splitStatementsWithParser(sqlContent);
1377
+ if (!separatePragma) return {
1378
+ pragmaStatements: [],
1379
+ regularStatements: statements
1380
+ };
1381
+ const pragmaStatements = [];
1382
+ const regularStatements = [];
1383
+ for (const statement of statements){
1384
+ const trimmed = statement.trim().toUpperCase();
1385
+ if (trimmed.startsWith('PRAGMA')) pragmaStatements.push(statement);
1386
+ else regularStatements.push(statement);
1387
+ }
1388
+ return {
1389
+ pragmaStatements,
1390
+ regularStatements
1391
+ };
1392
+ }
1393
+ /**
1394
+ * Split SQL content using node-sql-parser (more robust than custom parser)
1395
+ * Falls back to simple split if parser fails
1396
+ */ function splitStatementsWithParser(sqlContent) {
1397
+ try {
1398
+ const parser = new __WEBPACK_EXTERNAL_MODULE_node_sql_parser__.Parser();
1399
+ // node-sql-parser supports splitting multiple statements
1400
+ // Try to use it, but have fallback for edge cases
1401
+ const opt = {
1402
+ database: 'sqlite',
1403
+ type: 'table'
1404
+ };
1405
+ // Split by semicolons first to handle parser limitations with very large files
1406
+ const roughStatements = sqlContent.split(/;[\s\n]*/).map((s)=>s.trim()).filter((s)=>s.length > 0);
1407
+ const validStatements = [];
1408
+ for (const stmt of roughStatements){
1409
+ // Add semicolon back
1410
+ const fullStmt = stmt + ';';
1411
+ // Skip comments-only statements
1412
+ if (!stmt.trim().startsWith('--') && '' !== stmt.trim()) // Validate with parser (but don't fail if it can't parse complex statements)
1413
+ try {
1414
+ parser.astify(fullStmt, opt);
1415
+ validStatements.push(fullStmt);
1416
+ } catch (parseError) {
1417
+ // If parser can't handle it, include it anyway if it looks like valid SQL
1418
+ // This handles complex statements like CREATE TRIGGER, CREATE VIEW with subqueries
1419
+ if (stmt.match(/^(CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|SELECT|PRAGMA)/i)) validStatements.push(fullStmt);
1420
+ else console.warn(`Skipping potentially invalid statement: ${stmt.substring(0, 50)}...`);
1421
+ }
1422
+ }
1423
+ return validStatements;
1424
+ } catch (error) {
1425
+ console.warn('node-sql-parser failed, using fallback parser:', error);
1426
+ // Fallback to original simple parser
1427
+ return splitStatements(sqlContent);
1428
+ }
1429
+ }
1430
+ /**
1431
+ * Split SQL content into individual statements
1432
+ * Handles comments, quotes, and semicolons properly
1433
+ */ function splitStatements(sqlContent) {
1434
+ const statements = [];
1435
+ let currentStatement = '';
1436
+ let inQuote = false;
1437
+ let quoteChar = '';
1438
+ const lines = sqlContent.split('\n');
1439
+ for (const line of lines){
1440
+ let processedLine = '';
1441
+ for(let i = 0; i < line.length; i++){
1442
+ const char = line[i];
1443
+ const prevChar = i > 0 ? line[i - 1] : '';
1444
+ const nextChar = i < line.length - 1 ? line[i + 1] : '';
1445
+ // Handle single-line comments
1446
+ if (!inQuote && '-' === char && '-' === nextChar) break; // Skip rest of line
1447
+ // Track quotes
1448
+ if (("'" === char || '"' === char) && '\\' !== prevChar) {
1449
+ if (inQuote) {
1450
+ if (char === quoteChar) {
1451
+ inQuote = false;
1452
+ quoteChar = '';
1453
+ }
1454
+ } else {
1455
+ inQuote = true;
1456
+ quoteChar = char;
1457
+ }
1458
+ }
1459
+ processedLine += char;
1460
+ // Split on semicolon when not in quotes
1461
+ if (';' === char && !inQuote) {
1462
+ currentStatement += processedLine;
1463
+ const stmt = currentStatement.trim();
1464
+ if (stmt) statements.push(stmt);
1465
+ currentStatement = '';
1466
+ processedLine = '';
1467
+ }
1468
+ }
1469
+ if (processedLine.trim()) currentStatement += `${processedLine}\n`;
1470
+ }
1471
+ // Handle remaining statement
1472
+ const finalStmt = currentStatement.trim();
1473
+ if (finalStmt) statements.push(finalStmt);
1474
+ return statements;
1475
+ }
1476
+ /**
1477
+ * Simple split for systems that don't need PRAGMA separation
1478
+ */ function splitSQLStatements(sqlContent) {
1479
+ const { pragmaStatements, regularStatements } = parseSQL(sqlContent);
1480
+ return [
1481
+ ...pragmaStatements,
1482
+ ...regularStatements
1483
+ ];
1484
+ }
1485
+ /**
1486
+ * Database factory for creating and managing multiple databases
1487
+ *
1488
+ * Features:
1489
+ * - Creates multiple databases from single manager instance
1490
+ * - Supports libsql, bun:sqlite, and ODB-Lite backends
1491
+ * - Connection pooling with LRU eviction
1492
+ * - Automatic schema migrations
1493
+ *
1494
+ * @example
1495
+ * ```ts
1496
+ * const dbManager = new DatabaseManager({
1497
+ * backend: 'libsql',
1498
+ * databasePath: './data'
1499
+ * })
1500
+ *
1501
+ * // Create databases
1502
+ * await dbManager.createDatabase('identity', { schemaContent })
1503
+ * await dbManager.createDatabase('analytics', { schemaContent })
1504
+ *
1505
+ * // Get connections
1506
+ * const identityDB = await dbManager.getConnection('identity')
1507
+ * const analyticsDB = await dbManager.getConnection('analytics')
1508
+ * ```
1509
+ */ class DatabaseManager {
1510
+ config;
1511
+ adapter;
1512
+ connections = new Map();
1513
+ // Connection pool management (LRU)
1514
+ connectionTimestamps = new Map();
1515
+ maxConnections;
1516
+ constructor(config){
1517
+ this.config = config;
1518
+ this.maxConnections = config.connectionPoolSize || 10;
1519
+ // Create appropriate adapter based on backend
1520
+ switch(config.backend){
1521
+ case 'bun-sqlite':
1522
+ this.adapter = new BunSQLiteAdapter({
1523
+ databasePath: config.databasePath,
1524
+ create: true
1525
+ });
1526
+ break;
1527
+ case 'libsql':
1528
+ this.adapter = new LibSQLAdapter({
1529
+ url: `file:${config.databasePath}`,
1530
+ ...config.libsql
1531
+ });
1532
+ break;
1533
+ case 'odblite':
1534
+ if (!config.odblite) throw new Error('odblite config required for odblite backend');
1535
+ this.adapter = new ODBLiteAdapter(config.odblite);
1536
+ break;
1537
+ default:
1538
+ throw new Error(`Unknown backend type: ${config.backend}`);
1539
+ }
1540
+ }
1541
+ /**
1542
+ * Create a new database
1543
+ * @param name - Database name (becomes filename or ODB-Lite database name)
1544
+ * @param options - Optional creation options
1545
+ */ async createDatabase(name, options) {
1546
+ console.log(`📦 Creating database: ${name}`);
1547
+ // Build database-specific config
1548
+ const dbConfig = this.getDatabaseConfig(name);
1549
+ // Create connection
1550
+ const conn = await this.adapter.connect(dbConfig);
1551
+ // Run migrations if schema provided
1552
+ if (options?.schemaContent) await this.runMigrations(conn, {
1553
+ schemaContent: options.schemaContent
1554
+ });
1555
+ // Store connection
1556
+ this.connections.set(name, conn);
1557
+ this.connectionTimestamps.set(name, Date.now());
1558
+ // Evict LRU if pool is full
1559
+ if (this.connections.size > this.maxConnections) await this.evictLRU();
1560
+ console.log(`✅ Database created: ${name}`);
1561
+ }
1562
+ /**
1563
+ * Check if a database exists
1564
+ * @param name - Database name
1565
+ * @returns true if database exists, false otherwise
1566
+ *
1567
+ * Note: For local backends (libsql, bun-sqlite), checks file existence.
1568
+ * For ODB-Lite, checks if database is in connection cache.
1569
+ */ async databaseExists(name) {
1570
+ // Check if already in connection cache
1571
+ if (this.connections.has(name)) return true;
1572
+ // For local file-based backends, check if file exists
1573
+ switch(this.config.backend){
1574
+ case 'bun-sqlite':
1575
+ {
1576
+ const dbPath = `${this.config.databasePath}/${name}.db`;
1577
+ return __WEBPACK_EXTERNAL_MODULE_node_fs__["default"].existsSync(dbPath);
1578
+ }
1579
+ case 'libsql':
1580
+ {
1581
+ const dbPath = `${this.config.databasePath}/${name}.db`;
1582
+ return __WEBPACK_EXTERNAL_MODULE_node_fs__["default"].existsSync(dbPath);
1583
+ }
1584
+ case 'odblite':
1585
+ // For ODB-Lite, only return true if in cache
1586
+ // (no way to check remote database existence without connecting)
1587
+ return false;
1588
+ default:
1589
+ return false;
1590
+ }
1591
+ }
1592
+ /**
1593
+ * Get connection to a database
1594
+ * @param name - Database name
1595
+ *
1596
+ * If database exists on disk but not in cache, it will be connected automatically.
1597
+ * If database doesn't exist at all, an error will be thrown.
1598
+ */ async getConnection(name) {
1599
+ // Check if connection exists in cache
1600
+ let conn = this.connections.get(name);
1601
+ if (conn) {
1602
+ this.connectionTimestamps.set(name, Date.now());
1603
+ return conn;
1604
+ }
1605
+ // Check if database exists on disk
1606
+ const exists = await this.databaseExists(name);
1607
+ if (exists) {
1608
+ // Connect to existing database
1609
+ const dbConfig = this.getDatabaseConfig(name);
1610
+ conn = await this.adapter.connect(dbConfig);
1611
+ // Store connection
1612
+ this.connections.set(name, conn);
1613
+ this.connectionTimestamps.set(name, Date.now());
1614
+ // Evict LRU if pool is full
1615
+ if (this.connections.size > this.maxConnections) await this.evictLRU();
1616
+ return conn;
1617
+ }
1618
+ // If database doesn't exist, throw error
1619
+ throw new Error(`Database "${name}" not found. Call createDatabase("${name}", { schemaContent }) first.`);
1620
+ }
1621
+ /**
1622
+ * Delete a database
1623
+ * @param name - Database name
1624
+ */ async deleteDatabase(name) {
1625
+ console.log(`🗑️ Deleting database: ${name}`);
1626
+ // Close connection if exists
1627
+ const conn = this.connections.get(name);
1628
+ if (conn) {
1629
+ await conn.close();
1630
+ this.connections.delete(name);
1631
+ this.connectionTimestamps.delete(name);
1632
+ }
1633
+ // Backend-specific deletion
1634
+ await this.adapter.disconnect(name);
1635
+ console.log(`✅ Database deleted: ${name}`);
1636
+ }
1637
+ /**
1638
+ * Execute SQL content on a database connection
1639
+ * Useful for running migrations or initial schema
1640
+ * @param name - Database name
1641
+ * @param sqlContent - SQL content to execute
1642
+ */ async executeSQLFile(name, sqlContent) {
1643
+ const conn = await this.getConnection(name);
1644
+ await this.runMigrations(conn, {
1645
+ schemaContent: sqlContent
1646
+ });
1647
+ }
1648
+ /**
1649
+ * List all managed databases
1650
+ */ async listDatabases() {
1651
+ return Array.from(this.connections.keys());
1652
+ }
1653
+ /**
1654
+ * Close all connections
1655
+ */ async close() {
1656
+ console.log(`🔌 Closing all database connections...`);
1657
+ for (const [name, conn] of this.connections)await conn.close();
1658
+ this.connections.clear();
1659
+ this.connectionTimestamps.clear();
1660
+ console.log(`✅ All connections closed`);
1661
+ }
1662
+ /**
1663
+ * Run migrations on a connection
1664
+ */ async runMigrations(conn, config) {
1665
+ // Parse SQL statements
1666
+ const { pragmaStatements, regularStatements } = parseSQL(config.schemaContent);
1667
+ // Execute PRAGMA statements first
1668
+ for (const pragma of pragmaStatements)try {
1669
+ await conn.execute(pragma);
1670
+ } catch (error) {
1671
+ // Ignore PRAGMA errors (they might not be supported on all backends)
1672
+ console.log(`⚠️ PRAGMA note: ${error.message}`);
1673
+ }
1674
+ // Execute regular statements
1675
+ for (const statement of regularStatements)try {
1676
+ await conn.execute(statement);
1677
+ } catch (error) {
1678
+ // Ignore "already exists" errors for idempotency
1679
+ if (error.message?.includes('already exists') || error.message?.includes('no such column') || error.message?.includes('no such table')) {
1680
+ console.log(` ⚠️ Skipping: ${error.message}`);
1681
+ continue;
1682
+ }
1683
+ console.error(`❌ Migration failed: ${statement.substring(0, 100)}...`);
1684
+ throw error;
1685
+ }
1686
+ console.log(`✅ Migrations completed (${regularStatements.length} statements)`);
1687
+ }
1688
+ /**
1689
+ * Get database-specific configuration
1690
+ */ getDatabaseConfig(name) {
1691
+ switch(this.config.backend){
1692
+ case 'bun-sqlite':
1693
+ return {
1694
+ databasePath: `${this.config.databasePath}/${name}.db`,
1695
+ create: true
1696
+ };
1697
+ case 'libsql':
1698
+ return {
1699
+ url: `file:${this.config.databasePath}/${name}.db`,
1700
+ ...this.config.libsql
1701
+ };
1702
+ case 'odblite':
1703
+ return {
1704
+ databaseName: `${this.config.databasePath}_${name}`,
1705
+ ...this.config.odblite
1706
+ };
1707
+ default:
1708
+ throw new Error(`Unknown backend: ${this.config.backend}`);
1709
+ }
1710
+ }
1711
+ /**
1712
+ * Evict least recently used connection
1713
+ */ async evictLRU() {
1714
+ let oldestKey = null;
1715
+ let oldestTime = 1 / 0;
1716
+ for (const [key, timestamp] of this.connectionTimestamps)if (timestamp < oldestTime) {
1717
+ oldestTime = timestamp;
1718
+ oldestKey = key;
1719
+ }
1720
+ if (oldestKey) {
1721
+ const conn = this.connections.get(oldestKey);
1722
+ if (conn) await conn.close();
1723
+ this.connections.delete(oldestKey);
1724
+ this.connectionTimestamps.delete(oldestKey);
1725
+ console.log(`♻️ Evicted LRU connection: ${oldestKey}`);
1726
+ }
1727
+ }
1728
+ }
1001
1729
  // Main entry point for ODB Client
1002
1730
  // Core query client exports (postgres.js-like interface)
1003
1731
  // Service management exports (high-level tenant database management)
1732
+ // Database Manager exports (unified multi-backend database abstraction)
1004
1733
  // Export error classes
1005
1734
  // Export static utility functions for easy access
1006
1735
  const { raw: src_raw, identifier: src_identifier, where: src_where, insertValues: src_insertValues, updateSet: src_updateSet, join: src_join } = ODBLiteClient;
1007
- export { ConnectionError, HTTPClient, ODBLiteClient, ODBLiteError, ODBLiteTransaction, types_QueryError as QueryError, sql_parser_SQLParser as SQLParser, ServiceClient, SimpleTransaction, odblite as default, fragment, src_identifier as identifier, src_insertValues as insertValues, src_join as join, odblite, src_raw as raw, sql_parser_sql as sql, src_updateSet as updateSet, src_where as where };
1736
+ export { BunSQLiteAdapter, ConnectionError, DatabaseManager, HTTPClient, LibSQLAdapter, ODBLiteAdapter, ODBLiteClient, ODBLiteError, ODBLiteTransaction, types_QueryError as QueryError, sql_parser_SQLParser as SQLParser, ServiceClient, SimpleTransaction, odblite as default, fragment, src_identifier as identifier, src_insertValues as insertValues, src_join as join, odblite, parseSQL, src_raw as raw, splitSQLStatements, sql_parser_sql as sql, src_updateSet as updateSet, src_where as where };