@atscript/db-sqlite 0.1.37 → 0.1.39

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,13 +1,7 @@
1
- import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace } from "@atscript/utils-db";
2
- import { walkFilter } from "@uniqu/core";
1
+ import { AtscriptDbView, BaseDbAdapter, DbError, DbSpace } from "@atscript/db";
2
+ import { createRequire } from "node:module";
3
+ import { buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildSelect, buildUpdate, buildWhere as buildWhere$1, defaultValueForType, defaultValueToSqlLiteral, refActionToSql, toSqlValue } from "@atscript/db-sql-tools";
3
4
 
4
- //#region rolldown:runtime
5
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) {
6
- if (typeof require !== "undefined") return require.apply(this, arguments);
7
- throw Error("Calling `require` for \"" + x + "\" in an environment that doesn't expose the `require` function.");
8
- });
9
-
10
- //#endregion
11
5
  //#region packages/db-sqlite/src/better-sqlite3-driver.ts
12
6
  function _define_property$1(obj, key, value) {
13
7
  if (key in obj) Object.defineProperty(obj, key, {
@@ -45,7 +39,8 @@ var BetterSqlite3Driver = class {
45
39
  constructor(pathOrDb, options) {
46
40
  _define_property$1(this, "db", void 0);
47
41
  if (typeof pathOrDb === "string") {
48
- const Database = __require("better-sqlite3");
42
+ const req = createRequire(import.meta.url);
43
+ const Database = req("better-sqlite3");
49
44
  this.db = new Database(pathOrDb, options);
50
45
  } else this.db = pathOrDb;
51
46
  }
@@ -53,55 +48,80 @@ var BetterSqlite3Driver = class {
53
48
 
54
49
  //#endregion
55
50
  //#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
- };
51
+ function esc(name) {
52
+ return name.replace(/"/g, "\"\"");
64
53
  }
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
- };
54
+ /**
55
+ * Basic regex-to-LIKE conversion.
56
+ * - `^abc` `abc%`
57
+ * - `abc$` → `%abc`
58
+ * - `^abc$` → `abc`
59
+ * - `abc` → `%abc%`
60
+ */ function regexToLike(pattern) {
61
+ const hasStart = pattern.startsWith("^");
62
+ const hasEnd = pattern.endsWith("$");
63
+ let core = pattern;
64
+ if (hasStart) core = core.slice(1);
65
+ if (hasEnd) core = core.slice(0, -1);
66
+ core = core.replace(/%/g, "\\%").replace(/_/g, "\\_");
67
+ core = core.replace(/\.\*/g, "%").replace(/\./g, "_");
68
+ if (hasStart && hasEnd) return core;
69
+ if (hasStart) return `${core}%`;
70
+ if (hasEnd) return `%${core}`;
71
+ return `%${core}%`;
72
+ }
73
+ const sqliteDialect = {
74
+ quoteIdentifier(name) {
75
+ return `"${esc(name)}"`;
76
+ },
77
+ quoteTable(name) {
78
+ return `"${esc(name)}"`;
79
+ },
80
+ unlimitedLimit: "-1",
81
+ toValue: toSqlValue,
82
+ toParam(value) {
83
+ if (value === undefined) return null;
84
+ return typeof value === "boolean" ? value ? 1 : 0 : value;
85
+ },
86
+ regex(quotedCol, value) {
87
+ const pattern = regexToLike(value instanceof RegExp ? value.source : String(value));
88
+ return {
89
+ sql: `${quotedCol} LIKE ?`,
90
+ params: [pattern]
91
+ };
92
+ },
93
+ createViewPrefix: "CREATE VIEW IF NOT EXISTS"
94
+ };
95
+ function buildInsert$1(table, data) {
96
+ return buildInsert(sqliteDialect, table, data);
87
97
  }
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
- };
98
+ function buildSelect$1(table, where, controls) {
99
+ return buildSelect(sqliteDialect, table, where, controls);
99
100
  }
100
- function buildDelete(table, where) {
101
- return {
102
- sql: `DELETE FROM "${esc(table)}" WHERE ${where.sql}`,
103
- params: [...where.params]
104
- };
101
+ function buildUpdate$1(table, data, where) {
102
+ return buildUpdate(sqliteDialect, table, data, where);
103
+ }
104
+ function buildDelete$1(table, where) {
105
+ return buildDelete(sqliteDialect, table, where);
106
+ }
107
+ function buildCreateView$1(viewName, plan, columns, resolveFieldRef) {
108
+ return buildCreateView(sqliteDialect, viewName, plan, columns, resolveFieldRef);
109
+ }
110
+ function buildAggregateSelect$1(table, where, controls) {
111
+ return buildAggregateSelect(sqliteDialect, table, where, controls);
112
+ }
113
+ function buildAggregateCount$1(table, where, controls) {
114
+ return buildAggregateCount(sqliteDialect, table, where, controls);
115
+ }
116
+ function sqliteTypeFromDesignType(designType) {
117
+ switch (designType) {
118
+ case "number":
119
+ case "integer":
120
+ case "decimal": return "REAL";
121
+ case "boolean": return "INTEGER";
122
+ case "string": return "TEXT";
123
+ default: return "TEXT";
124
+ }
105
125
  }
106
126
  function buildCreateTable(table, fields, foreignKeys) {
107
127
  const colDefs = [];
@@ -110,9 +130,13 @@ function buildCreateTable(table, fields, foreignKeys) {
110
130
  if (field.ignored) continue;
111
131
  const sqlType = field.isPrimaryKey && (field.designType === "number" || field.designType === "integer") ? "INTEGER" : sqliteTypeFromDesignType(field.designType);
112
132
  let def = `"${esc(field.physicalName)}" ${sqlType}`;
113
- if (field.isPrimaryKey && primaryKeys.length === 1) def += " PRIMARY KEY";
133
+ if (field.isPrimaryKey && primaryKeys.length === 1) {
134
+ def += " PRIMARY KEY";
135
+ if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment" && (field.designType === "number" || field.designType === "integer")) def += " AUTOINCREMENT";
136
+ }
114
137
  if (!field.optional && !field.isPrimaryKey) def += " NOT NULL";
115
- if (field.defaultValue?.kind === "value") def += ` DEFAULT ${sqlStringLiteral(field.defaultValue.value)}`;
138
+ if (field.defaultValue?.kind === "value") def += ` DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
139
+ if (field.collate) def += ` COLLATE ${field.collate.toUpperCase()}`;
116
140
  colDefs.push(def);
117
141
  }
118
142
  if (primaryKeys.length > 1) {
@@ -129,228 +153,19 @@ function buildCreateTable(table, fields, foreignKeys) {
129
153
  }
130
154
  return `CREATE TABLE IF NOT EXISTS "${esc(table)}" (${colDefs.join(", ")})`;
131
155
  }
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
156
 
215
157
  //#endregion
216
158
  //#region packages/db-sqlite/src/filter-builder.ts
217
- const EMPTY_AND = {
218
- sql: "1=1",
219
- params: []
220
- };
221
- const EMPTY_OR = {
222
- sql: "0=1",
223
- params: []
224
- };
225
- /**
226
- * SQL visitor for `walkFilter` — renders a filter expression tree
227
- * into a parameterized SQL WHERE clause.
228
- */ const sqlVisitor = {
229
- comparison(field, op, value) {
230
- const col = `"${esc(field)}"`;
231
- const v = toSqliteParam(value);
232
- switch (op) {
233
- case "$eq": {
234
- if (v === null) return {
235
- sql: `${col} IS NULL`,
236
- params: []
237
- };
238
- return {
239
- sql: `${col} = ?`,
240
- params: [v]
241
- };
242
- }
243
- case "$ne": {
244
- if (v === null) return {
245
- sql: `${col} IS NOT NULL`,
246
- params: []
247
- };
248
- return {
249
- sql: `${col} != ?`,
250
- params: [v]
251
- };
252
- }
253
- case "$gt": return {
254
- sql: `${col} > ?`,
255
- params: [v]
256
- };
257
- case "$gte": return {
258
- sql: `${col} >= ?`,
259
- params: [v]
260
- };
261
- case "$lt": return {
262
- sql: `${col} < ?`,
263
- params: [v]
264
- };
265
- case "$lte": return {
266
- sql: `${col} <= ?`,
267
- params: [v]
268
- };
269
- case "$in": {
270
- const arr = value.map(toSqliteParam);
271
- if (arr.length === 0) return EMPTY_OR;
272
- const placeholders = arr.map(() => "?").join(", ");
273
- return {
274
- sql: `${col} IN (${placeholders})`,
275
- params: [...arr]
276
- };
277
- }
278
- case "$nin": {
279
- const arr = value.map(toSqliteParam);
280
- if (arr.length === 0) return EMPTY_AND;
281
- const placeholders = arr.map(() => "?").join(", ");
282
- return {
283
- sql: `${col} NOT IN (${placeholders})`,
284
- params: [...arr]
285
- };
286
- }
287
- case "$exists": return value ? {
288
- sql: `${col} IS NOT NULL`,
289
- params: []
290
- } : {
291
- sql: `${col} IS NULL`,
292
- params: []
293
- };
294
- case "$regex": {
295
- const pattern = regexToLike(value instanceof RegExp ? value.source : String(value));
296
- return {
297
- sql: `${col} LIKE ?`,
298
- params: [pattern]
299
- };
300
- }
301
- default: throw new Error(`Unsupported filter operator: ${op}`);
302
- }
303
- },
304
- and(children) {
305
- if (children.length === 0) return EMPTY_AND;
306
- return {
307
- sql: children.map((c) => c.sql).join(" AND "),
308
- params: children.flatMap((c) => c.params)
309
- };
310
- },
311
- or(children) {
312
- if (children.length === 0) return EMPTY_OR;
313
- return {
314
- sql: `(${children.map((c) => c.sql).join(" OR ")})`,
315
- params: children.flatMap((c) => c.params)
316
- };
317
- },
318
- not(child) {
319
- return {
320
- sql: `NOT (${child.sql})`,
321
- params: child.params
322
- };
323
- }
324
- };
325
159
  function buildWhere(filter) {
326
- if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
327
- return walkFilter(filter, sqlVisitor) ?? EMPTY_AND;
160
+ return buildWhere$1(sqliteDialect, filter);
328
161
  }
329
- /**
330
- * Basic regex-to-LIKE conversion.
331
- * - `^abc` → `abc%`
332
- * - `abc$` → `%abc`
333
- * - `^abc$` → `abc`
334
- * - `abc` → `%abc%`
335
- */ function regexToLike(pattern) {
336
- const hasStart = pattern.startsWith("^");
337
- const hasEnd = pattern.endsWith("$");
338
- let core = pattern;
339
- if (hasStart) core = core.slice(1);
340
- if (hasEnd) core = core.slice(0, -1);
341
- core = core.replace(/%/g, "\\%").replace(/_/g, "\\_");
342
- core = core.replace(/\.\*/g, "%").replace(/\./g, "_");
343
- if (hasStart && hasEnd) return core;
344
- if (hasStart) return `${core}%`;
345
- if (hasEnd) return `%${core}`;
346
- return `%${core}%`;
347
- }
348
- /**
349
- * Converts a JS value to a SQLite-bindable parameter.
350
- * SQLite cannot bind booleans — they must be 0/1.
351
- */ function toSqliteParam(value) {
352
- if (typeof value === "boolean") return value ? 1 : 0;
353
- return value;
162
+ function buildPrefixedWhere(alias, filter) {
163
+ return buildWhere$1({
164
+ ...sqliteDialect,
165
+ quoteIdentifier(name) {
166
+ return `${alias}."${esc(name)}"`;
167
+ }
168
+ }, filter);
354
169
  }
355
170
 
356
171
  //#endregion
@@ -366,6 +181,9 @@ else obj[key] = value;
366
181
  return obj;
367
182
  }
368
183
  var SqliteAdapter = class extends BaseDbAdapter {
184
+ supportsNativeValueDefaults() {
185
+ return true;
186
+ }
369
187
  async _beginTransaction() {
370
188
  this._log("BEGIN");
371
189
  this.driver.exec("BEGIN");
@@ -394,39 +212,39 @@ var SqliteAdapter = class extends BaseDbAdapter {
394
212
  */ _wrapConstraintError(fn) {
395
213
  try {
396
214
  return fn();
397
- } catch (e) {
398
- if (e instanceof Error) {
399
- if (e.message.includes("FOREIGN KEY constraint failed")) throw new DbError("FK_VIOLATION", [{
215
+ } catch (error) {
216
+ if (error instanceof Error) {
217
+ if (error.message.includes("FOREIGN KEY constraint failed")) throw new DbError("FK_VIOLATION", [{
400
218
  path: "",
401
- message: e.message
219
+ message: error.message
402
220
  }]);
403
- const uniqueMatch = e.message.match(/UNIQUE constraint failed:\s*\S+\.(\S+)/);
221
+ const uniqueMatch = error.message.match(/UNIQUE constraint failed:\s*\S+\.(\S+)/);
404
222
  if (uniqueMatch) throw new DbError("CONFLICT", [{
405
223
  path: uniqueMatch[1],
406
- message: e.message
224
+ message: error.message
407
225
  }]);
408
- if (e.message.includes("UNIQUE constraint failed")) throw new DbError("CONFLICT", [{
226
+ if (error.message.includes("UNIQUE constraint failed")) throw new DbError("CONFLICT", [{
409
227
  path: "",
410
- message: e.message
228
+ message: error.message
411
229
  }]);
412
230
  }
413
- throw e;
231
+ throw error;
414
232
  }
415
233
  }
416
234
  async insertOne(data) {
417
- const { sql, params } = buildInsert(this.resolveTableName(), data);
235
+ const { sql, params } = buildInsert$1(this.resolveTableName(), data);
418
236
  this._log(sql, params);
419
237
  const result = this._wrapConstraintError(() => this.driver.run(sql, params));
420
- return { insertedId: result.lastInsertRowid };
238
+ return { insertedId: this._resolveInsertedId(data, result.lastInsertRowid) };
421
239
  }
422
240
  async insertMany(data) {
423
241
  return this.withTransaction(async () => {
424
242
  const ids = [];
425
243
  for (const row of data) {
426
- const { sql, params } = buildInsert(this.resolveTableName(), row);
244
+ const { sql, params } = buildInsert$1(this.resolveTableName(), row);
427
245
  this._log(sql, params);
428
246
  const result = this._wrapConstraintError(() => this.driver.run(sql, params));
429
- ids.push(result.lastInsertRowid);
247
+ ids.push(this._resolveInsertedId(row, result.lastInsertRowid));
430
248
  }
431
249
  return {
432
250
  insertedCount: ids.length,
@@ -440,13 +258,13 @@ var SqliteAdapter = class extends BaseDbAdapter {
440
258
  ...query.controls,
441
259
  $limit: 1
442
260
  };
443
- const { sql, params } = buildSelect(this.resolveTableName(), where, controls);
261
+ const { sql, params } = buildSelect$1(this.resolveTableName(), where, controls);
444
262
  this._log(sql, params);
445
263
  return this.driver.get(sql, params);
446
264
  }
447
265
  async findMany(query) {
448
266
  const where = buildWhere(query.filter);
449
- const { sql, params } = buildSelect(this.resolveTableName(), where, query.controls);
267
+ const { sql, params } = buildSelect$1(this.resolveTableName(), where, query.controls);
450
268
  this._log(sql, params);
451
269
  return this.driver.all(sql, params);
452
270
  }
@@ -458,6 +276,19 @@ var SqliteAdapter = class extends BaseDbAdapter {
458
276
  const row = this.driver.get(sql, where.params);
459
277
  return row?.cnt ?? 0;
460
278
  }
279
+ async aggregate(query) {
280
+ const where = buildWhere(query.filter);
281
+ const tableName = this.resolveTableName();
282
+ if (query.controls.$count) {
283
+ const { sql: sql$1, params: params$1 } = buildAggregateCount$1(tableName, where, query.controls);
284
+ this._log(sql$1, params$1);
285
+ const row = this.driver.get(sql$1, params$1);
286
+ return [{ count: row?.count ?? 0 }];
287
+ }
288
+ const { sql, params } = buildAggregateSelect$1(tableName, where, query.controls);
289
+ this._log(sql, params);
290
+ return this.driver.all(sql, params);
291
+ }
461
292
  async updateOne(filter, data) {
462
293
  const where = buildWhere(filter);
463
294
  const tableName = this.resolveTableName();
@@ -465,7 +296,7 @@ var SqliteAdapter = class extends BaseDbAdapter {
465
296
  const setParams = [];
466
297
  for (const [key, value] of Object.entries(data)) {
467
298
  setClauses.push(`"${esc(key)}" = ?`);
468
- setParams.push(toSqliteValue(value));
299
+ setParams.push(toSqlValue(value));
469
300
  }
470
301
  const sql = `UPDATE "${esc(tableName)}" SET ${setClauses.join(", ")} WHERE rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`;
471
302
  const allParams = [...setParams, ...where.params];
@@ -478,7 +309,7 @@ var SqliteAdapter = class extends BaseDbAdapter {
478
309
  }
479
310
  async updateMany(filter, data) {
480
311
  const where = buildWhere(filter);
481
- const { sql, params } = buildUpdate(this.resolveTableName(), data, where);
312
+ const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where);
482
313
  this._log(sql, params);
483
314
  const result = this._wrapConstraintError(() => this.driver.run(sql, params));
484
315
  return {
@@ -493,7 +324,7 @@ var SqliteAdapter = class extends BaseDbAdapter {
493
324
  sql: `rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`,
494
325
  params: where.params
495
326
  };
496
- const { sql, params } = buildUpdate(tableName, data, limitedWhere);
327
+ const { sql, params } = buildUpdate$1(tableName, data, limitedWhere);
497
328
  this._log(sql, params);
498
329
  const result = this._wrapConstraintError(() => this.driver.run(sql, params));
499
330
  return {
@@ -503,7 +334,7 @@ var SqliteAdapter = class extends BaseDbAdapter {
503
334
  }
504
335
  async replaceMany(filter, data) {
505
336
  const where = buildWhere(filter);
506
- const { sql, params } = buildUpdate(this.resolveTableName(), data, where);
337
+ const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where);
507
338
  this._log(sql, params);
508
339
  const result = this._wrapConstraintError(() => this.driver.run(sql, params));
509
340
  return {
@@ -521,7 +352,7 @@ var SqliteAdapter = class extends BaseDbAdapter {
521
352
  }
522
353
  async deleteMany(filter) {
523
354
  const where = buildWhere(filter);
524
- const { sql, params } = buildDelete(this.resolveTableName(), where);
355
+ const { sql, params } = buildDelete$1(this.resolveTableName(), where);
525
356
  this._log(sql, params);
526
357
  const result = this._wrapConstraintError(() => this.driver.run(sql, params));
527
358
  return { deletedCount: result.changes };
@@ -531,10 +362,26 @@ var SqliteAdapter = class extends BaseDbAdapter {
531
362
  const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys);
532
363
  this._log(sql);
533
364
  this.driver.exec(sql);
365
+ this._seedIncrementStart();
366
+ }
367
+ /**
368
+ * Seeds the sqlite_sequence table for auto-increment fields that have a start value.
369
+ * Only applies once per adapter instance (idempotent via INSERT OR IGNORE + flag).
370
+ */ _seedIncrementStart() {
371
+ if (this._incrementSeeded) return;
372
+ this._incrementSeeded = true;
373
+ const tableName = this.resolveTableName();
374
+ for (const def of this._table.defaults.values()) if (def.kind === "fn" && def.fn === "increment" && typeof def.start === "number") {
375
+ const seedSql = `INSERT OR IGNORE INTO sqlite_sequence(name, seq) VALUES(?, ?)`;
376
+ const params = [tableName, def.start - 1];
377
+ this._log(seedSql, params);
378
+ this.driver.run(seedSql, params);
379
+ break;
380
+ }
534
381
  }
535
382
  async ensureView() {
536
383
  const view = this._table;
537
- const sql = buildCreateView(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref));
384
+ const sql = buildCreateView$1(this.resolveTableName(), view.viewPlan, view.getViewColumnMappings(), (ref) => view.resolveFieldRef(ref));
538
385
  this._log(sql);
539
386
  this.driver.exec(sql);
540
387
  }
@@ -555,8 +402,9 @@ var SqliteAdapter = class extends BaseDbAdapter {
555
402
  const sqlType = this.typeMapper(field);
556
403
  let ddl = `ALTER TABLE "${esc(tableName)}" ADD COLUMN "${esc(field.physicalName)}" ${sqlType}`;
557
404
  if (!field.optional && !field.isPrimaryKey) ddl += " NOT NULL";
558
- if (field.defaultValue?.kind === "value") ddl += ` DEFAULT ${sqlStringLiteral(field.defaultValue.value)}`;
405
+ if (field.defaultValue?.kind === "value") ddl += ` DEFAULT ${defaultValueToSqlLiteral(field.designType, field.defaultValue.value)}`;
559
406
  else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValueForType(field.designType)}`;
407
+ if (field.collate) ddl += ` COLLATE ${field.collate.toUpperCase()}`;
560
408
  this._log(ddl);
561
409
  this.driver.exec(ddl);
562
410
  added.push(field.physicalName);
@@ -569,6 +417,7 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
569
417
  async recreateTable() {
570
418
  const tableName = this.resolveTableName();
571
419
  const tempName = `${tableName}__tmp_${Date.now()}`;
420
+ this._dropAllFtsTables(tableName);
572
421
  this.driver.exec("PRAGMA foreign_keys = OFF");
573
422
  this.driver.exec("PRAGMA legacy_alter_table = ON");
574
423
  try {
@@ -585,7 +434,7 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
585
434
  const selectExprs = commonCols.map((c) => {
586
435
  const field = fieldsByName.get(c);
587
436
  if (field && !field.optional && !field.isPrimaryKey) {
588
- const fallback = field.defaultValue?.kind === "value" ? sqlStringLiteral(field.defaultValue.value) : defaultValueForType(field.designType);
437
+ const fallback = field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType);
589
438
  return `COALESCE("${esc(c)}", ${fallback}) AS "${esc(c)}"`;
590
439
  }
591
440
  return `"${esc(c)}"`;
@@ -605,6 +454,7 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
605
454
  }
606
455
  async dropTable() {
607
456
  const tableName = this.resolveTableName();
457
+ this._dropAllFtsTables(tableName);
608
458
  const ddl = `DROP TABLE IF EXISTS "${esc(tableName)}"`;
609
459
  this._log(ddl);
610
460
  this.driver.exec(ddl);
@@ -620,6 +470,7 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
620
470
  });
621
471
  }
622
472
  async dropTableByName(tableName) {
473
+ this._dropAllFtsTables(tableName);
623
474
  const ddl = `DROP TABLE IF EXISTS "${esc(tableName)}"`;
624
475
  this._log(ddl);
625
476
  this.driver.exec(ddl);
@@ -667,23 +518,151 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
667
518
  },
668
519
  shouldSkipType: (type) => type === "fulltext"
669
520
  });
521
+ this._syncFtsIndexes(tableName);
522
+ }
523
+ getSearchIndexes() {
524
+ return this._getFulltextIndexes().map((idx) => ({
525
+ name: idx.name,
526
+ description: `FTS5 index (${idx.fields.map((f) => f.name).join(", ")})`,
527
+ type: "text"
528
+ }));
529
+ }
530
+ async search(text, query, indexName) {
531
+ if (!text.trim()) return [];
532
+ const base = this._buildFtsBase(text, query.filter, indexName);
533
+ const controls = query.controls || {};
534
+ let cols = "t.*";
535
+ if (controls.$select?.asArray?.length) cols = controls.$select.asArray.map((c) => `t."${esc(c)}"`).join(", ");
536
+ let sql = `SELECT ${cols} ${base.fromWhere}`;
537
+ const params = [...base.params];
538
+ if (controls.$sort) {
539
+ const orderParts = [];
540
+ for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`t."${esc(col)}" ${dir === -1 ? "DESC" : "ASC"}`);
541
+ if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
542
+ }
543
+ if (controls.$limit !== undefined) {
544
+ sql += ` LIMIT ?`;
545
+ params.push(controls.$limit);
546
+ }
547
+ if (controls.$skip !== undefined) {
548
+ if (controls.$limit === undefined) sql += ` LIMIT -1`;
549
+ sql += ` OFFSET ?`;
550
+ params.push(controls.$skip);
551
+ }
552
+ this._log(sql, params);
553
+ return this.driver.all(sql, params);
554
+ }
555
+ async searchWithCount(text, query, indexName) {
556
+ if (!text.trim()) return {
557
+ data: [],
558
+ count: 0
559
+ };
560
+ const data = await this.search(text, query, indexName);
561
+ const base = this._buildFtsBase(text, query.filter, indexName);
562
+ const countSql = `SELECT COUNT(*) as cnt ${base.fromWhere}`;
563
+ this._log(countSql, base.params);
564
+ const row = this.driver.get(countSql, base.params);
565
+ return {
566
+ data,
567
+ count: row?.cnt ?? 0
568
+ };
569
+ }
570
+ /** Builds FTS table name from index name: `<table>__fts__<indexName>`. */ _ftsTableName(indexName) {
571
+ return `${this.resolveTableName()}__fts__${indexName}`;
572
+ }
573
+ /** Returns fulltext indexes from table metadata. */ _getFulltextIndexes() {
574
+ const result = [];
575
+ for (const index of this._table.indexes.values()) if (index.type === "fulltext") result.push(index);
576
+ return result;
577
+ }
578
+ /** Resolves a fulltext index by name, or returns the first available. */ _resolveFtsIndex(indexName) {
579
+ const ftIndexes = this._getFulltextIndexes();
580
+ if (ftIndexes.length === 0) throw new Error("No search index available");
581
+ if (indexName) {
582
+ const found = ftIndexes.find((idx) => idx.name === indexName);
583
+ if (!found) throw new Error(`Search index "${indexName}" not found`);
584
+ return found;
585
+ }
586
+ return ftIndexes[0];
587
+ }
588
+ /**
589
+ * Builds the shared FROM+JOIN+WHERE fragment for FTS5 queries.
590
+ * Both data and count queries reuse this to avoid duplicating index resolution and filter translation.
591
+ */ _buildFtsBase(text, filter, indexName) {
592
+ const ftsIndex = this._resolveFtsIndex(indexName);
593
+ const ftsTable = this._ftsTableName(ftsIndex.name);
594
+ const tableName = this.resolveTableName();
595
+ const where = buildPrefixedWhere("t", filter);
596
+ let fromWhere = `FROM "${esc(tableName)}" AS t`;
597
+ fromWhere += ` JOIN "${esc(ftsTable)}" AS fts ON t.rowid = fts.rowid`;
598
+ fromWhere += ` WHERE fts."${esc(ftsTable)}" MATCH ?`;
599
+ const params = [text];
600
+ if (where.sql !== "1=1") {
601
+ fromWhere += ` AND (${where.sql})`;
602
+ params.push(...where.params);
603
+ }
604
+ return {
605
+ fromWhere,
606
+ params
607
+ };
608
+ }
609
+ /**
610
+ * Creates/drops FTS5 virtual tables and sync triggers to match desired fulltext indexes.
611
+ */ _syncFtsIndexes(tableName) {
612
+ const ftIndexes = this._getFulltextIndexes();
613
+ const desiredFtsTables = new Set(ftIndexes.map((idx) => this._ftsTableName(idx.name)));
614
+ const existingFts = this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__fts__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE")).map((r) => r.name);
615
+ for (const name of existingFts) if (!desiredFtsTables.has(name)) this._dropFtsTable(name);
616
+ const existingSet = new Set(existingFts);
617
+ for (const index of ftIndexes) {
618
+ const ftsTable = this._ftsTableName(index.name);
619
+ if (!existingSet.has(ftsTable)) this._createFtsTable(tableName, ftsTable, index);
620
+ }
621
+ }
622
+ /** Creates an FTS5 virtual table with sync triggers and rebuilds the index. */ _createFtsTable(tableName, ftsTable, index) {
623
+ const fieldNames = index.fields.map((f) => `"${esc(f.name)}"`);
624
+ const fieldList = fieldNames.join(", ");
625
+ const createSql = `CREATE VIRTUAL TABLE IF NOT EXISTS "${esc(ftsTable)}" USING fts5(${fieldList}, content='${tableName.replace(/'/g, "''")}', content_rowid='rowid')`;
626
+ this._log(createSql);
627
+ this.driver.exec(createSql);
628
+ const newFields = index.fields.map((f) => `new."${esc(f.name)}"`).join(", ");
629
+ const oldFields = index.fields.map((f) => `old."${esc(f.name)}"`).join(", ");
630
+ const ef = esc(ftsTable);
631
+ const aiSql = `CREATE TRIGGER IF NOT EXISTS "${esc(ftsTable + "__ai")}" AFTER INSERT ON "${esc(tableName)}" BEGIN INSERT INTO "${ef}"(rowid, ${fieldList}) VALUES (new.rowid, ${newFields}); END`;
632
+ this._log(aiSql);
633
+ this.driver.exec(aiSql);
634
+ const adSql = `CREATE TRIGGER IF NOT EXISTS "${esc(ftsTable + "__ad")}" AFTER DELETE ON "${esc(tableName)}" BEGIN INSERT INTO "${ef}"("${ef}", rowid, ${fieldList}) VALUES ('delete', old.rowid, ${oldFields}); END`;
635
+ this._log(adSql);
636
+ this.driver.exec(adSql);
637
+ const auSql = `CREATE TRIGGER IF NOT EXISTS "${esc(ftsTable + "__au")}" AFTER UPDATE ON "${esc(tableName)}" BEGIN INSERT INTO "${ef}"("${ef}", rowid, ${fieldList}) VALUES ('delete', old.rowid, ${oldFields}); INSERT INTO "${ef}"(rowid, ${fieldList}) VALUES (new.rowid, ${newFields}); END`;
638
+ this._log(auSql);
639
+ this.driver.exec(auSql);
640
+ const rebuildSql = `INSERT INTO "${ef}"("${ef}") VALUES ('rebuild')`;
641
+ this._log(rebuildSql);
642
+ this.driver.exec(rebuildSql);
643
+ }
644
+ /** Drops an FTS5 virtual table and its sync triggers. */ _dropFtsTable(ftsTable) {
645
+ for (const suffix of [
646
+ "__ai",
647
+ "__ad",
648
+ "__au"
649
+ ]) this.driver.exec(`DROP TRIGGER IF EXISTS "${esc(ftsTable + suffix)}"`);
650
+ const sql = `DROP TABLE IF EXISTS "${esc(ftsTable)}"`;
651
+ this._log(sql);
652
+ this.driver.exec(sql);
653
+ }
654
+ /** Drops all FTS virtual tables and triggers for a content table. */ _dropAllFtsTables(tableName) {
655
+ const ftsTables = this.driver.all(`SELECT name, sql FROM sqlite_master WHERE type='table' AND name LIKE ?`, [`${tableName}__fts__%`]).filter((r) => r.sql.startsWith("CREATE VIRTUAL TABLE"));
656
+ for (const { name } of ftsTables) this._dropFtsTable(name);
670
657
  }
671
658
  constructor(driver) {
672
- super(), _define_property(this, "driver", void 0), this.driver = driver;
659
+ super(), _define_property(this, "driver", void 0), _define_property(this, "_incrementSeeded", void 0), this.driver = driver, this._incrementSeeded = false;
673
660
  this.driver.exec("PRAGMA foreign_keys = ON");
674
661
  }
675
662
  };
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
- }
683
- }
684
663
  /** Normalizes SQLite PRAGMA dflt_value to match serialized format.
685
664
  * PRAGMA returns `'active'` (SQL-quoted), we store `active` (raw). */ function normalizeSqliteDefault(value) {
686
- if (value == null) return undefined;
665
+ if (value === null) return undefined;
687
666
  if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
688
667
  return value;
689
668
  }