@zero-server/sdk 0.9.1 → 0.9.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.
Files changed (128) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +460 -443
  3. package/index.js +414 -412
  4. package/lib/app.js +1172 -1172
  5. package/lib/auth/authorize.js +399 -399
  6. package/lib/auth/enrollment.js +367 -367
  7. package/lib/auth/index.js +57 -57
  8. package/lib/auth/jwt.js +731 -731
  9. package/lib/auth/oauth.js +362 -362
  10. package/lib/auth/session.js +588 -588
  11. package/lib/auth/trustedDevice.js +409 -409
  12. package/lib/auth/twoFactor.js +1150 -1150
  13. package/lib/auth/webauthn.js +946 -946
  14. package/lib/body/index.js +14 -14
  15. package/lib/body/json.js +109 -109
  16. package/lib/body/multipart.js +440 -440
  17. package/lib/body/raw.js +71 -71
  18. package/lib/body/rawBuffer.js +160 -160
  19. package/lib/body/sendError.js +25 -25
  20. package/lib/body/text.js +75 -75
  21. package/lib/body/typeMatch.js +41 -41
  22. package/lib/body/urlencoded.js +235 -235
  23. package/lib/cli.js +845 -845
  24. package/lib/cluster.js +666 -666
  25. package/lib/debug.js +372 -372
  26. package/lib/env/index.js +465 -465
  27. package/lib/errors.js +683 -683
  28. package/lib/fetch/index.js +256 -256
  29. package/lib/grpc/balancer.js +378 -378
  30. package/lib/grpc/call.js +708 -708
  31. package/lib/grpc/client.js +764 -764
  32. package/lib/grpc/codec.js +1221 -1221
  33. package/lib/grpc/credentials.js +398 -398
  34. package/lib/grpc/frame.js +262 -262
  35. package/lib/grpc/health.js +287 -287
  36. package/lib/grpc/index.js +121 -121
  37. package/lib/grpc/metadata.js +461 -461
  38. package/lib/grpc/proto.js +821 -821
  39. package/lib/grpc/reflection.js +590 -590
  40. package/lib/grpc/server.js +445 -445
  41. package/lib/grpc/status.js +118 -118
  42. package/lib/grpc/watch.js +173 -173
  43. package/lib/http/index.js +10 -10
  44. package/lib/http/request.js +727 -727
  45. package/lib/http/response.js +799 -799
  46. package/lib/lifecycle.js +557 -557
  47. package/lib/middleware/compress.js +230 -230
  48. package/lib/middleware/cookieParser.js +237 -237
  49. package/lib/middleware/cors.js +93 -93
  50. package/lib/middleware/csrf.js +137 -137
  51. package/lib/middleware/errorHandler.js +101 -101
  52. package/lib/middleware/helmet.js +175 -175
  53. package/lib/middleware/index.js +19 -17
  54. package/lib/middleware/logger.js +74 -74
  55. package/lib/middleware/rateLimit.js +88 -88
  56. package/lib/middleware/requestId.js +53 -53
  57. package/lib/middleware/static.js +326 -326
  58. package/lib/middleware/timeout.js +71 -71
  59. package/lib/middleware/validator.js +255 -255
  60. package/lib/observe/health.js +326 -326
  61. package/lib/observe/index.js +50 -50
  62. package/lib/observe/logger.js +359 -359
  63. package/lib/observe/metrics.js +805 -805
  64. package/lib/observe/tracing.js +592 -592
  65. package/lib/orm/adapters/json.js +290 -290
  66. package/lib/orm/adapters/memory.js +764 -764
  67. package/lib/orm/adapters/mongo.js +764 -764
  68. package/lib/orm/adapters/mysql.js +933 -933
  69. package/lib/orm/adapters/postgres.js +1144 -1144
  70. package/lib/orm/adapters/redis.js +1534 -1534
  71. package/lib/orm/adapters/sql-base.js +212 -212
  72. package/lib/orm/adapters/sqlite.js +858 -858
  73. package/lib/orm/audit.js +649 -649
  74. package/lib/orm/cache.js +394 -394
  75. package/lib/orm/geo.js +387 -387
  76. package/lib/orm/index.js +784 -784
  77. package/lib/orm/migrate.js +432 -432
  78. package/lib/orm/model.js +1706 -1706
  79. package/lib/orm/plugin.js +375 -375
  80. package/lib/orm/procedures.js +836 -836
  81. package/lib/orm/profiler.js +233 -233
  82. package/lib/orm/query.js +1772 -1772
  83. package/lib/orm/replicas.js +241 -241
  84. package/lib/orm/schema.js +307 -307
  85. package/lib/orm/search.js +380 -380
  86. package/lib/orm/seed/data/commerce.js +136 -136
  87. package/lib/orm/seed/data/internet.js +111 -111
  88. package/lib/orm/seed/data/locations.js +204 -204
  89. package/lib/orm/seed/data/names.js +338 -338
  90. package/lib/orm/seed/data/person.js +128 -128
  91. package/lib/orm/seed/data/phone.js +211 -211
  92. package/lib/orm/seed/data/words.js +134 -134
  93. package/lib/orm/seed/factory.js +178 -178
  94. package/lib/orm/seed/fake.js +1186 -1186
  95. package/lib/orm/seed/index.js +18 -18
  96. package/lib/orm/seed/rng.js +70 -70
  97. package/lib/orm/seed/seeder.js +124 -124
  98. package/lib/orm/seed/unique.js +68 -68
  99. package/lib/orm/snapshot.js +366 -366
  100. package/lib/orm/tenancy.js +605 -605
  101. package/lib/orm/views.js +350 -350
  102. package/lib/router/index.js +436 -436
  103. package/lib/sse/index.js +8 -8
  104. package/lib/sse/stream.js +349 -349
  105. package/lib/ws/connection.js +451 -451
  106. package/lib/ws/handshake.js +125 -125
  107. package/lib/ws/index.js +14 -14
  108. package/lib/ws/room.js +223 -223
  109. package/package.json +73 -73
  110. package/types/app.d.ts +223 -223
  111. package/types/auth.d.ts +520 -520
  112. package/types/body.d.ts +14 -0
  113. package/types/cli.d.ts +2 -0
  114. package/types/cluster.d.ts +75 -75
  115. package/types/env.d.ts +80 -80
  116. package/types/errors.d.ts +316 -316
  117. package/types/fetch.d.ts +43 -43
  118. package/types/grpc.d.ts +432 -432
  119. package/types/index.d.ts +384 -384
  120. package/types/lifecycle.d.ts +60 -60
  121. package/types/middleware.d.ts +320 -320
  122. package/types/observe.d.ts +304 -304
  123. package/types/orm.d.ts +1887 -1887
  124. package/types/request.d.ts +109 -109
  125. package/types/response.d.ts +157 -157
  126. package/types/router.d.ts +78 -78
  127. package/types/sse.d.ts +78 -78
  128. package/types/websocket.d.ts +126 -126
