@atscript/db-sqlite 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { BaseDbAdapter } from "@atscript/utils-db";
1
+ import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace } from "@atscript/utils-db";
2
2
  import { walkFilter } from "@uniqu/core";
3
3
 
4
4
  //#region rolldown:runtime
@@ -7,6 +7,211 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  throw Error("Calling `require` for \"" + x + "\" in an environment that doesn't expose the `require` function.");
8
8
  });
9
9
 
10
+ //#endregion
11
+ //#region packages/db-sqlite/src/better-sqlite3-driver.ts
12
+ function _define_property$1(obj, key, value) {
13
+ if (key in obj) Object.defineProperty(obj, key, {
14
+ value,
15
+ enumerable: true,
16
+ configurable: true,
17
+ writable: true
18
+ });
19
+ else obj[key] = value;
20
+ return obj;
21
+ }
22
+ var BetterSqlite3Driver = class {
23
+ run(sql, params) {
24
+ const stmt = this.db.prepare(sql);
25
+ const result = params ? stmt.run(...params) : stmt.run();
26
+ return {
27
+ changes: result.changes,
28
+ lastInsertRowid: result.lastInsertRowid
29
+ };
30
+ }
31
+ all(sql, params) {
32
+ const stmt = this.db.prepare(sql);
33
+ return params ? stmt.all(...params) : stmt.all();
34
+ }
35
+ get(sql, params) {
36
+ const stmt = this.db.prepare(sql);
37
+ return (params ? stmt.get(...params) : stmt.get()) ?? null;
38
+ }
39
+ exec(sql) {
40
+ this.db.exec(sql);
41
+ }
42
+ close() {
43
+ this.db.close();
44
+ }
45
+ constructor(pathOrDb, options) {
46
+ _define_property$1(this, "db", void 0);
47
+ if (typeof pathOrDb === "string") {
48
+ const Database = __require("better-sqlite3");
49
+ this.db = new Database(pathOrDb, options);
50
+ } else this.db = pathOrDb;
51
+ }
52
+ };
53
+
54
+ //#endregion
55
+ //#region packages/db-sqlite/src/sql-builder.ts
56
+ function buildInsert(table, data) {
57
+ const keys = Object.keys(data);
58
+ const cols = keys.map((k) => `"${esc(k)}"`).join(", ");
59
+ const placeholders = keys.map(() => "?").join(", ");
60
+ return {
61
+ sql: `INSERT INTO "${esc(table)}" (${cols}) VALUES (${placeholders})`,
62
+ params: keys.map((k) => toSqliteValue(data[k]))
63
+ };
64
+ }
65
+ function buildSelect(table, where, controls) {
66
+ const cols = buildProjection(controls?.$select);
67
+ let sql = `SELECT ${cols} FROM "${esc(table)}" WHERE ${where.sql}`;
68
+ const params = [...where.params];
69
+ if (controls?.$sort) {
70
+ const orderParts = [];
71
+ for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`"${esc(col)}" ${dir === -1 ? "DESC" : "ASC"}`);
72
+ if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
73
+ }
74
+ if (controls?.$limit !== undefined) {
75
+ sql += ` LIMIT ?`;
76
+ params.push(controls.$limit);
77
+ }
78
+ if (controls?.$skip !== undefined) {
79
+ if (controls.$limit === undefined) sql += ` LIMIT -1`;
80
+ sql += ` OFFSET ?`;
81
+ params.push(controls.$skip);
82
+ }
83
+ return {
84
+ sql,
85
+ params
86
+ };
87
+ }
88
+ function buildUpdate(table, data, where) {
89
+ const setClauses = [];
90
+ const params = [];
91
+ for (const [key, value] of Object.entries(data)) {
92
+ setClauses.push(`"${esc(key)}" = ?`);
93
+ params.push(toSqliteValue(value));
94
+ }
95
+ return {
96
+ sql: `UPDATE "${esc(table)}" SET ${setClauses.join(", ")} WHERE ${where.sql}`,
97
+ params: [...params, ...where.params]
98
+ };
99
+ }
100
+ function buildDelete(table, where) {
101
+ return {
102
+ sql: `DELETE FROM "${esc(table)}" WHERE ${where.sql}`,
103
+ params: [...where.params]
104
+ };
105
+ }
106
+ function buildCreateTable(table, fields, foreignKeys) {
107
+ const colDefs = [];
108
+ const primaryKeys = fields.filter((f) => f.isPrimaryKey);
109
+ for (const field of fields) {
110
+ if (field.ignored) continue;
111
+ const sqlType = field.isPrimaryKey && (field.designType === "number" || field.designType === "integer") ? "INTEGER" : sqliteTypeFromDesignType(field.designType);
112
+ let def = `"${esc(field.physicalName)}" ${sqlType}`;
113
+ if (field.isPrimaryKey && primaryKeys.length === 1) def += " PRIMARY KEY";
114
+ if (!field.optional && !field.isPrimaryKey) def += " NOT NULL";
115
+ if (field.defaultValue?.kind === "value") def += ` DEFAULT ${sqlStringLiteral(field.defaultValue.value)}`;
116
+ colDefs.push(def);
117
+ }
118
+ if (primaryKeys.length > 1) {
119
+ const pkCols = primaryKeys.map((pk) => `"${esc(pk.physicalName)}"`).join(", ");
120
+ colDefs.push(`PRIMARY KEY (${pkCols})`);
121
+ }
122
+ if (foreignKeys) for (const fk of foreignKeys.values()) {
123
+ const localCols = fk.fields.map((f) => `"${esc(f)}"`).join(", ");
124
+ const targetCols = fk.targetFields.map((f) => `"${esc(f)}"`).join(", ");
125
+ let constraint = `FOREIGN KEY (${localCols}) REFERENCES "${esc(fk.targetTable)}" (${targetCols})`;
126
+ if (fk.onDelete) constraint += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
127
+ if (fk.onUpdate) constraint += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
128
+ colDefs.push(constraint);
129
+ }
130
+ return `CREATE TABLE IF NOT EXISTS "${esc(table)}" (${colDefs.join(", ")})`;
131
+ }
132
+ function refActionToSql(action) {
133
+ switch (action) {
134
+ case "cascade": return "CASCADE";
135
+ case "restrict": return "RESTRICT";
136
+ case "setNull": return "SET NULL";
137
+ case "setDefault": return "SET DEFAULT";
138
+ default: return "NO ACTION";
139
+ }
140
+ }
141
+ function sqliteTypeFromDesignType(designType) {
142
+ switch (designType) {
143
+ case "number":
144
+ case "integer": return "REAL";
145
+ case "boolean": return "INTEGER";
146
+ case "string": return "TEXT";
147
+ default: return "TEXT";
148
+ }
149
+ }
150
+ function buildProjection(select) {
151
+ const fields = select?.asArray;
152
+ if (!fields) return "*";
153
+ let sql = "";
154
+ for (let i = 0; i < fields.length; i++) {
155
+ if (i > 0) sql += ", ";
156
+ sql += `"${esc(fields[i])}"`;
157
+ }
158
+ return sql || "*";
159
+ }
160
+ function esc(name) {
161
+ return name.replace(/"/g, "\"\"");
162
+ }
163
+ function sqlStringLiteral(value) {
164
+ return `'${value.replace(/'/g, "''")}'`;
165
+ }
166
+ function toSqliteValue(value) {
167
+ if (value === undefined) return null;
168
+ if (value === null) return null;
169
+ if (typeof value === "object") return JSON.stringify(value);
170
+ if (typeof value === "boolean") return value ? 1 : 0;
171
+ return value;
172
+ }
173
+ function buildCreateView(viewName, plan, columns, resolveFieldRef) {
174
+ const selectCols = columns.map((c) => `"${esc(c.sourceTable)}"."${esc(c.sourceColumn)}" AS "${esc(c.viewColumn)}"`).join(", ");
175
+ let sql = `CREATE VIEW IF NOT EXISTS "${esc(viewName)}" AS SELECT ${selectCols} FROM "${esc(plan.entryTable)}"`;
176
+ for (const join of plan.joins) {
177
+ const onClause = queryNodeToSql(join.condition, resolveFieldRef);
178
+ sql += ` JOIN "${esc(join.targetTable)}" ON ${onClause}`;
179
+ }
180
+ if (plan.filter) {
181
+ const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
182
+ sql += ` WHERE ${whereClause}`;
183
+ }
184
+ return sql;
185
+ }
186
+ const queryOpToSql = {
187
+ $eq: "=",
188
+ $ne: "!=",
189
+ $gt: ">",
190
+ $gte: ">=",
191
+ $lt: "<",
192
+ $lte: "<="
193
+ };
194
+ /**
195
+ * Renders an AtscriptQueryNode tree to raw SQL (no parameters — for DDL use only).
196
+ */ function queryNodeToSql(node, resolveFieldRef) {
197
+ if ("$and" in node) {
198
+ const children = node.$and;
199
+ return children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" AND ");
200
+ }
201
+ if ("$or" in node) {
202
+ const children = node.$or;
203
+ return `(${children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" OR ")})`;
204
+ }
205
+ if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef)})`;
206
+ const comp = node;
207
+ const leftSql = resolveFieldRef(comp.left);
208
+ const sqlOp = queryOpToSql[comp.op] || "=";
209
+ if (comp.right && typeof comp.right === "object" && "field" in comp.right) return `${leftSql} ${sqlOp} ${resolveFieldRef(comp.right)}`;
210
+ if (comp.right === null || comp.right === undefined) return comp.op === "$ne" ? `${leftSql} IS NOT NULL` : `${leftSql} IS NULL`;
211
+ if (typeof comp.right === "string") return `${leftSql} ${sqlOp} '${comp.right.replace(/'/g, "''")}'`;
212
+ return `${leftSql} ${sqlOp} ${comp.right}`;
213
+ }
214
+
10
215
  //#endregion
