@zero-server/sdk 0.9.0 → 0.9.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/LICENSE +21 -21
- package/README.md +460 -437
- package/index.js +414 -412
- package/lib/app.js +1172 -1172
- package/lib/auth/authorize.js +399 -399
- package/lib/auth/enrollment.js +367 -367
- package/lib/auth/index.js +57 -57
- package/lib/auth/jwt.js +731 -731
- package/lib/auth/oauth.js +362 -362
- package/lib/auth/session.js +588 -588
- package/lib/auth/trustedDevice.js +409 -409
- package/lib/auth/twoFactor.js +1150 -1150
- package/lib/auth/webauthn.js +946 -946
- package/lib/body/index.js +14 -14
- package/lib/body/json.js +109 -109
- package/lib/body/multipart.js +440 -440
- package/lib/body/raw.js +71 -71
- package/lib/body/rawBuffer.js +160 -160
- package/lib/body/sendError.js +25 -25
- package/lib/body/text.js +75 -75
- package/lib/body/typeMatch.js +41 -41
- package/lib/body/urlencoded.js +235 -235
- package/lib/cli.js +845 -845
- package/lib/cluster.js +666 -666
- package/lib/debug.js +372 -372
- package/lib/env/index.js +460 -460
- package/lib/errors.js +683 -683
- package/lib/fetch/index.js +256 -256
- package/lib/grpc/balancer.js +378 -378
- package/lib/grpc/call.js +708 -708
- package/lib/grpc/client.js +764 -764
- package/lib/grpc/codec.js +1221 -1221
- package/lib/grpc/credentials.js +398 -398
- package/lib/grpc/frame.js +262 -262
- package/lib/grpc/health.js +287 -287
- package/lib/grpc/index.js +121 -121
- package/lib/grpc/metadata.js +461 -461
- package/lib/grpc/proto.js +821 -821
- package/lib/grpc/reflection.js +590 -590
- package/lib/grpc/server.js +445 -445
- package/lib/grpc/status.js +118 -118
- package/lib/grpc/watch.js +173 -173
- package/lib/http/index.js +10 -10
- package/lib/http/request.js +727 -727
- package/lib/http/response.js +799 -799
- package/lib/lifecycle.js +557 -557
- package/lib/middleware/compress.js +230 -230
- package/lib/middleware/cookieParser.js +237 -237
- package/lib/middleware/cors.js +93 -93
- package/lib/middleware/csrf.js +136 -136
- package/lib/middleware/errorHandler.js +101 -101
- package/lib/middleware/helmet.js +175 -175
- package/lib/middleware/index.js +19 -17
- package/lib/middleware/logger.js +74 -74
- package/lib/middleware/rateLimit.js +88 -88
- package/lib/middleware/requestId.js +53 -53
- package/lib/middleware/static.js +326 -326
- package/lib/middleware/timeout.js +71 -71
- package/lib/middleware/validator.js +254 -254
- package/lib/observe/health.js +326 -326
- package/lib/observe/index.js +50 -50
- package/lib/observe/logger.js +359 -359
- package/lib/observe/metrics.js +805 -805
- package/lib/observe/tracing.js +592 -592
- package/lib/orm/adapters/json.js +290 -290
- package/lib/orm/adapters/memory.js +764 -764
- package/lib/orm/adapters/mongo.js +764 -764
- package/lib/orm/adapters/mysql.js +933 -933
- package/lib/orm/adapters/postgres.js +1144 -1144
- package/lib/orm/adapters/redis.js +1534 -1534
- package/lib/orm/adapters/sql-base.js +212 -212
- package/lib/orm/adapters/sqlite.js +858 -858
- package/lib/orm/audit.js +649 -649
- package/lib/orm/cache.js +394 -394
- package/lib/orm/geo.js +387 -387
- package/lib/orm/index.js +784 -784
- package/lib/orm/migrate.js +432 -432
- package/lib/orm/model.js +1706 -1706
- package/lib/orm/plugin.js +375 -375
- package/lib/orm/procedures.js +836 -836
- package/lib/orm/profiler.js +233 -233
- package/lib/orm/query.js +1772 -1772
- package/lib/orm/replicas.js +241 -241
- package/lib/orm/schema.js +307 -307
- package/lib/orm/search.js +380 -380
- package/lib/orm/seed/data/commerce.js +136 -136
- package/lib/orm/seed/data/internet.js +111 -111
- package/lib/orm/seed/data/locations.js +204 -204
- package/lib/orm/seed/data/names.js +338 -338
- package/lib/orm/seed/data/person.js +128 -128
- package/lib/orm/seed/data/phone.js +211 -211
- package/lib/orm/seed/data/words.js +134 -134
- package/lib/orm/seed/factory.js +178 -178
- package/lib/orm/seed/fake.js +1186 -1186
- package/lib/orm/seed/index.js +18 -18
- package/lib/orm/seed/rng.js +70 -70
- package/lib/orm/seed/seeder.js +124 -124
- package/lib/orm/seed/unique.js +68 -68
- package/lib/orm/snapshot.js +366 -366
- package/lib/orm/tenancy.js +605 -605
- package/lib/orm/views.js +350 -350
- package/lib/router/index.js +436 -436
- package/lib/sse/index.js +8 -8
- package/lib/sse/stream.js +349 -349
- package/lib/ws/connection.js +451 -451
- package/lib/ws/handshake.js +125 -125
- package/lib/ws/index.js +14 -14
- package/lib/ws/room.js +223 -223
- package/package.json +73 -73
- package/types/app.d.ts +223 -223
- package/types/auth.d.ts +520 -520
- package/types/cluster.d.ts +75 -75
- package/types/env.d.ts +80 -80
- package/types/errors.d.ts +316 -316
- package/types/fetch.d.ts +43 -43
- package/types/grpc.d.ts +432 -432
- package/types/index.d.ts +384 -384
- package/types/lifecycle.d.ts +60 -60
- package/types/middleware.d.ts +320 -320
- package/types/observe.d.ts +304 -304
- package/types/orm.d.ts +1887 -1887
- package/types/request.d.ts +109 -109
- package/types/response.d.ts +157 -157
- package/types/router.d.ts +78 -78
- package/types/sse.d.ts +78 -78
- 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;
|