@@ -1,858 +1,858 @@
1
- /**
2
- * @module orm/adapters/sqlite
3
- * @description SQLite adapter using the optional `better-sqlite3` driver.
4
- * Requires: `npm install better-sqlite3`
5
- *
6
- * @example
7
- * const { Database, Model, TYPES } = require('@zero-server/sdk');
8
- *
9
- * const db = Database.connect('sqlite', { filename: './app.db' });
10
- *
11
- * class User extends Model {
12
- * static table = 'users';
13
- * static schema = {
14
- * id: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },
15
- * name: { type: TYPES.STRING, required: true },
16
- * email: { type: TYPES.STRING, required: true, unique: true },
17
- * };
18
- * static timestamps = true;
19
- * }
20
- *
21
- * db.register(User);
22
- * await db.sync();
23
- *
24
- * const user = await User.create({ name: 'Alice', email: 'a@b.com' });
25
- * const found = await User.findById(user.id);
26
- */
27
- const path = require('path');
28
- const fs = require('fs');
29
- const BaseSqlAdapter = require('./sql-base');
30
- const { validateFKAction, validateCheck } = require('../schema');
31
-
32
- class SqliteAdapter extends BaseSqlAdapter
33
- {
34
- /**
35
- * @constructor
36
- * @param {object} options - Configuration options.
37
- * @param {string} [options.filename=':memory:'] - Path to SQLite file, or ':memory:'.
38
- * @param {boolean} [options.readonly=false] - Open database in read-only mode.
39
- * @param {boolean} [options.fileMustExist=false] - Throw if the database file does not exist.
40
- * @param {boolean} [options.verbose] - Log every SQL statement (debug).
41
- * @param {boolean} [options.createDir=true] - Automatically create parent directories for the file.
42
- * @param {object} [options.pragmas] - PRAGMA settings to apply on open.
43
- * @param {string} [options.pragmas.journal_mode='WAL'] - Journal mode (WAL, DELETE, TRUNCATE, MEMORY, OFF).
44
- * @param {string} [options.pragmas.foreign_keys='ON'] - Enforce foreign-key constraints.
45
- * @param {string} [options.pragmas.busy_timeout='5000'] - Milliseconds to wait on a locked database.
46
- * @param {string} [options.pragmas.synchronous='NORMAL'] - Sync mode (OFF, NORMAL, FULL, EXTRA).
47
- * @param {string} [options.pragmas.cache_size='-64000'] - Page cache size (negative = KiB, e.g. -64000 = 64 MB).
48
- * @param {string} [options.pragmas.temp_store='MEMORY'] - Temp tables in memory for speed.
49
- * @param {string} [options.pragmas.mmap_size='268435456'] - Memory-mapped I/O size (256 MB).
50
- * @param {string} [options.pragmas.page_size] - Page size in bytes (must be set before WAL).
51
- * @param {string} [options.pragmas.auto_vacuum] - Auto-vacuum mode (NONE, FULL, INCREMENTAL).
52
- * @param {string} [options.pragmas.secure_delete] - Overwrite deleted content with zeros.
53
- * @param {string} [options.pragmas.wal_autocheckpoint] - Pages before auto-checkpoint (default 1000).
54
- * @param {string} [options.pragmas.locking_mode] - NORMAL or EXCLUSIVE.
55
- */
56
- constructor(options = {})
57
- {
58
- super();
59
- let Database;
60
- try { Database = require('better-sqlite3'); }
61
- catch (e)
62
- {
63
- throw new Error(
64
- 'SQLite adapter requires "better-sqlite3" package.\n' +
65
- 'Install it with: npm install better-sqlite3'
66
- );
67
- }
68
-
69
- const filename = options.filename || ':memory:';
70
-
71
- // Auto-create parent directories for file-based databases
72
- if (filename !== ':memory:' && options.createDir !== false)
73
- {
74
- const dir = path.dirname(path.resolve(filename));
75
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
76
- }
77
-
78
- // Build better-sqlite3 constructor options
79
- const dbOpts = {};
80
- if (options.readonly) dbOpts.readonly = true;
81
- if (options.fileMustExist) dbOpts.fileMustExist = true;
82
- if (options.verbose) dbOpts.verbose = console.log;
83
-
84
- this._db = new Database(filename, dbOpts);
85
- this._filename = filename;
86
-
87
- /** @private Prepared statement cache — avoids recompilation overhead */
88
- this._stmtCache = new Map();
89
- /** @private Maximum cached statements before LRU eviction */
90
- this._stmtCacheMax = options.stmtCacheSize || 256;
91
- /** @private Statement cache hit counter */
92
- this._stmtCacheHits = 0;
93
- /** @private Statement cache miss counter */
94
- this._stmtCacheMisses = 0;
95
-
96
- // Apply pragmas (with production-ready defaults)
97
- const pragmas = {
98
- journal_mode: 'WAL',
99
- foreign_keys: 'ON',
100
- busy_timeout: '5000',
101
- synchronous: 'NORMAL',
102
- cache_size: '-64000',
103
- temp_store: 'MEMORY',
104
- mmap_size: '268435456',
105
- ...options.pragmas,
106
- };
107
- for (const [key, val] of Object.entries(pragmas))
108
- this._db.pragma(`${key} = ${val}`);
109
- }
110
-
111
- // -- Statement Caching --------------------------------
112
-
113
- /**
114
- * Get or compile a prepared statement from cache.
115
- * Uses LRU eviction when cache exceeds max size.
116
- * @param {string} sql - SQL query string.
117
- * @returns {Statement} Compiled prepared statement.
118
- * @private
119
- */
120
- _prepare(sql)
121
- {
122
- let stmt = this._stmtCache.get(sql);
123
- if (stmt)
124
- {
125
- this._stmtCacheHits++;
126
- // LRU: move to end (most recently used)
127
- this._stmtCache.delete(sql);
128
- this._stmtCache.set(sql, stmt);
129
- return stmt;
130
- }
131
-
132
- this._stmtCacheMisses++;
133
- stmt = this._db.prepare(sql);
134
- if (this._stmtCache.size >= this._stmtCacheMax)
135
- {
136
- const oldest = this._stmtCache.keys().next().value;
137
- this._stmtCache.delete(oldest);
138
- }
139
- this._stmtCache.set(sql, stmt);
140
- return stmt;
141
- }
142
-
143
- /**
144
- * Get prepared statement cache statistics.
145
- * @returns {{ size: number, maxSize: number, hits: number, misses: number, hitRate: number }}
146
- */
147
- stmtCacheStats()
148
- {
149
- const total = this._stmtCacheHits + this._stmtCacheMisses;
150
- return {
151
- size: this._stmtCache.size,
152
- maxSize: this._stmtCacheMax,
153
- hits: this._stmtCacheHits,
154
- misses: this._stmtCacheMisses,
155
- hitRate: total > 0 ? this._stmtCacheHits / total : 0,
156
- };
157
- }
158
-
159
- /**
160
- * Get the query execution plan (EXPLAIN QUERY PLAN).
161
- * @param {object} descriptor - Query descriptor from the Query builder.
162
- * @returns {Array<{ id: number, parent: number, notused: number, detail: string }>}
163
- */
164
- explain(descriptor)
165
- {
166
- const { table, fields, where, orderBy, limit, offset, distinct, joins, groupBy, having } = descriptor;
167
-
168
- const selectFields = fields && fields.length
169
- ? fields.map(f => `"${f}"`).join(', ')
170
- : '*';
171
- const distinctStr = distinct ? 'DISTINCT ' : '';
172
- const joinStr = this._buildJoins(joins, table);
173
- let sql = `SELECT ${distinctStr}${selectFields} FROM "${table}"${joinStr}`;
174
-
175
- const values = [];
176
- if (where && where.length > 0)
177
- {
178
- const { clause, values: wv } = this._buildWhereFromChain(where);
179
- sql += clause;
180
- values.push(...wv);
181
- }
182
-
183
- sql += this._buildGroupBy(groupBy);
184
- sql += this._buildHaving(having, values);
185
-
186
- if (orderBy && orderBy.length > 0)
187
- sql += ' ORDER BY ' + orderBy.map(o => `"${o.field}" ${o.dir}`).join(', ');
188
- if (limit !== null && limit !== undefined) { sql += ' LIMIT ?'; values.push(limit); }
189
- if (offset !== null && offset !== undefined) { sql += ' OFFSET ?'; values.push(offset); }
190
-
191
- return this._db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...values);
192
- }
193
-
194
- /** @private @override */
195
- _typeMap(colDef)
196
- {
197
- const map = {
198
- string: 'TEXT', text: 'TEXT', integer: 'INTEGER', float: 'REAL',
199
- boolean: 'INTEGER', date: 'TEXT', datetime: 'TEXT',
200
- json: 'TEXT', blob: 'BLOB', uuid: 'TEXT',
201
- bigint: 'INTEGER', smallint: 'INTEGER', tinyint: 'INTEGER',
202
- decimal: 'REAL', double: 'REAL', real: 'REAL',
203
- timestamp: 'TEXT', time: 'TEXT',
204
- binary: 'BLOB', varbinary: 'BLOB',
205
- char: 'TEXT', varchar: 'TEXT',
206
- numeric: 'NUMERIC',
207
- };
208
- return map[colDef.type] || 'TEXT';
209
- }
210
-
211
- /**
212
- * Create a table with the given schema.
213
- * @param {string} table - Table name.
214
- * @param {object} schema - Column definitions keyed by column name.
215
- * @returns {Promise<void>}
216
- */
217
- async createTable(table, schema)
218
- {
219
- const cols = [];
220
- const tableConstraints = [];
221
- const compositePKs = [];
222
-
223
- for (const [name, def] of Object.entries(schema))
224
- {
225
- let line = `"${name}" ${this._typeMap(def)}`;
226
-
227
- // Collect composite PK candidates
228
- if (def.primaryKey && def.compositeKey) { compositePKs.push(name); }
229
- else if (def.primaryKey)
230
- {
231
- line += ' PRIMARY KEY';
232
- if (def.autoIncrement) line += ' AUTOINCREMENT';
233
- }
234
-
235
- if (def.required && !def.primaryKey) line += ' NOT NULL';
236
- if (def.unique && !def.compositeUnique) line += ' UNIQUE';
237
- if (def.default !== undefined && typeof def.default !== 'function')
238
- {
239
- line += ` DEFAULT ${this._sqlDefault(def.default)}`;
240
- }
241
-
242
- // CHECK constraint
243
- if (def.check)
244
- {
245
- line += ` CHECK(${validateCheck(def.check)})`;
246
- }
247
- else if (def.enum && def.type !== 'enum')
248
- {
249
- const vals = def.enum.map(v => `'${String(v).replace(/'/g, "''")}'`).join(', ');
250
- line += ` CHECK("${name}" IN (${vals}))`;
251
- }
252
-
253
- // Foreign key (inline reference)
254
- if (def.references)
255
- {
256
- line += ` REFERENCES "${def.references.table}"("${def.references.column || 'id'}")`;
257
- if (def.references.onDelete) line += ` ON DELETE ${validateFKAction(def.references.onDelete)}`;
258
- if (def.references.onUpdate) line += ` ON UPDATE ${validateFKAction(def.references.onUpdate)}`;
259
- }
260
-
261
- cols.push(line);
262
- }
263
-
264
- // Composite primary key
265
- if (compositePKs.length > 0)
266
- {
267
- tableConstraints.push(`PRIMARY KEY (${compositePKs.map(k => `"${k}"`).join(', ')})`);
268
- }
269
-
270
- // Composite unique constraints
271
- const compositeUniques = {};
272
- for (const [name, def] of Object.entries(schema))
273
- {
274
- if (def.compositeUnique)
275
- {
276
- const group = typeof def.compositeUnique === 'string' ? def.compositeUnique : 'default';
277
- if (!compositeUniques[group]) compositeUniques[group] = [];
278
- compositeUniques[group].push(name);
279
- }
280
- }
281
- for (const [, columns] of Object.entries(compositeUniques))
282
- {
283
- tableConstraints.push(`UNIQUE (${columns.map(c => `"${c}"`).join(', ')})`);
284
- }
285
-
286
- const allParts = [...cols, ...tableConstraints];
287
- this._db.exec(`CREATE TABLE IF NOT EXISTS "${table}" (${allParts.join(', ')})`);
288
-
289
- // Create indexes defined in schema
290
- for (const [name, def] of Object.entries(schema))
291
- {
292
- if (def.index)
293
- {
294
- const idxName = typeof def.index === 'string' ? def.index : `idx_${table}_${name}`;
295
- this._db.exec(`CREATE INDEX IF NOT EXISTS "${idxName}" ON "${table}" ("${name}")`);
296
- }
297
- }
298
-
299
- // Composite indexes
300
- const compositeIndexes = {};
301
- for (const [name, def] of Object.entries(schema))
302
- {
303
- if (def.compositeIndex)
304
- {
305
- const group = typeof def.compositeIndex === 'string' ? def.compositeIndex : 'default';
306
- if (!compositeIndexes[group]) compositeIndexes[group] = [];
307
- compositeIndexes[group].push(name);
308
- }
309
- }
310
- for (const [group, columns] of Object.entries(compositeIndexes))
311
- {
312
- const idxName = `idx_${table}_${group}`;
313
- this._db.exec(`CREATE INDEX IF NOT EXISTS "${idxName}" ON "${table}" (${columns.map(c => `"${c}"`).join(', ')})`);
314
- }
315
- }
316
-
317
- /**
318
- * Drop a table if it exists.
319
- * @param {string} table - Table name.
320
- * @returns {Promise<void>}
321
- */
322
- async dropTable(table)
323
- {
324
- this._db.exec(`DROP TABLE IF EXISTS "${table}"`);
325
- }
326
-
327
- /**
328
- * Insert a single row.
329
- * @param {string} table - Table name.
330
- * @param {object} data - Row data as key-value pairs.
331
- * @returns {Promise<object>} The inserted row.
332
- */
333
- async insert(table, data)
334
- {
335
- const keys = Object.keys(data);
336
- const placeholders = keys.map(() => '?').join(', ');
337
- const values = keys.map(k => this._toSqlValue(data[k]));
338
- const sql = `INSERT INTO "${table}" (${keys.map(k => `"${k}"`).join(', ')}) VALUES (${placeholders})`;
339
- const result = this._prepare(sql).run(...values);
340
- return { ...data, id: result.lastInsertRowid };
341
- }
342
-
343
- /**
344
- * Insert multiple rows in a batch.
345
- * @param {string} table - Table name.
346
- * @param {Array<object>} dataArray - Array of row objects.
347
- * @returns {Promise<Array<object>>} The inserted rows.
348
- */
349
- async insertMany(table, dataArray)
350
- {
351
- if (!dataArray.length) return [];
352
- const keys = Object.keys(dataArray[0]);
353
- const placeholders = keys.map(() => '?').join(', ');
354
- const sql = `INSERT INTO "${table}" (${keys.map(k => `"${k}"`).join(', ')}) VALUES (${placeholders})`;
355
- const stmt = this._prepare(sql);
356
- const results = [];
357
- const runAll = this._db.transaction((items) => {
358
- for (const data of items)
359
- {
360
- const values = keys.map(k => this._toSqlValue(data[k]));
361
- const result = stmt.run(...values);
362
- results.push({ ...data, id: result.lastInsertRowid });
363
- }
364
- });
365
- runAll(dataArray);
366
- return results;
367
- }
368
-
369
- /**
370
- * Update a single row by primary key.
371
- * @param {string} table - Table name.
372
- * @param {string} pk - Primary key column.
373
- * @param {*} pkVal - Primary key value.
374
- * @param {object} data - Fields to update.
375
- * @returns {Promise<void>}
376
- */
377
- async update(table, pk, pkVal, data)
378
- {
379
- const sets = Object.keys(data).map(k => `"${k}" = ?`).join(', ');
380
- const values = [...Object.values(data).map(v => this._toSqlValue(v)), pkVal];
381
- this._prepare(`UPDATE "${table}" SET ${sets} WHERE "${pk}" = ?`).run(...values);
382
- }
383
-
384
- /**
385
- * Update rows matching the given conditions.
386
- * @param {string} table - Table name.
387
- * @param {object} conditions - Filter conditions.
388
- * @param {object} data - Fields to update.
389
- * @returns {Promise<number>} Number of affected rows.
390
- */
391
- async updateWhere(table, conditions, data)
392
- {
393
- const { clause, values: whereVals } = this._buildWhere(conditions);
394
- const sets = Object.keys(data).map(k => `"${k}" = ?`).join(', ');
395
- const values = [...Object.values(data).map(v => this._toSqlValue(v)), ...whereVals];
396
- const result = this._prepare(`UPDATE "${table}" SET ${sets}${clause}`).run(...values);
397
- return result.changes;
398
- }
399
-
400
- /**
401
- * Delete a single row by primary key.
402
- * @param {string} table - Table name.
403
- * @param {string} pk - Primary key column.
404
- * @param {*} pkVal - Primary key value.
405
- * @returns {Promise<void>}
406
- */
407
- async remove(table, pk, pkVal)
408
- {
409
- this._prepare(`DELETE FROM "${table}" WHERE "${pk}" = ?`).run(pkVal);
410
- }
411
-
412
- /**
413
- * Delete rows matching the given conditions.
414
- * @param {string} table - Table name.
415
- * @param {object} conditions - Filter conditions.
416
- * @returns {Promise<number>} Number of deleted rows.
417
- */
418
- async deleteWhere(table, conditions)
419
- {
420
- const { clause, values } = this._buildWhere(conditions);
421
- const result = this._prepare(`DELETE FROM "${table}"${clause}`).run(...values);
422
- return result.changes;
423
- }
424
-
425
- /**
426
- * Execute a query descriptor built by the Query builder.
427
- * @param {object} descriptor - Query descriptor with table, fields, where, orderBy, limit, offset.
428
- * @returns {Promise<Array<object>>} Result rows.
429
- */
430
- async execute(descriptor)
431
- {
432
- const { action, table, fields, where, orderBy, limit, offset, distinct, joins, groupBy, having } = descriptor;
433
-
434
- if (action === 'count')
435
- {
436
- const { clause, values } = this._buildWhereFromChain(where);
437
- const joinStr = this._buildJoins(joins, table);
438
- const row = this._prepare(`SELECT COUNT(*) as count FROM "${table}"${joinStr}${clause}`).get(...values);
439
- return row.count;
440
- }
441
-
442
- const selectFields = fields && fields.length
443
- ? fields.map(f => `"${f}"`).join(', ')
444
- : '*';
445
- const distinctStr = distinct ? 'DISTINCT ' : '';
446
- const joinStr = this._buildJoins(joins, table);
447
- let sql = `SELECT ${distinctStr}${selectFields} FROM "${table}"${joinStr}`;
448
-
449
- const values = [];
450
- if (where && where.length > 0)
451
- {
452
- const { clause, values: wv } = this._buildWhereFromChain(where);
453
- sql += clause;
454
- values.push(...wv);
455
- }
456
-
457
- sql += this._buildGroupBy(groupBy);
458
- sql += this._buildHaving(having, values);
459
-
460
- if (orderBy && orderBy.length > 0)
461
- {
462
- sql += ' ORDER BY ' + orderBy.map(o => `"${o.field}" ${o.dir}`).join(', ');
463
- }
464
-
465
- if (limit !== null && limit !== undefined)
466
- {
467
- sql += ' LIMIT ?';
468
- values.push(limit);
469
- }
470
-
471
- if (offset !== null && offset !== undefined)
472
- {
473
- sql += ' OFFSET ?';
474
- values.push(offset);
475
- }
476
-
477
- return this._prepare(sql).all(...values);
478
- }
479
-
480
- /**
481
- * Execute an aggregate function (count, sum, avg, min, max).
482
- * @param {object} descriptor - Aggregate descriptor with table, function, field, where.
483
- * @returns {Promise<number|null>} Aggregate result.
484
- */
485
- async aggregate(descriptor)
486
- {
487
- const { table, where, aggregateFn, aggregateField, joins, groupBy, having } = descriptor;
488
- const fn = aggregateFn.toUpperCase();
489
- const joinStr = this._buildJoins(joins, table);
490
- const values = [];
491
-
492
- let sql = `SELECT ${fn}("${aggregateField}") as result FROM "${table}"${joinStr}`;
493
-
494
- if (where && where.length > 0)
495
- {
496
- const { clause, values: wv } = this._buildWhereFromChain(where);
497
- sql += clause;
498
- values.push(...wv);
499
- }
500
-
501
- sql += this._buildGroupBy(groupBy);
502
- sql += this._buildHaving(having, values);
503
-
504
- const row = this._prepare(sql).get(...values);
505
- return row ? row.result : null;
506
- }
507
-
508
- // -- SQLite Utilities -----------------------------------------------
509
-
510
- /**
511
- * Ping the database to check connectivity.
512
- * @returns {boolean} true if healthy.
513
- */
514
- ping()
515
- {
516
- try
517
- {
518
- this._db.prepare('SELECT 1').get();
519
- return true;
520
- }
521
- catch
522
- {
523
- return false;
524
- }
525
- }
526
-
527
- /**
528
- * Read a single PRAGMA value.
529
- * @param {string} key - PRAGMA name (e.g. 'journal_mode').
530
- * @returns {*} Current value.
531
- */
532
- pragma(key)
533
- {
534
- const rows = this._db.pragma(key);
535
- if (Array.isArray(rows) && rows.length === 1) return Object.values(rows[0])[0];
536
- return rows;
537
- }
538
-
539
- /**
540
- * Force a WAL checkpoint (only useful in WAL mode).
541
- * @param {'PASSIVE'|'FULL'|'RESTART'|'TRUNCATE'} [mode='PASSIVE'] - Operation mode.
542
- * @returns {{ busy: number, log: number, checkpointed: number }}
543
- */
544
- checkpoint(mode = 'PASSIVE')
545
- {
546
- const allowed = ['PASSIVE', 'FULL', 'RESTART', 'TRUNCATE'];
547
- const m = String(mode).toUpperCase();
548
- if (!allowed.includes(m)) throw new Error(`Invalid checkpoint mode: ${mode}`);
549
- const row = this._db.pragma(`wal_checkpoint(${m})`);
550
- return Array.isArray(row) ? row[0] : row;
551
- }
552
-
553
- /**
554
- * Run `PRAGMA integrity_check`.
555
- * @returns {string} 'ok' if healthy, or a description of the problem.
556
- */
557
- integrity()
558
- {
559
- const rows = this._db.pragma('integrity_check');
560
- const val = Array.isArray(rows) ? rows[0] : rows;
561
- return (val && typeof val === 'object') ? Object.values(val)[0] : val;
562
- }
563
-
564
- /**
565
- * Rebuild the database file, reclaiming free pages.
566
- */
567
- vacuum()
568
- {
569
- this._db.exec('VACUUM');
570
- }
571
-
572
- /**
573
- * Get the size of the database file in bytes.
574
- * Returns 0 for in-memory databases.
575
- * @returns {number} File size in bytes.
576
- */
577
- fileSize()
578
- {
579
- if (this._filename === ':memory:') return 0;
580
- try { return fs.statSync(path.resolve(this._filename)).size; }
581
- catch { return 0; }
582
- }
583
-
584
- /**
585
- * List all user-created tables.
586
- * @returns {string[]} Array of table names.
587
- */
588
- tables()
589
- {
590
- const rows = this._db.prepare(
591
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
592
- ).all();
593
- return rows.map(r => r.name);
594
- }
595
-
596
- /**
597
- * Close the database connection.
598
- */
599
- close()
600
- {
601
- this._db.close();
602
- }
603
-
604
- /**
605
- * Run a raw SQL query.
606
- * @param {string} sql - SQL query string.
607
- * @param {...*} params - Bound parameter values.
608
- * @returns {*} Query result rows.
609
- */
610
- raw(sql, ...params)
611
- {
612
- const stmt = this._db.prepare(sql);
613
- return stmt.all(...params);
614
- }
615
-
616
- /**
617
- * Begin a transaction.
618
- * @param {Function} fn - Function to run inside the transaction.
619
- * @returns {*} Return value of fn.
620
- */
621
- transaction(fn)
622
- {
623
- return this._db.transaction(fn)();
624
- }
625
-
626
- // -- Table Info & Debug (Schema Introspection) -----------
627
-
628
- /**
629
- * Get column information for a table.
630
- * @param {string} table - Table name.
631
- * @returns {Array<{ cid: number, name: string, type: string, notnull: boolean, defaultValue: *, pk: boolean }>}
632
- */
633
- columns(table)
634
- {
635
- const rows = this._db.pragma(`table_info("${table.replace(/"/g, '""')}")`);
636
- return rows.map(r => ({
637
- cid: r.cid, name: r.name, type: r.type,
638
- notnull: !!r.notnull, defaultValue: r.dflt_value, pk: !!r.pk,
639
- }));
640
- }
641
-
642
- /**
643
- * Get indexes for a table.
644
- * @param {string} table - Table name.
645
- * @returns {Array<{ name: string, unique: boolean, columns: string[] }>}
646
- */
647
- indexes(table)
648
- {
649
- const idxList = this._db.pragma(`index_list("${table.replace(/"/g, '""')}")`);
650
- return idxList.map(idx => {
651
- const cols = this._db.pragma(`index_info("${idx.name.replace(/"/g, '""')}")`);
652
- return {
653
- name: idx.name, unique: !!idx.unique,
654
- columns: cols.map(c => c.name),
655
- };
656
- });
657
- }
658
-
659
- /**
660
- * Get foreign keys for a table.
661
- * @param {string} table - Table name.
662
- * @returns {Array<{ id: number, table: string, from: string, to: string, onUpdate: string, onDelete: string }>}
663
- */
664
- foreignKeys(table)
665
- {
666
- const rows = this._db.pragma(`foreign_key_list("${table.replace(/"/g, '""')}")`);
667
- return rows.map(r => ({
668
- id: r.id, table: r.table, from: r.from, to: r.to,
669
- onUpdate: r.on_update, onDelete: r.on_delete,
670
- }));
671
- }
672
-
673
- /**
674
- * Get detailed table status (size estimates, row counts).
675
- * @param {string} [table] - If omitted, returns all tables.
676
- * @returns {Array<{ name: string, rows: number, pageCount: number }>}
677
- */
678
- tableStatus(table)
679
- {
680
- const names = table ? [table] : this.tables();
681
- return names.map(name => {
682
- const count = this._db.prepare(`SELECT COUNT(*) as count FROM "${name.replace(/"/g, '""')}"`).get();
683
- return { name, rows: count.count };
684
- });
685
- }
686
-
687
- /**
688
- * Get counts for all tables — structured database overview.
689
- * @returns {{ tables: Array<{ name: string, rows: number }>, totalRows: number, fileSize: string }}
690
- */
691
- overview()
692
- {
693
- const tables = this.tableStatus();
694
- let totalRows = 0;
695
- for (const t of tables) totalRows += t.rows;
696
- const bytes = this.fileSize();
697
- const fmt = (b) => {
698
- if (b >= 1073741824) return (b / 1073741824).toFixed(2) + ' GB';
699
- if (b >= 1048576) return (b / 1048576).toFixed(2) + ' MB';
700
- if (b >= 1024) return (b / 1024).toFixed(2) + ' KB';
701
- return b + ' B';
702
- };
703
- return { tables, totalRows, fileSize: fmt(bytes) };
704
- }
705
-
706
- /**
707
- * Get the page size and page count (helps estimate table overhead).
708
- * @returns {{ pageSize: number, pageCount: number, totalBytes: number }}
709
- */
710
- pageInfo()
711
- {
712
- const pageSize = this.pragma('page_size');
713
- const pageCount = this.pragma('page_count');
714
- return { pageSize, pageCount, totalBytes: pageSize * pageCount };
715
- }
716
-
717
- /**
718
- * Get compile-time options that SQLite was built with.
719
- * @returns {string[]} Array of compile option strings.
720
- */
721
- compileOptions()
722
- {
723
- return this._db.pragma('compile_options').map(r => Object.values(r)[0]);
724
- }
725
-
726
- /**
727
- * Get the number of cached prepared statements.
728
- * @returns {{ cached: number, max: number }}
729
- */
730
- cacheStatus()
731
- {
732
- return { cached: this._stmtCache.size, max: this._stmtCacheMax };
733
- }
734
-
735
- // -- Schema Migrations -----------------------------------------------
736
-
737
- /**
738
- * Add a column to an existing table.
739
- * @param {string} table - Table name.
740
- * @param {string} column - Column name.
741
- * @param {object} colDef - Column definition (same format as schema entries).
742
- */
743
- addColumn(table, column, colDef)
744
- {
745
- let line = `"${column}" ${this._typeMap(colDef)}`;
746
- if (colDef.required) line += ' NOT NULL';
747
- if (colDef.unique) line += ' UNIQUE';
748
- if (colDef.default !== undefined && typeof colDef.default !== 'function')
749
- line += ` DEFAULT ${this._sqlDefault(colDef.default)}`;
750
- if (colDef.check) line += ` CHECK(${validateCheck(colDef.check)})`;
751
- if (colDef.references)
752
- {
753
- line += ` REFERENCES "${colDef.references.table}"("${colDef.references.column || 'id'}")`;
754
- if (colDef.references.onDelete) line += ` ON DELETE ${validateFKAction(colDef.references.onDelete)}`;
755
- if (colDef.references.onUpdate) line += ` ON UPDATE ${validateFKAction(colDef.references.onUpdate)}`;
756
- }
757
- this._db.exec(`ALTER TABLE "${table}" ADD COLUMN ${line}`);
758
- }
759
-
760
- /**
761
- * Drop a column from an existing table.
762
- * Requires SQLite 3.35.0+ (2021-03-12).
763
- * @param {string} table - Table name.
764
- * @param {string} column - Column name.
765
- */
766
- dropColumn(table, column)
767
- {
768
- this._db.exec(`ALTER TABLE "${table}" DROP COLUMN "${column}"`);
769
- }
770
-
771
- /**
772
- * Rename a column in an existing table.
773
- * Requires SQLite 3.25.0+ (2018-09-15).
774
- * @param {string} table - Table name.
775
- * @param {string} oldName - Current column name.
776
- * @param {string} newName - New column name.
777
- */
778
- renameColumn(table, oldName, newName)
779
- {
780
- this._db.exec(`ALTER TABLE "${table}" RENAME COLUMN "${oldName}" TO "${newName}"`);
781
- }
782
-
783
- /**
784
- * Rename a table.
785
- * @param {string} oldName - Current table name.
786
- * @param {string} newName - New table name.
787
- */
788
- renameTable(oldName, newName)
789
- {
790
- this._db.exec(`ALTER TABLE "${oldName}" RENAME TO "${newName}"`);
791
- }
792
-
793
- /**
794
- * Create an index.
795
- * @param {string} table - Table name.
796
- * @param {string[]} columns - Column names.
797
- * @param {object} [opts] - Options.
798
- * @param {string} [opts.name] - Index name (auto-generated if omitted).
799
- * @param {boolean} [opts.unique] - Create a UNIQUE index.
800
- */
801
- createIndex(table, columns, opts = {})
802
- {
803
- const name = opts.name || `idx_${table}_${columns.join('_')}`;
804
- const unique = opts.unique ? 'UNIQUE ' : '';
805
- this._db.exec(`CREATE ${unique}INDEX IF NOT EXISTS "${name}" ON "${table}" (${columns.map(c => `"${c}"`).join(', ')})`);
806
- }
807
-
808
- /**
809
- * Drop an index.
810
- * @param {string} _table - Table name (unused — SQLite indexes are schema-scoped).
811
- * @param {string} name - Index name.
812
- */
813
- dropIndex(_table, name)
814
- {
815
- this._db.exec(`DROP INDEX IF EXISTS "${name}"`);
816
- }
817
-
818
- /**
819
- * Check if a table exists.
820
- * @param {string} table - Table name.
821
- * @returns {boolean} `true` if the table exists.
822
- */
823
- hasTable(table)
824
- {
825
- const row = this._db.prepare(
826
- "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?"
827
- ).get(table);
828
- return !!row;
829
- }
830
-
831
- /**
832
- * Check if a column exists in a table.
833
- * @param {string} table - Table name.
834
- * @param {string} column - Column name.
835
- * @returns {boolean} `true` if the column exists.
836
- */
837
- hasColumn(table, column)
838
- {
839
- const cols = this.columns(table);
840
- return cols.some(c => c.name === column);
841
- }
842
-
843
- /**
844
- * Get a unified table description.
845
- * @param {string} table - Table name.
846
- * @returns {{ columns: Array, indexes: Array, foreignKeys: Array }}
847
- */
848
- describeTable(table)
849
- {
850
- return {
851
- columns: this.columns(table),
852
- indexes: this.indexes(table),
853
- foreignKeys: this.foreignKeys(table),
854
- };
855
- }
856
- }
857
-
858
- module.exports = SqliteAdapter;
1
+ /**
2
+ * @module orm/adapters/sqlite
3
+ * @description SQLite adapter using the optional `better-sqlite3` driver.
4
+ * Requires: `npm install better-sqlite3`
5
+ *
6
+ * @example
7
+ * const { Database, Model, TYPES } = require('@zero-server/sdk');
8
+ *
9
+ * const db = Database.connect('sqlite', { filename: './app.db' });
10
+ *
11
+ * class User extends Model {
12
+ * static table = 'users';
13
+ * static schema = {
14
+ * id: { type: TYPES.INTEGER, primaryKey: true, autoIncrement: true },
15
+ * name: { type: TYPES.STRING, required: true },
16
+ * email: { type: TYPES.STRING, required: true, unique: true },
17
+ * };
18
+ * static timestamps = true;
19
+ * }
20
+ *
21
+ * db.register(User);
22
+ * await db.sync();
23
+ *
24
+ * const user = await User.create({ name: 'Alice', email: 'a@b.com' });
25
+ * const found = await User.findById(user.id);
26
+ */
27
+ const path = require('path');
28
+ const fs = require('fs');
29
+ const BaseSqlAdapter = require('./sql-base');
30
+ const { validateFKAction, validateCheck } = require('../schema');
31
+
32
+ class SqliteAdapter extends BaseSqlAdapter
33
+ {
34
+ /**
35
+ * @constructor
36
+ * @param {object} options - Configuration options.
37
+ * @param {string} [options.filename=':memory:'] - Path to SQLite file, or ':memory:'.
38
+ * @param {boolean} [options.readonly=false] - Open database in read-only mode.
39
+ * @param {boolean} [options.fileMustExist=false] - Throw if the database file does not exist.
40
+ * @param {boolean} [options.verbose] - Log every SQL statement (debug).
41
+ * @param {boolean} [options.createDir=true] - Automatically create parent directories for the file.
42
+ * @param {object} [options.pragmas] - PRAGMA settings to apply on open.
43
+ * @param {string} [options.pragmas.journal_mode='WAL'] - Journal mode (WAL, DELETE, TRUNCATE, MEMORY, OFF).
44
+ * @param {string} [options.pragmas.foreign_keys='ON'] - Enforce foreign-key constraints.
45
+ * @param {string} [options.pragmas.busy_timeout='5000'] - Milliseconds to wait on a locked database.
46
+ * @param {string} [options.pragmas.synchronous='NORMAL'] - Sync mode (OFF, NORMAL, FULL, EXTRA).
47
+ * @param {string} [options.pragmas.cache_size='-64000'] - Page cache size (negative = KiB, e.g. -64000 = 64 MB).
48
+ * @param {string} [options.pragmas.temp_store='MEMORY'] - Temp tables in memory for speed.
49
+ * @param {string} [options.pragmas.mmap_size='268435456'] - Memory-mapped I/O size (256 MB).
50
+ * @param {string} [options.pragmas.page_size] - Page size in bytes (must be set before WAL).
51
+ * @param {string} [options.pragmas.auto_vacuum] - Auto-vacuum mode (NONE, FULL, INCREMENTAL).
52
+ * @param {string} [options.pragmas.secure_delete] - Overwrite deleted content with zeros.
53
+ * @param {string} [options.pragmas.wal_autocheckpoint] - Pages before auto-checkpoint (default 1000).
54
+ * @param {string} [options.pragmas.locking_mode] - NORMAL or EXCLUSIVE.
55
+ */
56
+ constructor(options = {})
57
+ {
58
+ super();
59
+ let Database;
60
+ try { Database = require('better-sqlite3'); }
61
+ catch (e)
62
+ {
63
+ throw new Error(
64
+ 'SQLite adapter requires "better-sqlite3" package.\n' +
65
+ 'Install it with: npm install better-sqlite3'
66
+ );
67
+ }
68
+
69
+ const filename = options.filename || ':memory:';
70
+
71
+ // Auto-create parent directories for file-based databases
72
+ if (filename !== ':memory:' && options.createDir !== false)
73
+ {
74
+ const dir = path.dirname(path.resolve(filename));
75
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
76
+ }
77
+
78
+ // Build better-sqlite3 constructor options
79
+ const dbOpts = {};
80
+ if (options.readonly) dbOpts.readonly = true;
81
+ if (options.fileMustExist) dbOpts.fileMustExist = true;
82
+ if (options.verbose) dbOpts.verbose = console.log;
83
+
84
+ this._db = new Database(filename, dbOpts);
85
+ this._filename = filename;
86
+
87
+ /** @private Prepared statement cache — avoids recompilation overhead */
88
+ this._stmtCache = new Map();
89
+ /** @private Maximum cached statements before LRU eviction */
90
+ this._stmtCacheMax = options.stmtCacheSize || 256;
91
+ /** @private Statement cache hit counter */
92
+ this._stmtCacheHits = 0;
93
+ /** @private Statement cache miss counter */
94
+ this._stmtCacheMisses = 0;
95
+
96
+ // Apply pragmas (with production-ready defaults)
97
+ const pragmas = {
98
+ journal_mode: 'WAL',
99
+ foreign_keys: 'ON',
100
+ busy_timeout: '5000',
101
+ synchronous: 'NORMAL',
102
+ cache_size: '-64000',
103
+ temp_store: 'MEMORY',
104
+ mmap_size: '268435456',
105
+ ...options.pragmas,
106
+ };
107
+ for (const [key, val] of Object.entries(pragmas))
108
+ this._db.pragma(`${key} = ${val}`);
109
+ }
110
+
111
+ // -- Statement Caching --------------------------------
112
+
113
+ /**
114
+ * Get or compile a prepared statement from cache.
115
+ * Uses LRU eviction when cache exceeds max size.
116
+ * @param {string} sql - SQL query string.
117
+ * @returns {Statement} Compiled prepared statement.
118
+ * @private
119
+ */
120
+ _prepare(sql)
121
+ {
122
+ let stmt = this._stmtCache.get(sql);
123
+ if (stmt)
124
+ {
125
+ this._stmtCacheHits++;
126
+ // LRU: move to end (most recently used)
127
+ this._stmtCache.delete(sql);
128
+ this._stmtCache.set(sql, stmt);
129
+ return stmt;
130
+ }
131
+
132
+ this._stmtCacheMisses++;
133
+ stmt = this._db.prepare(sql);
134
+ if (this._stmtCache.size >= this._stmtCacheMax)
135
+ {
136
+ const oldest = this._stmtCache.keys().next().value;
137
+ this._stmtCache.delete(oldest);
138
+ }
139
+ this._stmtCache.set(sql, stmt);
140
+ return stmt;
141
+ }
142
+
143
+ /**
144
+ * Get prepared statement cache statistics.
145
+ * @returns {{ size: number, maxSize: number, hits: number, misses: number, hitRate: number }}
146
+ */
147
+ stmtCacheStats()
148
+ {
149
+ const total = this._stmtCacheHits + this._stmtCacheMisses;
150
+ return {
151
+ size: this._stmtCache.size,
152
+ maxSize: this._stmtCacheMax,
153
+ hits: this._stmtCacheHits,
154
+ misses: this._stmtCacheMisses,
155
+ hitRate: total > 0 ? this._stmtCacheHits / total : 0,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Get the query execution plan (EXPLAIN QUERY PLAN).
161
+ * @param {object} descriptor - Query descriptor from the Query builder.
162
+ * @returns {Array<{ id: number, parent: number, notused: number, detail: string }>}
163
+ */
164
+ explain(descriptor)
165
+ {
166
+ const { table, fields, where, orderBy, limit, offset, distinct, joins, groupBy, having } = descriptor;
167
+
168
+ const selectFields = fields && fields.length
169
+ ? fields.map(f => `"${f}"`).join(', ')
170
+ : '*';
171
+ const distinctStr = distinct ? 'DISTINCT ' : '';
172
+ const joinStr = this._buildJoins(joins, table);
173
+ let sql = `SELECT ${distinctStr}${selectFields} FROM "${table}"${joinStr}`;
174
+
175
+ const values = [];
176
+ if (where && where.length > 0)
177
+ {
178
+ const { clause, values: wv } = this._buildWhereFromChain(where);
179
+ sql += clause;
180
+ values.push(...wv);
181
+ }
182
+
183
+ sql += this._buildGroupBy(groupBy);
184
+ sql += this._buildHaving(having, values);
185
+
186
+ if (orderBy && orderBy.length > 0)
187
+ sql += ' ORDER BY ' + orderBy.map(o => `"${o.field}" ${o.dir}`).join(', ');
188
+ if (limit !== null && limit !== undefined) { sql += ' LIMIT ?'; values.push(limit); }
189
+ if (offset !== null && offset !== undefined) { sql += ' OFFSET ?'; values.push(offset); }
190
+
191
+ return this._db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(...values);
192
+ }
193
+
194
+ /** @private @override */
195
+ _typeMap(colDef)
196
+ {
197
+ const map = {
198
+ string: 'TEXT', text: 'TEXT', integer: 'INTEGER', float: 'REAL',
199
+ boolean: 'INTEGER', date: 'TEXT', datetime: 'TEXT',
200
+ json: 'TEXT', blob: 'BLOB', uuid: 'TEXT',
201
+ bigint: 'INTEGER', smallint: 'INTEGER', tinyint: 'INTEGER',
202
+ decimal: 'REAL', double: 'REAL', real: 'REAL',
203
+ timestamp: 'TEXT', time: 'TEXT',
204
+ binary: 'BLOB', varbinary: 'BLOB',
205
+ char: 'TEXT', varchar: 'TEXT',
206
+ numeric: 'NUMERIC',
207
+ };
208
+ return map[colDef.type] || 'TEXT';
209
+ }
210
+
211
+ /**
212
+ * Create a table with the given schema.
213
+ * @param {string} table - Table name.
214
+ * @param {object} schema - Column definitions keyed by column name.
215
+ * @returns {Promise<void>}
216
+ */
217
+ async createTable(table, schema)
218
+ {
219
+ const cols = [];
220
+ const tableConstraints = [];
221
+ const compositePKs = [];
222
+
223
+ for (const [name, def] of Object.entries(schema))
224
+ {
225
+ let line = `"${name}" ${this._typeMap(def)}`;
226
+
227
+ // Collect composite PK candidates
228
+ if (def.primaryKey && def.compositeKey) { compositePKs.push(name); }
229
+ else if (def.primaryKey)
230
+ {
231
+ line += ' PRIMARY KEY';
232
+ if (def.autoIncrement) line += ' AUTOINCREMENT';
233
+ }
234
+
235
+ if (def.required && !def.primaryKey) line += ' NOT NULL';
236
+ if (def.unique && !def.compositeUnique) line += ' UNIQUE';
237
+ if (def.default !== undefined && typeof def.default !== 'function')
238
+ {
239
+ line += ` DEFAULT ${this._sqlDefault(def.default)}`;
240
+ }
241
+
242
+ // CHECK constraint
243
+ if (def.check)
244
+ {
245
+ line += ` CHECK(${validateCheck(def.check)})`;
246
+ }
247
+ else if (def.enum && def.type !== 'enum')
248
+ {
249
+ const vals = def.enum.map(v => `'${String(v).replace(/'/g, "''")}'`).join(', ');
250
+ line += ` CHECK("${name}" IN (${vals}))`;
251
+ }
252
+
253
+ // Foreign key (inline reference)
254
+ if (def.references)
255
+ {
256
+ line += ` REFERENCES "${def.references.table}"("${def.references.column || 'id'}")`;
257
+ if (def.references.onDelete) line += ` ON DELETE ${validateFKAction(def.references.onDelete)}`;
258
+ if (def.references.onUpdate) line += ` ON UPDATE ${validateFKAction(def.references.onUpdate)}`;
259
+ }
260
+
261
+ cols.push(line);
262
+ }
263
+
264
+ // Composite primary key
265
+ if (compositePKs.length > 0)
266
+ {
267
+ tableConstraints.push(`PRIMARY KEY (${compositePKs.map(k => `"${k}"`).join(', ')})`);
268
+ }
269
+
270
+ // Composite unique constraints
271
+ const compositeUniques = {};
272
+ for (const [name, def] of Object.entries(schema))
273
+ {
274
+ if (def.compositeUnique)
275
+ {
276
+ const group = typeof def.compositeUnique === 'string' ? def.compositeUnique : 'default';
277
+ if (!compositeUniques[group]) compositeUniques[group] = [];
278
+ compositeUniques[group].push(name);
279
+ }
280
+ }
281
+ for (const [, columns] of Object.entries(compositeUniques))
282
+ {
283
+ tableConstraints.push(`UNIQUE (${columns.map(c => `"${c}"`).join(', ')})`);
284
+ }
285
+
286
+ const allParts = [...cols, ...tableConstraints];
287
+ this._db.exec(`CREATE TABLE IF NOT EXISTS "${table}" (${allParts.join(', ')})`);
288
+
289
+ // Create indexes defined in schema
290
+ for (const [name, def] of Object.entries(schema))
291
+ {
292
+ if (def.index)
293
+ {
294
+ const idxName = typeof def.index === 'string' ? def.index : `idx_${table}_${name}`;
295
+ this._db.exec(`CREATE INDEX IF NOT EXISTS "${idxName}" ON "${table}" ("${name}")`);
296
+ }
297
+ }
298
+
299
+ // Composite indexes
300
+ const compositeIndexes = {};
301
+ for (const [name, def] of Object.entries(schema))
302
+ {
303
+ if (def.compositeIndex)
304
+ {
305
+ const group = typeof def.compositeIndex === 'string' ? def.compositeIndex : 'default';
306
+ if (!compositeIndexes[group]) compositeIndexes[group] = [];
307
+ compositeIndexes[group].push(name);
308
+ }
309
+ }
310
+ for (const [group, columns] of Object.entries(compositeIndexes))
311
+ {
312
+ const idxName = `idx_${table}_${group}`;
313
+ this._db.exec(`CREATE INDEX IF NOT EXISTS "${idxName}" ON "${table}" (${columns.map(c => `"${c}"`).join(', ')})`);
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Drop a table if it exists.
319
+ * @param {string} table - Table name.
320
+ * @returns {Promise<void>}
321
+ */
322
+ async dropTable(table)
323
+ {
324
+ this._db.exec(`DROP TABLE IF EXISTS "${table}"`);
325
+ }
326
+
327
+ /**
328
+ * Insert a single row.
329
+ * @param {string} table - Table name.
330
+ * @param {object} data - Row data as key-value pairs.
331
+ * @returns {Promise<object>} The inserted row.
332
+ */
333
+ async insert(table, data)
334
+ {
335
+ const keys = Object.keys(data);
336
+ const placeholders = keys.map(() => '?').join(', ');
337
+ const values = keys.map(k => this._toSqlValue(data[k]));
338
+ const sql = `INSERT INTO "${table}" (${keys.map(k => `"${k}"`).join(', ')}) VALUES (${placeholders})`;
339
+ const result = this._prepare(sql).run(...values);
340
+ return { ...data, id: result.lastInsertRowid };
341
+ }
342
+
343
+ /**
344
+ * Insert multiple rows in a batch.
345
+ * @param {string} table - Table name.
346
+ * @param {Array<object>} dataArray - Array of row objects.
347
+ * @returns {Promise<Array<object>>} The inserted rows.
348
+ */
349
+ async insertMany(table, dataArray)
350
+ {
351
+ if (!dataArray.length) return [];
352
+ const keys = Object.keys(dataArray[0]);
353
+ const placeholders = keys.map(() => '?').join(', ');
354
+ const sql = `INSERT INTO "${table}" (${keys.map(k => `"${k}"`).join(', ')}) VALUES (${placeholders})`;
355
+ const stmt = this._prepare(sql);
356
+ const results = [];
357
+ const runAll = this._db.transaction((items) => {
358
+ for (const data of items)
359
+ {
360
+ const values = keys.map(k => this._toSqlValue(data[k]));
361
+ const result = stmt.run(...values);
362
+ results.push({ ...data, id: result.lastInsertRowid });
363
+ }
364
+ });
365
+ runAll(dataArray);
366
+ return results;
367
+ }
368
+
369
+ /**
370
+ * Update a single row by primary key.
371
+ * @param {string} table - Table name.
372
+ * @param {string} pk - Primary key column.
373
+ * @param {*} pkVal - Primary key value.
374
+ * @param {object} data - Fields to update.
375
+ * @returns {Promise<void>}
376
+ */
377
+ async update(table, pk, pkVal, data)
378
+ {
379
+ const sets = Object.keys(data).map(k => `"${k}" = ?`).join(', ');
380
+ const values = [...Object.values(data).map(v => this._toSqlValue(v)), pkVal];
381
+ this._prepare(`UPDATE "${table}" SET ${sets} WHERE "${pk}" = ?`).run(...values);
382
+ }
383
+
384
+ /**
385
+ * Update rows matching the given conditions.
386
+ * @param {string} table - Table name.
387
+ * @param {object} conditions - Filter conditions.
388
+ * @param {object} data - Fields to update.
389
+ * @returns {Promise<number>} Number of affected rows.
390
+ */
391
+ async updateWhere(table, conditions, data)
392
+ {
393
+ const { clause, values: whereVals } = this._buildWhere(conditions);
394
+ const sets = Object.keys(data).map(k => `"${k}" = ?`).join(', ');
395
+ const values = [...Object.values(data).map(v => this._toSqlValue(v)), ...whereVals];
396
+ const result = this._prepare(`UPDATE "${table}" SET ${sets}${clause}`).run(...values);
397
+ return result.changes;
398
+ }
399
+
400
+ /**
401
+ * Delete a single row by primary key.
402
+ * @param {string} table - Table name.
403
+ * @param {string} pk - Primary key column.
404
+ * @param {*} pkVal - Primary key value.
405
+ * @returns {Promise<void>}
406
+ */
407
+ async remove(table, pk, pkVal)
408
+ {
409
+ this._prepare(`DELETE FROM "${table}" WHERE "${pk}" = ?`).run(pkVal);
410
+ }
411
+
412
+ /**
413
+ * Delete rows matching the given conditions.
414
+ * @param {string} table - Table name.
415
+ * @param {object} conditions - Filter conditions.
416
+ * @returns {Promise<number>} Number of deleted rows.
417
+ */
418
+ async deleteWhere(table, conditions)
419
+ {
420
+ const { clause, values } = this._buildWhere(conditions);
421
+ const result = this._prepare(`DELETE FROM "${table}"${clause}`).run(...values);
422
+ return result.changes;
423
+ }
424
+
425
+ /**
426
+ * Execute a query descriptor built by the Query builder.
427
+ * @param {object} descriptor - Query descriptor with table, fields, where, orderBy, limit, offset.
428
+ * @returns {Promise<Array<object>>} Result rows.
429
+ */
430
+ async execute(descriptor)
431
+ {
432
+ const { action, table, fields, where, orderBy, limit, offset, distinct, joins, groupBy, having } = descriptor;
433
+
434
+ if (action === 'count')
435
+ {
436
+ const { clause, values } = this._buildWhereFromChain(where);
437
+ const joinStr = this._buildJoins(joins, table);
438
+ const row = this._prepare(`SELECT COUNT(*) as count FROM "${table}"${joinStr}${clause}`).get(...values);
439
+ return row.count;
440
+ }
441
+
442
+ const selectFields = fields && fields.length
443
+ ? fields.map(f => `"${f}"`).join(', ')
444
+ : '*';
445
+ const distinctStr = distinct ? 'DISTINCT ' : '';
446
+ const joinStr = this._buildJoins(joins, table);
447
+ let sql = `SELECT ${distinctStr}${selectFields} FROM "${table}"${joinStr}`;
448
+
449
+ const values = [];
450
+ if (where && where.length > 0)
451
+ {
452
+ const { clause, values: wv } = this._buildWhereFromChain(where);
453
+ sql += clause;
454
+ values.push(...wv);
455
+ }
456
+
457
+ sql += this._buildGroupBy(groupBy);
458
+ sql += this._buildHaving(having, values);
459
+
460
+ if (orderBy && orderBy.length > 0)
461
+ {
462
+ sql += ' ORDER BY ' + orderBy.map(o => `"${o.field}" ${o.dir}`).join(', ');
463
+ }
464
+
465
+ if (limit !== null && limit !== undefined)
466
+ {
467
+ sql += ' LIMIT ?';
468
+ values.push(limit);
469
+ }
470
+
471
+ if (offset !== null && offset !== undefined)
472
+ {
473
+ sql += ' OFFSET ?';
474
+ values.push(offset);
475
+ }
476
+
477
+ return this._prepare(sql).all(...values);
478
+ }
479
+
480
+ /**
481
+ * Execute an aggregate function (count, sum, avg, min, max).
482
+ * @param {object} descriptor - Aggregate descriptor with table, function, field, where.
483
+ * @returns {Promise<number|null>} Aggregate result.
484
+ */
485
+ async aggregate(descriptor)
486
+ {
487
+ const { table, where, aggregateFn, aggregateField, joins, groupBy, having } = descriptor;
488
+ const fn = aggregateFn.toUpperCase();
489
+ const joinStr = this._buildJoins(joins, table);
490
+ const values = [];
491
+
492
+ let sql = `SELECT ${fn}("${aggregateField}") as result FROM "${table}"${joinStr}`;
493
+
494
+ if (where && where.length > 0)
495
+ {
496
+ const { clause, values: wv } = this._buildWhereFromChain(where);
497
+ sql += clause;
498
+ values.push(...wv);
499
+ }
500
+
501
+ sql += this._buildGroupBy(groupBy);
502
+ sql += this._buildHaving(having, values);
503
+
504
+ const row = this._prepare(sql).get(...values);
505
+ return row ? row.result : null;
506
+ }
507
+
508
+ // -- SQLite Utilities -----------------------------------------------
509
+
510
+ /**
511
+ * Ping the database to check connectivity.
512
+ * @returns {boolean} true if healthy.
513
+ */
514
+ ping()
515
+ {
516
+ try
517
+ {
518
+ this._db.prepare('SELECT 1').get();
519
+ return true;
520
+ }
521
+ catch
522
+ {
523
+ return false;
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Read a single PRAGMA value.
529
+ * @param {string} key - PRAGMA name (e.g. 'journal_mode').
530
+ * @returns {*} Current value.
531
+ */
532
+ pragma(key)
533
+ {
534
+ const rows = this._db.pragma(key);
535
+ if (Array.isArray(rows) && rows.length === 1) return Object.values(rows[0])[0];
536
+ return rows;
537
+ }
538
+
539
+ /**
540
+ * Force a WAL checkpoint (only useful in WAL mode).
541
+ * @param {'PASSIVE'|'FULL'|'RESTART'|'TRUNCATE'} [mode='PASSIVE'] - Operation mode.
542
+ * @returns {{ busy: number, log: number, checkpointed: number }}
543
+ */
544
+ checkpoint(mode = 'PASSIVE')
545
+ {
546
+ const allowed = ['PASSIVE', 'FULL', 'RESTART', 'TRUNCATE'];
547
+ const m = String(mode).toUpperCase();
548
+ if (!allowed.includes(m)) throw new Error(`Invalid checkpoint mode: ${mode}`);
549
+ const row = this._db.pragma(`wal_checkpoint(${m})`);
550
+ return Array.isArray(row) ? row[0] : row;
551
+ }
552
+
553
+ /**
554
+ * Run `PRAGMA integrity_check`.
555
+ * @returns {string} 'ok' if healthy, or a description of the problem.
556
+ */
557
+ integrity()
558
+ {
559
+ const rows = this._db.pragma('integrity_check');
560
+ const val = Array.isArray(rows) ? rows[0] : rows;
561
+ return (val && typeof val === 'object') ? Object.values(val)[0] : val;
562
+ }
563
+
564
+ /**
565
+ * Rebuild the database file, reclaiming free pages.
566
+ */
567
+ vacuum()
568
+ {
569
+ this._db.exec('VACUUM');
570
+ }
571
+
572
+ /**
573
+ * Get the size of the database file in bytes.
574
+ * Returns 0 for in-memory databases.
575
+ * @returns {number} File size in bytes.
576
+ */
577
+ fileSize()
578
+ {
579
+ if (this._filename === ':memory:') return 0;
580
+ try { return fs.statSync(path.resolve(this._filename)).size; }
581
+ catch { return 0; }
582
+ }
583
+
584
+ /**
585
+ * List all user-created tables.
586
+ * @returns {string[]} Array of table names.
587
+ */
588
+ tables()
589
+ {
590
+ const rows = this._db.prepare(
591
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
592
+ ).all();
593
+ return rows.map(r => r.name);
594
+ }
595
+
596
+ /**
597
+ * Close the database connection.
598
+ */
599
+ close()
600
+ {
601
+ this._db.close();
602
+ }
603
+
604
+ /**
605
+ * Run a raw SQL query.
606
+ * @param {string} sql - SQL query string.
607
+ * @param {...*} params - Bound parameter values.
608
+ * @returns {*} Query result rows.
609
+ */
610
+ raw(sql, ...params)
611
+ {
612
+ const stmt = this._db.prepare(sql);
613
+ return stmt.all(...params);
614
+ }
615
+
616
+ /**
617
+ * Begin a transaction.
618
+ * @param {Function} fn - Function to run inside the transaction.
619
+ * @returns {*} Return value of fn.
620
+ */
621
+ transaction(fn)
622
+ {
623
+ return this._db.transaction(fn)();
624
+ }
625
+
626
+ // -- Table Info & Debug (Schema Introspection) -----------
627
+
628
+ /**
629
+ * Get column information for a table.
630
+ * @param {string} table - Table name.
631
+ * @returns {Array<{ cid: number, name: string, type: string, notnull: boolean, defaultValue: *, pk: boolean }>}
632
+ */
633
+ columns(table)
634
+ {
635
+ const rows = this._db.pragma(`table_info("${table.replace(/"/g, '""')}")`);
636
+ return rows.map(r => ({
637
+ cid: r.cid, name: r.name, type: r.type,
638
+ notnull: !!r.notnull, defaultValue: r.dflt_value, pk: !!r.pk,
639
+ }));
640
+ }
641
+
642
+ /**
643
+ * Get indexes for a table.
644
+ * @param {string} table - Table name.
645
+ * @returns {Array<{ name: string, unique: boolean, columns: string[] }>}
646
+ */
647
+ indexes(table)
648
+ {
649
+ const idxList = this._db.pragma(`index_list("${table.replace(/"/g, '""')}")`);
650
+ return idxList.map(idx => {
651
+ const cols = this._db.pragma(`index_info("${idx.name.replace(/"/g, '""')}")`);
652
+ return {
653
+ name: idx.name, unique: !!idx.unique,
654
+ columns: cols.map(c => c.name),
655
+ };
656
+ });
657
+ }
658
+
659
+ /**
660
+ * Get foreign keys for a table.
661
+ * @param {string} table - Table name.
662
+ * @returns {Array<{ id: number, table: string, from: string, to: string, onUpdate: string, onDelete: string }>}
663
+ */
664
+ foreignKeys(table)
665
+ {
666
+ const rows = this._db.pragma(`foreign_key_list("${table.replace(/"/g, '""')}")`);
667
+ return rows.map(r => ({
668
+ id: r.id, table: r.table, from: r.from, to: r.to,
669
+ onUpdate: r.on_update, onDelete: r.on_delete,
670
+ }));
671
+ }
672
+
673
+ /**
674
+ * Get detailed table status (size estimates, row counts).
675
+ * @param {string} [table] - If omitted, returns all tables.
676
+ * @returns {Array<{ name: string, rows: number, pageCount: number }>}
677
+ */
678
+ tableStatus(table)
679
+ {
680
+ const names = table ? [table] : this.tables();
681
+ return names.map(name => {
682
+ const count = this._db.prepare(`SELECT COUNT(*) as count FROM "${name.replace(/"/g, '""')}"`).get();
683
+ return { name, rows: count.count };
684
+ });
685
+ }
686
+
687
+ /**
688
+ * Get counts for all tables — structured database overview.
689
+ * @returns {{ tables: Array<{ name: string, rows: number }>, totalRows: number, fileSize: string }}
690
+ */
691
+ overview()
692
+ {
693
+ const tables = this.tableStatus();
694
+ let totalRows = 0;
695
+ for (const t of tables) totalRows += t.rows;
696
+ const bytes = this.fileSize();
697
+ const fmt = (b) => {
698
+ if (b >= 1073741824) return (b / 1073741824).toFixed(2) + ' GB';
699
+ if (b >= 1048576) return (b / 1048576).toFixed(2) + ' MB';
700
+ if (b >= 1024) return (b / 1024).toFixed(2) + ' KB';
701
+ return b + ' B';
702
+ };
703
+ return { tables, totalRows, fileSize: fmt(bytes) };
704
+ }
705
+
706
+ /**
707
+ * Get the page size and page count (helps estimate table overhead).
708
+ * @returns {{ pageSize: number, pageCount: number, totalBytes: number }}
709
+ */
710
+ pageInfo()
711
+ {
712
+ const pageSize = this.pragma('page_size');
713
+ const pageCount = this.pragma('page_count');
714
+ return { pageSize, pageCount, totalBytes: pageSize * pageCount };
715
+ }
716
+
717
+ /**
718
+ * Get compile-time options that SQLite was built with.
719
+ * @returns {string[]} Array of compile option strings.
720
+ */
721
+ compileOptions()
722
+ {
723
+ return this._db.pragma('compile_options').map(r => Object.values(r)[0]);
724
+ }
725
+
726
+ /**
727
+ * Get the number of cached prepared statements.
728
+ * @returns {{ cached: number, max: number }}
729
+ */
730
+ cacheStatus()
731
+ {
732
+ return { cached: this._stmtCache.size, max: this._stmtCacheMax };
733
+ }
734
+
735
+ // -- Schema Migrations -----------------------------------------------
736
+
737
+ /**
738
+ * Add a column to an existing table.
739
+ * @param {string} table - Table name.
740
+ * @param {string} column - Column name.
741
+ * @param {object} colDef - Column definition (same format as schema entries).
742
+ */
743
+ addColumn(table, column, colDef)
744
+ {
745
+ let line = `"${column}" ${this._typeMap(colDef)}`;
746
+ if (colDef.required) line += ' NOT NULL';
747
+ if (colDef.unique) line += ' UNIQUE';
748
+ if (colDef.default !== undefined && typeof colDef.default !== 'function')
749
+ line += ` DEFAULT ${this._sqlDefault(colDef.default)}`;
750
+ if (colDef.check) line += ` CHECK(${validateCheck(colDef.check)})`;
751
+ if (colDef.references)
752
+ {
753
+ line += ` REFERENCES "${colDef.references.table}"("${colDef.references.column || 'id'}")`;
754
+ if (colDef.references.onDelete) line += ` ON DELETE ${validateFKAction(colDef.references.onDelete)}`;
755
+ if (colDef.references.onUpdate) line += ` ON UPDATE ${validateFKAction(colDef.references.onUpdate)}`;
756
+ }
757
+ this._db.exec(`ALTER TABLE "${table}" ADD COLUMN ${line}`);
758
+ }
759
+
760
+ /**
761
+ * Drop a column from an existing table.
762
+ * Requires SQLite 3.35.0+ (2021-03-12).
763
+ * @param {string} table - Table name.
764
+ * @param {string} column - Column name.
765
+ */
766
+ dropColumn(table, column)
767
+ {
768
+ this._db.exec(`ALTER TABLE "${table}" DROP COLUMN "${column}"`);
769
+ }
770
+
771
+ /**
772
+ * Rename a column in an existing table.
773
+ * Requires SQLite 3.25.0+ (2018-09-15).
774
+ * @param {string} table - Table name.
775
+ * @param {string} oldName - Current column name.
776
+ * @param {string} newName - New column name.
777
+ */
778
+ renameColumn(table, oldName, newName)
779
+ {
780
+ this._db.exec(`ALTER TABLE "${table}" RENAME COLUMN "${oldName}" TO "${newName}"`);
781
+ }
782
+
783
+ /**
784
+ * Rename a table.
785
+ * @param {string} oldName - Current table name.
786
+ * @param {string} newName - New table name.
787
+ */
788
+ renameTable(oldName, newName)
789
+ {
790
+ this._db.exec(`ALTER TABLE "${oldName}" RENAME TO "${newName}"`);
791
+ }
792
+
793
+ /**
794
+ * Create an index.
795
+ * @param {string} table - Table name.
796
+ * @param {string[]} columns - Column names.
797
+ * @param {object} [opts] - Options.
798
+ * @param {string} [opts.name] - Index name (auto-generated if omitted).
799
+ * @param {boolean} [opts.unique] - Create a UNIQUE index.
800
+ */
801
+ createIndex(table, columns, opts = {})
802
+ {
803
+ const name = opts.name || `idx_${table}_${columns.join('_')}`;
804
+ const unique = opts.unique ? 'UNIQUE ' : '';
805
+ this._db.exec(`CREATE ${unique}INDEX IF NOT EXISTS "${name}" ON "${table}" (${columns.map(c => `"${c}"`).join(', ')})`);
806
+ }
807
+
808
+ /**
809
+ * Drop an index.
810
+ * @param {string} _table - Table name (unused — SQLite indexes are schema-scoped).
811
+ * @param {string} name - Index name.
812
+ */
813
+ dropIndex(_table, name)
814
+ {
815
+ this._db.exec(`DROP INDEX IF EXISTS "${name}"`);
816
+ }
817
+
818
+ /**
819
+ * Check if a table exists.
820
+ * @param {string} table - Table name.
821
+ * @returns {boolean} `true` if the table exists.
822
+ */
823
+ hasTable(table)
824
+ {
825
+ const row = this._db.prepare(
826
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?"
827
+ ).get(table);
828
+ return !!row;
829
+ }
830
+
831
+ /**
832
+ * Check if a column exists in a table.
833
+ * @param {string} table - Table name.
834
+ * @param {string} column - Column name.
835
+ * @returns {boolean} `true` if the column exists.
836
+ */
837
+ hasColumn(table, column)
838
+ {
839
+ const cols = this.columns(table);
840
+ return cols.some(c => c.name === column);
841
+ }
842
+
843
+ /**
844
+ * Get a unified table description.
845
+ * @param {string} table - Table name.
846
+ * @returns {{ columns: Array, indexes: Array, foreignKeys: Array }}
847
+ */
848
+ describeTable(table)
849
+ {
850
+ return {
851
+ columns: this.columns(table),
852
+ indexes: this.indexes(table),
853
+ foreignKeys: this.foreignKeys(table),
854
+ };
855
+ }
856
+ }
857
+
858
+ module.exports = SqliteAdapter;