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