@pineliner/odb-client 1.0.0 → 1.0.2

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