11
216
  //#region packages/db-sqlite/src/filter-builder.ts
12
217
  const EMPTY_AND = {
@@ -22,7 +227,7 @@ const EMPTY_OR = {
22
227
  * into a parameterized SQL WHERE clause.
23
228
  */ const sqlVisitor = {
24
229
  comparison(field, op, value) {
25
- const col = `"${escapeIdent(field)}"`;
230
+ const col = `"${esc(field)}"`;
26
231
  const v = toSqliteParam(value);
27
232
  switch (op) {
28
233
  case "$eq": {
@@ -119,7 +324,7 @@ const EMPTY_OR = {
119
324
  };
120
325
  function buildWhere(filter) {
121
326
  if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
122
- return walkFilter(filter, sqlVisitor);
327
+ return walkFilter(filter, sqlVisitor) ?? EMPTY_AND;
123
328
  }
124
329
  /**
125
330
  * Basic regex-to-LIKE conversion.
@@ -141,12 +346,6 @@ function buildWhere(filter) {
141
346
  return `%${core}%`;
142
347
  }
143
348
  /**
144
- * Escapes a SQL identifier to prevent injection.
145
- * Doubles any embedded double-quotes.
146
- */ function escapeIdent(name) {
147
- return name.replace(/"/g, "\"\"");
148
- }
149
- /**
150
349
  * Converts a JS value to a SQLite-bindable parameter.
151
350
  * SQLite cannot bind booleans — they must be 0/1.
152
351
  */ function toSqliteParam(value) {
@@ -154,128 +353,9 @@ function buildWhere(filter) {
154
353
  return value;
155
354
  }
156
355
 
157
- //#endregion
158
- //#region packages/db-sqlite/src/sql-builder.ts
159
- function buildInsert(table, data) {
160
- const keys = Object.keys(data);
161
- const cols = keys.map((k) => `"${esc$1(k)}"`).join(", ");
162
- const placeholders = keys.map(() => "?").join(", ");
163
- return {
164
- sql: `INSERT INTO "${esc$1(table)}" (${cols}) VALUES (${placeholders})`,
165
- params: keys.map((k) => toSqliteValue$1(data[k]))
166
- };
167
- }
168
- function buildSelect(table, where, controls) {
169
- const cols = buildProjection(controls?.$select);
170
- let sql = `SELECT ${cols} FROM "${esc$1(table)}" WHERE ${where.sql}`;
171
- const params = [...where.params];
172
- if (controls?.$sort) {
173
- const orderParts = [];
174
- for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`"${esc$1(col)}" ${dir === -1 ? "DESC" : "ASC"}`);
175
- if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
176
- }
177
- if (controls?.$limit !== undefined) {
178
- sql += ` LIMIT ?`;
179
- params.push(controls.$limit);
180
- }
181
- if (controls?.$skip !== undefined) {
182
- if (controls.$limit === undefined) sql += ` LIMIT -1`;
183
- sql += ` OFFSET ?`;
184
- params.push(controls.$skip);
185
- }
186
- return {
187
- sql,
188
- params
189
- };
190
- }
191
- function buildUpdate(table, data, where) {
192
- const setClauses = [];
193
- const params = [];
194
- for (const [key, value] of Object.entries(data)) {
195
- setClauses.push(`"${esc$1(key)}" = ?`);
196
- params.push(toSqliteValue$1(value));
197
- }
198
- return {
199
- sql: `UPDATE "${esc$1(table)}" SET ${setClauses.join(", ")} WHERE ${where.sql}`,
200
- params: [...params, ...where.params]
201
- };
202
- }
203
- function buildDelete(table, where) {
204
- return {
205
- sql: `DELETE FROM "${esc$1(table)}" WHERE ${where.sql}`,
206
- params: [...where.params]
207
- };
208
- }
209
- function buildCreateTable(table, fields, foreignKeys) {
210
- const colDefs = [];
211
- const primaryKeys = fields.filter((f) => f.isPrimaryKey);
212
- for (const field of fields) {
213
- if (field.ignored) continue;
214
- const sqlType = field.isPrimaryKey && (field.designType === "number" || field.designType === "integer") ? "INTEGER" : sqliteTypeFromDesignType(field.designType);
215
- let def = `"${esc$1(field.physicalName)}" ${sqlType}`;
216
- if (field.isPrimaryKey && primaryKeys.length === 1) def += " PRIMARY KEY";
217
- if (!field.optional && !field.isPrimaryKey) def += " NOT NULL";
218
- colDefs.push(def);
219
- }
220
- if (primaryKeys.length > 1) {
221
- const pkCols = primaryKeys.map((pk) => `"${esc$1(pk.physicalName)}"`).join(", ");
222
- colDefs.push(`PRIMARY KEY (${pkCols})`);
223
- }
224
- if (foreignKeys) for (const fk of foreignKeys.values()) {
225
- const localCols = fk.fields.map((f) => `"${esc$1(f)}"`).join(", ");
226
- const targetCols = fk.targetFields.map((f) => `"${esc$1(f)}"`).join(", ");
227
- let constraint = `FOREIGN KEY (${localCols}) REFERENCES "${esc$1(fk.targetTable)}" (${targetCols})`;
228
- if (fk.onDelete) constraint += ` ON DELETE ${refActionToSql(fk.onDelete)}`;
229
- if (fk.onUpdate) constraint += ` ON UPDATE ${refActionToSql(fk.onUpdate)}`;
230
- colDefs.push(constraint);
231
- }
232
- return `CREATE TABLE IF NOT EXISTS "${esc$1(table)}" (${colDefs.join(", ")})`;
233
- }
234
- function refActionToSql(action) {
235
- switch (action) {
236
- case "cascade": return "CASCADE";
237
- case "restrict": return "RESTRICT";
238
- case "setNull": return "SET NULL";
239
- case "setDefault": return "SET DEFAULT";
240
- default: return "NO ACTION";
241
- }
242
- }
243
- function sqliteTypeFromDesignType(designType) {
244
- switch (designType) {
245
- case "number":
246
- case "integer": return "REAL";
247
- case "boolean": return "INTEGER";
248
- case "string": return "TEXT";
249
- default: return "TEXT";
250
- }
251
- }
252
- function buildProjection(select) {
253
- const fields = select?.asArray;
254
- if (!fields) return "*";
255
- let sql = "";
256
- for (let i = 0; i < fields.length; i++) {
257
- if (i > 0) sql += ", ";
258
- sql += `"${esc$1(fields[i])}"`;
259
- }
260
- return sql || "*";
261
- }
262
- function esc$1(name) {
263
- return name.replace(/"/g, "\"\"");
264
- }
265
- /**
266
- * Converts a JS value to a SQLite-compatible value.
267
- * Objects and arrays are stored as JSON strings.
268
- */ function toSqliteValue$1(value) {
269
- if (value === undefined) return null;
270
- if (value === null) return null;
271
- if (typeof value === "object") return JSON.stringify(value);
272
- if (typeof value === "boolean") return value ? 1 : 0;
273
- return value;
274
- }
275
-
276
356
  //#endregion
277
357
  //#region packages/db-sqlite/src/sqlite-adapter.ts
278
- function _define_property$1(obj, key, value) {
358
+ function _define_property(obj, key, value) {
279
359
  if (key in obj) Object.defineProperty(obj, key, {
280
360
  value,
281
361
  enumerable: true,
@@ -286,35 +366,73 @@ else obj[key] = value;
286
366
  return obj;
287
367
  }
288
368
  var SqliteAdapter = class extends BaseDbAdapter {
369
+ async _beginTransaction() {
370
+ this._log("BEGIN");
371
+ this.driver.exec("BEGIN");
372
+ return undefined;
373
+ }
374
+ async _commitTransaction() {
375
+ this._log("COMMIT");
376
+ this.driver.exec("COMMIT");
377
+ }
378
+ async _rollbackTransaction() {
379
+ this._log("ROLLBACK");
380
+ this.driver.exec("ROLLBACK");
381
+ }
289
382
  /** SQLite does not use schemas — override to always exclude schema. */ resolveTableName() {
290
383
  return super.resolveTableName(false);
291
384
  }
385
+ /** SQLite enforces FK constraints natively via PRAGMA foreign_keys. */ supportsNativeForeignKeys() {
386
+ return true;
387
+ }
292
388
  prepareId(id, _fieldType) {
293
389
  return id;
294
390
  }
391
+ /**
392
+ * Wraps a write operation to catch native SQLite constraint errors
393
+ * and rethrow as structured `DbError`.
394
+ */ _wrapConstraintError(fn) {
395
+ try {
396
+ return fn();
397
+ } catch (e) {
398
+ if (e instanceof Error) {
399
+ if (e.message.includes("FOREIGN KEY constraint failed")) throw new DbError("FK_VIOLATION", [{
400
+ path: "",
401
+ message: e.message
402
+ }]);
403
+ const uniqueMatch = e.message.match(/UNIQUE constraint failed:\s*\S+\.(\S+)/);
404
+ if (uniqueMatch) throw new DbError("CONFLICT", [{
405
+ path: uniqueMatch[1],
406
+ message: e.message
407
+ }]);
408
+ if (e.message.includes("UNIQUE constraint failed")) throw new DbError("CONFLICT", [{
409
+ path: "",
410
+ message: e.message
411
+ }]);
412
+ }
413
+ throw e;
414
+ }
415
+ }
295
416
  async insertOne(data) {
296
417
  const { sql, params } = buildInsert(this.resolveTableName(), data);
297
- const result = this.driver.run(sql, params);
418
+ this._log(sql, params);
419
+ const result = this._wrapConstraintError(() => this.driver.run(sql, params));
298
420
  return { insertedId: result.lastInsertRowid };
299
421
  }
300
422
  async insertMany(data) {
301
- const ids = [];
302
- this.driver.exec("BEGIN");
303
- try {
423
+ return this.withTransaction(async () => {
424
+ const ids = [];
304
425
  for (const row of data) {
305
426
  const { sql, params } = buildInsert(this.resolveTableName(), row);
306
- const result = this.driver.run(sql, params);
427
+ this._log(sql, params);
428
+ const result = this._wrapConstraintError(() => this.driver.run(sql, params));
307
429
  ids.push(result.lastInsertRowid);
308
430
  }
309
- this.driver.exec("COMMIT");
310
- } catch (error) {
311
- this.driver.exec("ROLLBACK");
312
- throw error;
313
- }
314
- return {
315
- insertedCount: ids.length,
316
- insertedIds: ids
317
- };
431
+ return {
432
+ insertedCount: ids.length,
433
+ insertedIds: ids
434
+ };
435
+ });
318
436
  }
319
437
  async findOne(query) {
320
438
  const where = buildWhere(query.filter);
@@ -323,17 +441,20 @@ var SqliteAdapter = class extends BaseDbAdapter {
323
441
  $limit: 1
324
442
  };
325
443
  const { sql, params } = buildSelect(this.resolveTableName(), where, controls);
444
+ this._log(sql, params);
326
445
  return this.driver.get(sql, params);
327
446
  }
328
447
  async findMany(query) {
329
448
  const where = buildWhere(query.filter);
330
449
  const { sql, params } = buildSelect(this.resolveTableName(), where, query.controls);
450
+ this._log(sql, params);
331
451
  return this.driver.all(sql, params);
332
452
  }
333
453
  async count(query) {
334
454
  const where = buildWhere(query.filter);
335
455
  const tableName = this.resolveTableName();
336
456
  const sql = `SELECT COUNT(*) as cnt FROM "${esc(tableName)}" WHERE ${where.sql}`;
457
+ this._log(sql, where.params);
337
458
  const row = this.driver.get(sql, where.params);
338
459
  return row?.cnt ?? 0;
339
460
  }
@@ -347,7 +468,9 @@ var SqliteAdapter = class extends BaseDbAdapter {
347
468
  setParams.push(toSqliteValue(value));
348
469
  }
349
470
  const sql = `UPDATE "${esc(tableName)}" SET ${setClauses.join(", ")} WHERE rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`;
350
- const result = this.driver.run(sql, [...setParams, ...where.params]);
471
+ const allParams = [...setParams, ...where.params];
472
+ this._log(sql, allParams);
473
+ const result = this._wrapConstraintError(() => this.driver.run(sql, allParams));
351
474
  return {
352
475
  matchedCount: result.changes,
353
476
  modifiedCount: result.changes
@@ -356,7 +479,8 @@ var SqliteAdapter = class extends BaseDbAdapter {
356
479
  async updateMany(filter, data) {
357
480
  const where = buildWhere(filter);
358
481
  const { sql, params } = buildUpdate(this.resolveTableName(), data, where);
359
- const result = this.driver.run(sql, params);
482
+ this._log(sql, params);
483
+ const result = this._wrapConstraintError(() => this.driver.run(sql, params));
360
484
  return {
361
485
  matchedCount: result.changes,
362
486
  modifiedCount: result.changes
@@ -365,28 +489,23 @@ var SqliteAdapter = class extends BaseDbAdapter {
365
489
  async replaceOne(filter, data) {
366
490
  const where = buildWhere(filter);
367
491
  const tableName = this.resolveTableName();
368
- this.driver.exec("BEGIN");
369
- try {
370
- const delSql = `DELETE FROM "${esc(tableName)}" WHERE rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`;
371
- const delResult = this.driver.run(delSql, where.params);
372
- if (delResult.changes > 0) {
373
- const { sql, params } = buildInsert(tableName, data);
374
- this.driver.run(sql, params);
375
- }
376
- this.driver.exec("COMMIT");
377
- return {
378
- matchedCount: delResult.changes,
379
- modifiedCount: delResult.changes
380
- };
381
- } catch (error) {
382
- this.driver.exec("ROLLBACK");
383
- throw error;
384
- }
492
+ const limitedWhere = {
493
+ sql: `rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`,
494
+ params: where.params
495
+ };
496
+ const { sql, params } = buildUpdate(tableName, data, limitedWhere);
497
+ this._log(sql, params);
498
+ const result = this._wrapConstraintError(() => this.driver.run(sql, params));
499
+ return {
500
+ matchedCount: result.changes,
501
+ modifiedCount: result.changes
502
+ };
385
503
  }
386
504
  async replaceMany(filter, data) {
387
505
  const where = buildWhere(filter);
388
506
  const { sql, params } = buildUpdate(this.resolveTableName(), data, where);
389
- const result = this.driver.run(sql, params);
507
+ this._log(sql, params);
508
+ const result = this._wrapConstraintError(() => this.driver.run(sql, params));
390
509
  return {
391
510
  matchedCount: result.changes,
392
511
  modifiedCount: result.changes
@@ -396,19 +515,140 @@ var SqliteAdapter = class extends BaseDbAdapter {
396
515
  const where = buildWhere(filter);
397
516
  const tableName = this.resolveTableName();
398
517
  const sql = `DELETE FROM "${esc(tableName)}" WHERE rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`;
399
- const result = this.driver.run(sql, where.params);
518
+ this._log(sql, where.params);
519
+ const result = this._wrapConstraintError(() => this.driver.run(sql, where.params));
400
520
  return { deletedCount: result.changes };
401
521
  }
402
522
  async deleteMany(filter) {
403
523
  const where = buildWhere(filter);
404
524
  const { sql, params } = buildDelete(this.resolveTableName(), where);
405
- const result = this.driver.run(sql, params);
525
+ this._log(sql, params);
526
+ const result = this._wrapConstraintError(() => this.driver.run(sql, params));
406
527
  return { deletedCount: result.changes };
407
528
  }
408
529
  async ensureTable() {
530
+ if (this._table instanceof AtscriptDbView) return this.ensureView();
409
531
  const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys);
532
+ this._log(sql);
533
+ this.driver.exec(sql);
534
+ }
535
+ async ensureView() {
536
+ const view = this._table;
537
+ const sql = buildCreateView(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref));
538
+ this._log(sql);
410
539
  this.driver.exec(sql);
411
540
  }
541
+ async getExistingColumns() {
542
+ return this.getExistingColumnsForTable(this.resolveTableName());
543
+ }
544
+ async syncColumns(diff) {
545
+ const tableName = this.resolveTableName();
546
+ const added = [];
547
+ const renamed = [];
548
+ for (const { field, oldName } of diff.renamed ?? []) {
549
+ const ddl = `ALTER TABLE "${esc(tableName)}" RENAME COLUMN "${esc(oldName)}" TO "${esc(field.physicalName)}"`;
550
+ this._log(ddl);
551
+ this.driver.exec(ddl);
552
+ renamed.push(field.physicalName);
553
+ }
554
+ for (const field of diff.added) {
555
+ const sqlType = this.typeMapper(field);
556
+ let ddl = `ALTER TABLE "${esc(tableName)}" ADD COLUMN "${esc(field.physicalName)}" ${sqlType}`;
557
+ if (!field.optional && !field.isPrimaryKey) ddl += " NOT NULL";
558
+ if (field.defaultValue?.kind === "value") ddl += ` DEFAULT ${sqlStringLiteral(field.defaultValue.value)}`;
559
+ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValueForType(field.designType)}`;
560
+ this._log(ddl);
561
+ this.driver.exec(ddl);
562
+ added.push(field.physicalName);
563
+ }
564
+ return {
565
+ added,
566
+ renamed
567
+ };
568
+ }
569
+ async recreateTable() {
570
+ const tableName = this.resolveTableName();
571
+ const tempName = `${tableName}__tmp_${Date.now()}`;
572
+ this.driver.exec("PRAGMA foreign_keys = OFF");
573
+ this.driver.exec("PRAGMA legacy_alter_table = ON");
574
+ try {
575
+ const createSql = buildCreateTable(tempName, this._table.fieldDescriptors, this._table.foreignKeys);
576
+ this._log(createSql);
577
+ this.driver.exec(createSql);
578
+ const oldCols = (await this.getExistingColumns()).map((c) => c.name);
579
+ const newCols = this._table.fieldDescriptors.filter((f) => !f.ignored).map((f) => f.physicalName);
580
+ const oldColSet = new Set(oldCols);
581
+ const commonCols = newCols.filter((c) => oldColSet.has(c));
582
+ if (commonCols.length > 0) {
583
+ const fieldsByName = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
584
+ const colNames = commonCols.map((c) => `"${esc(c)}"`).join(", ");
585
+ const selectExprs = commonCols.map((c) => {
586
+ const field = fieldsByName.get(c);
587
+ if (field && !field.optional && !field.isPrimaryKey) {
588
+ const fallback = field.defaultValue?.kind === "value" ? sqlStringLiteral(field.defaultValue.value) : defaultValueForType(field.designType);
589
+ return `COALESCE("${esc(c)}", ${fallback}) AS "${esc(c)}"`;
590
+ }
591
+ return `"${esc(c)}"`;
592
+ }).join(", ");
593
+ const copySql = `INSERT INTO "${esc(tempName)}" (${colNames}) SELECT ${selectExprs} FROM "${esc(tableName)}"`;
594
+ this._log(copySql);
595
+ this.driver.exec(copySql);
596
+ }
597
+ const oldName = `${tableName}__old_${Date.now()}`;
598
+ this.driver.exec(`ALTER TABLE "${esc(tableName)}" RENAME TO "${esc(oldName)}"`);
599
+ this.driver.exec(`ALTER TABLE "${esc(tempName)}" RENAME TO "${esc(tableName)}"`);
600
+ this.driver.exec(`DROP TABLE IF EXISTS "${esc(oldName)}"`);
601
+ } finally {
602
+ this.driver.exec("PRAGMA legacy_alter_table = OFF");
603
+ this.driver.exec("PRAGMA foreign_keys = ON");
604
+ }
605
+ }
606
+ async dropTable() {
607
+ const tableName = this.resolveTableName();
608
+ const ddl = `DROP TABLE IF EXISTS "${esc(tableName)}"`;
609
+ this._log(ddl);
610
+ this.driver.exec(ddl);
611
+ }
612
+ async dropColumns(columns) {
613
+ await this.withTransaction(async () => {
614
+ const tableName = this.resolveTableName();
615
+ for (const col of columns) {
616
+ const ddl = `ALTER TABLE "${esc(tableName)}" DROP COLUMN "${esc(col)}"`;
617
+ this._log(ddl);
618
+ this.driver.exec(ddl);
619
+ }
620
+ });
621
+ }
622
+ async dropTableByName(tableName) {
623
+ const ddl = `DROP TABLE IF EXISTS "${esc(tableName)}"`;
624
+ this._log(ddl);
625
+ this.driver.exec(ddl);
626
+ }
627
+ async dropViewByName(viewName) {
628
+ const ddl = `DROP VIEW IF EXISTS "${esc(viewName)}"`;
629
+ this._log(ddl);
630
+ this.driver.exec(ddl);
631
+ }
632
+ async renameTable(oldName) {
633
+ const newName = this.resolveTableName();
634
+ const ddl = `ALTER TABLE "${esc(oldName)}" RENAME TO "${esc(newName)}"`;
635
+ this._log(ddl);
636
+ this.driver.exec(ddl);
637
+ }
638
+ typeMapper(field) {
639
+ if (field.isPrimaryKey && (field.designType === "number" || field.designType === "integer")) return "INTEGER";
640
+ return sqliteTypeFromDesignType(field.designType);
641
+ }
642
+ async getExistingColumnsForTable(tableName) {
643
+ const rows = this.driver.all(`PRAGMA table_info("${esc(tableName)}")`);
644
+ return rows.map((r) => ({
645
+ name: r.name,
646
+ type: r.type,
647
+ notnull: r.notnull === 1,
648
+ pk: r.pk > 0,
649
+ dflt_value: normalizeSqliteDefault(r.dflt_value)
650
+ }));
651
+ }
412
652
  async syncIndexes() {
413
653
  const tableName = this.resolveTableName();
414
654
  await this.syncIndexesWithDiff({
@@ -416,73 +656,44 @@ var SqliteAdapter = class extends BaseDbAdapter {
416
656
  createIndex: async (index) => {
417
657
  const unique = index.type === "unique" ? "UNIQUE " : "";
418
658
  const cols = index.fields.map((f) => `"${esc(f.name)}" ${f.sort === "desc" ? "DESC" : "ASC"}`).join(", ");
419
- this.driver.exec(`CREATE ${unique}INDEX IF NOT EXISTS "${esc(index.key)}" ON "${esc(tableName)}" (${cols})`);
659
+ const sql = `CREATE ${unique}INDEX IF NOT EXISTS "${esc(index.key)}" ON "${esc(tableName)}" (${cols})`;
660
+ this._log(sql);
661
+ this.driver.exec(sql);
420
662
  },
421
663
  dropIndex: async (name) => {
422
- this.driver.exec(`DROP INDEX IF EXISTS "${esc(name)}"`);
664
+ const sql = `DROP INDEX IF EXISTS "${esc(name)}"`;
665
+ this._log(sql);
666
+ this.driver.exec(sql);
423
667
  },
424
668
  shouldSkipType: (type) => type === "fulltext"
425
669
  });
426
670
  }
427
671
  constructor(driver) {
428
- super(), _define_property$1(this, "driver", void 0), this.driver = driver;
429
- driver.exec("PRAGMA foreign_keys = ON");
672
+ super(), _define_property(this, "driver", void 0), this.driver = driver;
673
+ this.driver.exec("PRAGMA foreign_keys = ON");
430
674
  }
431
675
  };
432
- function esc(name) {
433
- return name.replace(/"/g, "\"\"");
676
+ /** Returns a safe SQLite DEFAULT literal for a given design type. */ function defaultValueForType(designType) {
677
+ switch (designType) {
678
+ case "number":
679
+ case "integer": return "0";
680
+ case "boolean": return "0";
681
+ default: return "''";
682
+ }
434
683
  }
435
- function toSqliteValue(value) {
436
- if (value === undefined) return null;
437
- if (value === null) return null;
438
- if (typeof value === "object") return JSON.stringify(value);
439
- if (typeof value === "boolean") return value ? 1 : 0;
684
+ /** Normalizes SQLite PRAGMA dflt_value to match serialized format.
685
+ * PRAGMA returns `'active'` (SQL-quoted), we store `active` (raw). */ function normalizeSqliteDefault(value) {
686
+ if (value == null) return undefined;
687
+ if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
440
688
  return value;
441
689
  }
442
690
 
443
691
  //#endregion
444
- //#region packages/db-sqlite/src/better-sqlite3-driver.ts
445
- function _define_property(obj, key, value) {
446
- if (key in obj) Object.defineProperty(obj, key, {
447
- value,
448
- enumerable: true,
449
- configurable: true,
450
- writable: true
451
- });
452
- else obj[key] = value;
453
- return obj;
692
+ //#region packages/db-sqlite/src/index.ts
693
+ function createAdapter(connection, options) {
694
+ const driver = new BetterSqlite3Driver(connection, options);
695
+ return new DbSpace(() => new SqliteAdapter(driver));
454
696
  }
455
- var BetterSqlite3Driver = class {
456
- run(sql, params) {
457
- const stmt = this.db.prepare(sql);
458
- const result = params ? stmt.run(...params) : stmt.run();
459
- return {
460
- changes: result.changes,
461
- lastInsertRowid: result.lastInsertRowid
462
- };
463
- }
464
- all(sql, params) {
465
- const stmt = this.db.prepare(sql);
466
- return params ? stmt.all(...params) : stmt.all();
467
- }
468
- get(sql, params) {
469
- const stmt = this.db.prepare(sql);
470
- return (params ? stmt.get(...params) : stmt.get()) ?? null;
471
- }
472
- exec(sql) {
473
- this.db.exec(sql);
474
- }
475
- close() {
476
- this.db.close();
477
- }
478
- constructor(pathOrDb, options) {
479
- _define_property(this, "db", void 0);
480
- if (typeof pathOrDb === "string") {
481
- const Database = __require("better-sqlite3");
482
- this.db = new Database(pathOrDb, options);
483
- } else this.db = pathOrDb;
484
- }
485
- };
486
697
 
487
698
  //#endregion
488
- export { BetterSqlite3Driver, SqliteAdapter, buildWhere };
699
+ export { BetterSqlite3Driver, SqliteAdapter, buildWhere, createAdapter };