@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.cjs CHANGED
@@ -22,8 +22,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
  }) : target, mod));
23
23
 
24
24
  //#endregion
25
- const __atscript_utils_db = __toESM(require("@atscript/utils-db"));
26
- const __uniqu_core = __toESM(require("@uniqu/core"));
25
+ const __atscript_db = __toESM(require("@atscript/db"));
26
+ const node_module = __toESM(require("node:module"));
27
+ const __atscript_db_sql_tools = __toESM(require("@atscript/db-sql-tools"));
27
28
 
28
29
  //#region packages/db-sqlite/src/better-sqlite3-driver.ts
29
30
  function _define_property$1(obj, key, value) {
@@ -62,7 +63,8 @@ var BetterSqlite3Driver = class {
62
63
  constructor(pathOrDb, options) {
63
64
  _define_property$1(this, "db", void 0);
64
65
  if (typeof pathOrDb === "string") {
65
- const Database = require("better-sqlite3");
66
+ const req = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
67
+ const Database = req("better-sqlite3");
66
68
  this.db = new Database(pathOrDb, options);
67
69
  } else this.db = pathOrDb;
68
70
  }
@@ -70,55 +72,80 @@ var BetterSqlite3Driver = class {
70
72
 
71
73
  //#endregion
72
74
  //#region packages/db-sqlite/src/sql-builder.ts
75
+ function esc(name) {
76
+ return name.replace(/"/g, "\"\"");
77
+ }
78
+ /**
79
+ * Basic regex-to-LIKE conversion.
80
+ * - `^abc` → `abc%`
81
+ * - `abc$` → `%abc`
82
+ * - `^abc$` → `abc`
83
+ * - `abc` → `%abc%`
84
+ */ function regexToLike(pattern) {
85
+ const hasStart = pattern.startsWith("^");
86
+ const hasEnd = pattern.endsWith("$");
87
+ let core = pattern;
88
+ if (hasStart) core = core.slice(1);
89
+ if (hasEnd) core = core.slice(0, -1);
90
+ core = core.replace(/%/g, "\\%").replace(/_/g, "\\_");
91
+ core = core.replace(/\.\*/g, "%").replace(/\./g, "_");
92
+ if (hasStart && hasEnd) return core;
93
+ if (hasStart) return `${core}%`;
94
+ if (hasEnd) return `%${core}`;
95
+ return `%${core}%`;
96
+ }
97
+ const sqliteDialect = {
98
+ quoteIdentifier(name) {
99
+ return `"${esc(name)}"`;
100
+ },
101
+ quoteTable(name) {
102
+ return `"${esc(name)}"`;
103
+ },
104
+ unlimitedLimit: "-1",
105
+ toValue: __atscript_db_sql_tools.toSqlValue,
106
+ toParam(value) {
107
+ if (value === undefined) return null;
108
+ return typeof value === "boolean" ? value ? 1 : 0 : value;
109
+ },
110
+ regex(quotedCol, value) {
111
+ const pattern = regexToLike(value instanceof RegExp ? value.source : String(value));
112
+ return {
113
+ sql: `${quotedCol} LIKE ?`,
114
+ params: [pattern]
115
+ };
116
+ },
117
+ createViewPrefix: "CREATE VIEW IF NOT EXISTS"
118
+ };
73
119
  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
- };
120
+ return (0, __atscript_db_sql_tools.buildInsert)(sqliteDialect, table, data);
81
121
  }
82
122
  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
- };
123
+ return (0, __atscript_db_sql_tools.buildSelect)(sqliteDialect, table, where, controls);
104
124
  }
105
125
  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
- };
126
+ return (0, __atscript_db_sql_tools.buildUpdate)(sqliteDialect, table, data, where);
116
127
  }
117
128
  function buildDelete(table, where) {
118
- return {
119
- sql: `DELETE FROM "${esc(table)}" WHERE ${where.sql}`,
120
- params: [...where.params]
121
- };
129
+ return (0, __atscript_db_sql_tools.buildDelete)(sqliteDialect, table, where);
130
+ }
131
+ function buildCreateView(viewName, plan, columns, resolveFieldRef) {
132
+ return (0, __atscript_db_sql_tools.buildCreateView)(sqliteDialect, viewName, plan, columns, resolveFieldRef);
133
+ }
134
+ function buildAggregateSelect(table, where, controls) {
135
+ return (0, __atscript_db_sql_tools.buildAggregateSelect)(sqliteDialect, table, where, controls);
136
+ }
137
+ function buildAggregateCount(table, where, controls) {
138
+ return (0, __atscript_db_sql_tools.buildAggregateCount)(sqliteDialect, table, where, controls);
139
+ }
140
+ function sqliteTypeFromDesignType(designType) {
141
+ switch (designType) {
142
+ case "number":
143
+ case "integer":
144
+ case "decimal": return "REAL";
145
+ case "boolean": return "INTEGER";
146
+ case "string": return "TEXT";
147
+ default: return "TEXT";
148
+ }
122
149
  }
123
150
  function buildCreateTable(table, fields, foreignKeys) {
124
151
  const colDefs = [];
@@ -127,9 +154,13 @@ function buildCreateTable(table, fields, foreignKeys) {
127
154
  if (field.ignored) continue;
128
155
  const sqlType = field.isPrimaryKey && (field.designType === "number" || field.designType === "integer") ? "INTEGER" : sqliteTypeFromDesignType(field.designType);
129
156
  let def = `"${esc(field.physicalName)}" ${sqlType}`;
130
- if (field.isPrimaryKey && primaryKeys.length === 1) def += " PRIMARY KEY";
157
+ if (field.isPrimaryKey && primaryKeys.length === 1) {
158
+ def += " PRIMARY KEY";
159
+ if (field.defaultValue?.kind === "fn" && field.defaultValue.fn === "increment" && (field.designType === "number" || field.designType === "integer")) def += " AUTOINCREMENT";
160
+ }
131
161
  if (!field.optional && !field.isPrimaryKey) def += " NOT NULL";
132
- if (field.defaultValue?.kind === "value") def += ` DEFAULT ${sqlStringLiteral(field.defaultValue.value)}`;
162
+ if (field.defaultValue?.kind === "value") def += ` DEFAULT ${(0, __atscript_db_sql_tools.defaultValueToSqlLiteral)(field.designType, field.defaultValue.value)}`;
163
+ if (field.collate) def += ` COLLATE ${field.collate.toUpperCase()}`;
133
164
  colDefs.push(def);
134
165
  }
135
166
  if (primaryKeys.length > 1) {
@@ -140,234 +171,25 @@ function buildCreateTable(table, fields, foreignKeys) {
140
171
  const localCols = fk.fields.map((f) => `"${esc(f)}"`).join(", ");
141
172
  const targetCols = fk.targetFields.map((f) => `"${esc(f)}"`).join(", ");
142
173
  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)}`;
174
+ if (fk.onDelete) constraint += ` ON DELETE ${(0, __atscript_db_sql_tools.refActionToSql)(fk.onDelete)}`;
175
+ if (fk.onUpdate) constraint += ` ON UPDATE ${(0, __atscript_db_sql_tools.refActionToSql)(fk.onUpdate)}`;
145
176
  colDefs.push(constraint);
146
177
  }
147
178
  return `CREATE TABLE IF NOT EXISTS "${esc(table)}" (${colDefs.join(", ")})`;
148
179
  }
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
180
 
232
181
  //#endregion
233
182
  //#region packages/db-sqlite/src/filter-builder.ts
234
- const EMPTY_AND = {
235
- sql: "1=1",
236
- params: []
237
- };
238
- const EMPTY_OR = {
239
- sql: "0=1",
240
- params: []
241
- };
242
- /**
243
- * SQL visitor for `walkFilter` — renders a filter expression tree
244
- * into a parameterized SQL WHERE clause.
245
- */ const sqlVisitor = {
246
- comparison(field, op, value) {
247
- const col = `"${esc(field)}"`;
248
- const v = toSqliteParam(value);
249
- switch (op) {
250
- case "$eq": {
251
- if (v === null) return {
252
- sql: `${col} IS NULL`,
253
- params: []
254
- };
255
- return {
256
- sql: `${col} = ?`,
257
- params: [v]
258
- };
259
- }
260
- case "$ne": {
261
- if (v === null) return {
262
- sql: `${col} IS NOT NULL`,
263
- params: []
264
- };
265
- return {
266
- sql: `${col} != ?`,
267
- params: [v]
268
- };
269
- }
270
- case "$gt": return {
271
- sql: `${col} > ?`,
272
- params: [v]
273
- };
274
- case "$gte": return {
275
- sql: `${col} >= ?`,
276
- params: [v]
277
- };
278
- case "$lt": return {
279
- sql: `${col} < ?`,
280
- params: [v]
281
- };
282
- case "$lte": return {
283
- sql: `${col} <= ?`,
284
- params: [v]
285
- };
286
- case "$in": {
287
- const arr = value.map(toSqliteParam);
288
- if (arr.length === 0) return EMPTY_OR;
289
- const placeholders = arr.map(() => "?").join(", ");
290
- return {
291
- sql: `${col} IN (${placeholders})`,
292
- params: [...arr]
293
- };
294
- }
295
- case "$nin": {
296
- const arr = value.map(toSqliteParam);
297
- if (arr.length === 0) return EMPTY_AND;
298
- const placeholders = arr.map(() => "?").join(", ");
299
- return {
300
- sql: `${col} NOT IN (${placeholders})`,
301
- params: [...arr]
302
- };
303
- }
304
- case "$exists": return value ? {
305
- sql: `${col} IS NOT NULL`,
306
- params: []
307
- } : {
308
- sql: `${col} IS NULL`,
309
- params: []
310
- };
311
- case "$regex": {
312
- const pattern = regexToLike(value instanceof RegExp ? value.source : String(value));
313
- return {
314
- sql: `${col} LIKE ?`,
315
- params: [pattern]
316
- };
317
- }
318
- default: throw new Error(`Unsupported filter operator: ${op}`);
319
- }
320
- },
321
- and(children) {
322
- if (children.length === 0) return EMPTY_AND;
323
- return {
324
- sql: children.map((c) => c.sql).join(" AND "),
325
- params: children.flatMap((c) => c.params)
326
- };
327
- },
328
- or(children) {
329
- if (children.length === 0) return EMPTY_OR;
330
- return {
331
- sql: `(${children.map((c) => c.sql).join(" OR ")})`,
332
- params: children.flatMap((c) => c.params)
333
- };
334
- },
335
- not(child) {
336
- return {
337
- sql: `NOT (${child.sql})`,
338
- params: child.params
339
- };
340
- }
341
- };
342
183
  function buildWhere(filter) {
343
- if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
344
- return (0, __uniqu_core.walkFilter)(filter, sqlVisitor) ?? EMPTY_AND;
184
+ return (0, __atscript_db_sql_tools.buildWhere)(sqliteDialect, filter);
345
185
  }
346
- /**
347
- * Basic regex-to-LIKE conversion.
348
- * - `^abc` → `abc%`
349
- * - `abc$` → `%abc`
350
- * - `^abc$` → `abc`
351
- * - `abc` → `%abc%`
352
- */ function regexToLike(pattern) {
353
- const hasStart = pattern.startsWith("^");
354
- const hasEnd = pattern.endsWith("$");
355
- let core = pattern;
356
- if (hasStart) core = core.slice(1);
357
- if (hasEnd) core = core.slice(0, -1);
358
- core = core.replace(/%/g, "\\%").replace(/_/g, "\\_");
359
- core = core.replace(/\.\*/g, "%").replace(/\./g, "_");
360
- if (hasStart && hasEnd) return core;
361
- if (hasStart) return `${core}%`;
362
- if (hasEnd) return `%${core}`;
363
- return `%${core}%`;
364
- }
365
- /**
366
- * Converts a JS value to a SQLite-bindable parameter.
367
- * SQLite cannot bind booleans — they must be 0/1.
368
- */ function toSqliteParam(value) {
369
- if (typeof value === "boolean") return value ? 1 : 0;
370
- return value;
186
+ function buildPrefixedWhere(alias, filter) {
187
+ return (0, __atscript_db_sql_tools.buildWhere)({
188
+ ...sqliteDialect,
189
+ quoteIdentifier(name) {
190
+ return `${alias}."${esc(name)}"`;
191
+ }
192
+ }, filter);
371
193
  }
372
194
 
373
195
  //#endregion
@@ -382,7 +204,10 @@ function _define_property(obj, key, value) {
382
204
  else obj[key] = value;
383
205
  return obj;
384
206
  }
385
- var SqliteAdapter = class extends __atscript_utils_db.BaseDbAdapter {
207
+ var SqliteAdapter = class extends __atscript_db.BaseDbAdapter {
208
+ supportsNativeValueDefaults() {
209
+ return true;
210
+ }
386
211
  async _beginTransaction() {
387
212
  this._log("BEGIN");
388
213
  this.driver.exec("BEGIN");
@@ -411,30 +236,30 @@ var SqliteAdapter = class extends __atscript_utils_db.BaseDbAdapter {
411
236
  */ _wrapConstraintError(fn) {
412
237
  try {
413
238
  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", [{
239
+ } catch (error) {
240
+ if (error instanceof Error) {
241
+ if (error.message.includes("FOREIGN KEY constraint failed")) throw new __atscript_db.DbError("FK_VIOLATION", [{
417
242
  path: "",
418
- message: e.message
243
+ message: error.message
419
244
  }]);
420
- const uniqueMatch = e.message.match(/UNIQUE constraint failed:\s*\S+\.(\S+)/);
421
- if (uniqueMatch) throw new __atscript_utils_db.DbError("CONFLICT", [{
245
+ const uniqueMatch = error.message.match(/UNIQUE constraint failed:\s*\S+\.(\S+)/);
246
+ if (uniqueMatch) throw new __atscript_db.DbError("CONFLICT", [{
422
247
  path: uniqueMatch[1],
423
- message: e.message
248
+ message: error.message
424
249
  }]);
425
- if (e.message.includes("UNIQUE constraint failed")) throw new __atscript_utils_db.DbError("CONFLICT", [{
250
+ if (error.message.includes("UNIQUE constraint failed")) throw new __atscript_db.DbError("CONFLICT", [{
426
251
  path: "",
427
- message: e.message
252
+ message: error.message
428
253
  }]);
429
254
  }
430
- throw e;
255
+ throw error;
431
256
  }
432
257
  }
433
258
  async insertOne(data) {
434
259
  const { sql, params } = buildInsert(this.resolveTableName(), data);
435
260
  this._log(sql, params);
436
261
  const result = this._wrapConstraintError(() => this.driver.run(sql, params));
437
- return { insertedId: result.lastInsertRowid };
262
+ return { insertedId: this._resolveInsertedId(data, result.lastInsertRowid) };
438
263
  }
439
264
  async insertMany(data) {
440
265
  return this.withTransaction(async () => {
@@ -443,7 +268,7 @@ var SqliteAdapter = class extends __atscript_utils_db.BaseDbAdapter {
443
268
  const { sql, params } = buildInsert(this.resolveTableName(), row);
444
269
  this._log(sql, params);
445
270
  const result = this._wrapConstraintError(() => this.driver.run(sql, params));
446
- ids.push(result.lastInsertRowid);
271
+ ids.push(this._resolveInsertedId(row, result.lastInsertRowid));
447
272
  }
448
273
  return {
449
274
  insertedCount: ids.length,
@@ -475,6 +300,19 @@ var SqliteAdapter = class extends __atscript_utils_db.BaseDbAdapter {
475
300
  const row = this.driver.get(sql, where.params);
476
301
  return row?.cnt ?? 0;
477
302
  }
303
+ async aggregate(query) {
304
+ const where = buildWhere(query.filter);
305
+ const tableName = this.resolveTableName();
306
+ if (query.controls.$count) {
307
+ const { sql: sql$1, params: params$1 } = buildAggregateCount(tableName, where, query.controls);
308
+ this._log(sql$1, params$1);
309
+ const row = this.driver.get(sql$1, params$1);
310
+ return [{ count: row?.count ?? 0 }];
311
+ }
312
+ const { sql, params } = buildAggregateSelect(tableName, where, query.controls);
313
+ this._log(sql, params);
314
+ return this.driver.all(sql, params);
315
+ }
478
316
  async updateOne(filter, data) {
479
317
  const where = buildWhere(filter);
480
318
  const tableName = this.resolveTableName();
@@ -482,7 +320,7 @@ var SqliteAdapter = class extends __atscript_utils_db.BaseDbAdapter {
482
320
  const setParams = [];
483
321
  for (const [key, value] of Object.entries(data)) {
484
322
  setClauses.push(`"${esc(key)}" = ?`);
485
- setParams.push(toSqliteValue(value));
323
+ setParams.push((0, __atscript_db_sql_tools.toSqlValue)(value));
486
324
  }
487
325
  const sql = `UPDATE "${esc(tableName)}" SET ${setClauses.join(", ")} WHERE rowid = (SELECT rowid FROM "${esc(tableName)}" WHERE ${where.sql} LIMIT 1)`;
488
326
  const allParams = [...setParams, ...where.params];
@@ -544,10 +382,26 @@ var SqliteAdapter = class extends __atscript_utils_db.BaseDbAdapter {
544
382
  return { deletedCount: result.changes };
545
383
  }
546
384
  async ensureTable() {
547
- if (this._table instanceof __atscript_utils_db.AtscriptDbView) return this.ensureView();
385
+ if (this._table instanceof __atscript_db.AtscriptDbView) return this.ensureView();
548
386
  const sql = buildCreateTable(this.resolveTableName(), this._table.fieldDescriptors, this._table.foreignKeys);
549
387
  this._log(sql);
550
388
  this.driver.exec(sql);
389
+ this._seedIncrementStart();
390
+ }
391
+ /**
392
+ * Seeds the sqlite_sequence table for auto-increment fields that have a start value.
393
+ * Only applies once per adapter instance (idempotent via INSERT OR IGNORE + flag).
394
+ */ _seedIncrementStart() {
395
+ if (this._incrementSeeded) return;
396
+ this._incrementSeeded = true;
397
+ const tableName = this.resolveTableName();
398
+ for (const def of this._table.defaults.values()) if (def.kind === "fn" && def.fn === "increment" && typeof def.start === "number") {
399
+ const seedSql = `INSERT OR IGNORE INTO sqlite_sequence(name, seq) VALUES(?, ?)`;
400
+ const params = [tableName, def.start - 1];
401
+ this._log(seedSql, params);
402
+ this.driver.run(seedSql, params);
403
+ break;
404
+ }
551
405
  }
552
406
  async ensureView() {
553
407
  const view = this._table;
@@ -572,8 +426,9 @@ var SqliteAdapter = class extends __atscript_utils_db.BaseDbAdapter {
572
426
  const sqlType = this.typeMapper(field);
573
427
  let ddl = `ALTER TABLE "${esc(tableName)}" ADD COLUMN "${esc(field.physicalName)}" ${sqlType}`;
574
428
  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)}`;
429
+ if (field.defaultValue?.kind === "value") ddl += ` DEFAULT ${(0, __atscript_db_sql_tools.defaultValueToSqlLiteral)(field.designType, field.defaultValue.value)}`;
430
+ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${(0, __atscript_db_sql_tools.defaultValueForType)(field.designType)}`;
431
+ if (field.collate) ddl += ` COLLATE ${field.collate.toUpperCase()}`;
577
432
  this._log(ddl);
578
433
  this.driver.exec(ddl);
579
434
  added.push(field.physicalName);
@@ -586,6 +441,7 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
586
441
  async recreateTable() {
587
442
  const tableName = this.resolveTableName();
588
443
  const tempName = `${tableName}__tmp_${Date.now()}`;
444
+ this._dropAllFtsTables(tableName);
589
445
  this.driver.exec("PRAGMA foreign_keys = OFF");
590
446
  this.driver.exec("PRAGMA legacy_alter_table = ON");
591
447
  try {
@@ -602,7 +458,7 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
602
458
  const selectExprs = commonCols.map((c) => {
603
459
  const field = fieldsByName.get(c);
604
460
  if (field && !field.optional && !field.isPrimaryKey) {
605
- const fallback = field.defaultValue?.kind === "value" ? sqlStringLiteral(field.defaultValue.value) : defaultValueForType(field.designType);
461
+ const fallback = field.defaultValue?.kind === "value" ? (0, __atscript_db_sql_tools.defaultValueToSqlLiteral)(field.designType, field.defaultValue.value) : (0, __atscript_db_sql_tools.defaultValueForType)(field.designType);
606
462
  return `COALESCE("${esc(c)}", ${fallback}) AS "${esc(c)}"`;
607
463
  }
608
464
  return `"${esc(c)}"`;
@@ -622,6 +478,7 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
622
478
  }
623
479
  async dropTable() {
624
480
  const tableName = this.resolveTableName();
481
+ this._dropAllFtsTables(tableName);
625
482
  const ddl = `DROP TABLE IF EXISTS "${esc(tableName)}"`;
626
483
  this._log(ddl);
627
484
  this.driver.exec(ddl);
@@ -637,6 +494,7 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
637
494
  });
638
495
  }
639
496
  async dropTableByName(tableName) {
497
+ this._dropAllFtsTables(tableName);
640
498
  const ddl = `DROP TABLE IF EXISTS "${esc(tableName)}"`;
641
499
  this._log(ddl);
642
500
  this.driver.exec(ddl);
@@ -684,23 +542,151 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
684
542
  },
685
543
  shouldSkipType: (type) => type === "fulltext"
686
544
  });
545
+ this._syncFtsIndexes(tableName);
546
+ }
547
+ getSearchIndexes() {
548
+ return this._getFulltextIndexes().map((idx) => ({
549
+ name: idx.name,
550
+ description: `FTS5 index (${idx.fields.map((f) => f.name).join(", ")})`,
551
+ type: "text"
552
+ }));
553
+ }
554
+ async search(text, query, indexName) {
555
+ if (!text.trim()) return [];
556
+ const base = this._buildFtsBase(text, query.filter, indexName);
557
+ const controls = query.controls || {};
558
+ let cols = "t.*";
559
+ if (controls.$select?.asArray?.length) cols = controls.$select.asArray.map((c) => `t."${esc(c)}"`).join(", ");
560
+ let sql = `SELECT ${cols} ${base.fromWhere}`;
561
+ const params = [...base.params];
562
+ if (controls.$sort) {
563
+ const orderParts = [];
564
+ for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`t."${esc(col)}" ${dir === -1 ? "DESC" : "ASC"}`);
565
+ if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
566
+ }
567
+ if (controls.$limit !== undefined) {
568
+ sql += ` LIMIT ?`;
569
+ params.push(controls.$limit);
570
+ }
571
+ if (controls.$skip !== undefined) {
572
+ if (controls.$limit === undefined) sql += ` LIMIT -1`;
573
+ sql += ` OFFSET ?`;
574
+ params.push(controls.$skip);
575
+ }
576
+ this._log(sql, params);
577
+ return this.driver.all(sql, params);
578
+ }
579
+ async searchWithCount(text, query, indexName) {
580
+ if (!text.trim()) return {
581
+ data: [],
582
+ count: 0
583
+ };
584
+ const data = await this.search(text, query, indexName);
585
+ const base = this._buildFtsBase(text, query.filter, indexName);
586
+ const countSql = `SELECT COUNT(*) as cnt ${base.fromWhere}`;
587
+ this._log(countSql, base.params);
588
+ const row = this.driver.get(countSql, base.params);
589
+ return {
590
+ data,
591
+ count: row?.cnt ?? 0
592
+ };
593
+ }
594
+ /** Builds FTS table name from index name: `<table>__fts__<indexName>`. */ _ftsTableName(indexName) {
595
+ return `${this.resolveTableName()}__fts__${indexName}`;
596
+ }
597
+ /** Returns fulltext indexes from table metadata. */ _getFulltextIndexes() {
598
+ const result = [];
599
+ for (const index of this._table.indexes.values()) if (index.type === "fulltext") result.push(index);
600
+ return result;
601
+ }
602
+ /** Resolves a fulltext index by name, or returns the first available. */ _resolveFtsIndex(indexName) {
603
+ const ftIndexes = this._getFulltextIndexes();
604
+ if (ftIndexes.length === 0) throw new Error("No search index available");
605
+ if (indexName) {
606
+ const found = ftIndexes.find((idx) => idx.name === indexName);
607
+ if (!found) throw new Error(`Search index "${indexName}" not found`);
608
+ return found;
609
+ }
610
+ return ftIndexes[0];
611
+ }
612
+ /**
613
+ * Builds the shared FROM+JOIN+WHERE fragment for FTS5 queries.
614
+ * Both data and count queries reuse this to avoid duplicating index resolution and filter translation.
615
+ */ _buildFtsBase(text, filter, indexName) {
616
+ const ftsIndex = this._resolveFtsIndex(indexName);
617
+ const ftsTable = this._ftsTableName(ftsIndex.name);
618
+ const tableName = this.resolveTableName();
619
+ const where = buildPrefixedWhere("t", filter);
620
+ let fromWhere = `FROM "${esc(tableName)}" AS t`;
621
+ fromWhere += ` JOIN "${esc(ftsTable)}" AS fts ON t.rowid = fts.rowid`;
622
+ fromWhere += ` WHERE fts."${esc(ftsTable)}" MATCH ?`;
623
+ const params = [text];
624
+ if (where.sql !== "1=1") {
625
+ fromWhere += ` AND (${where.sql})`;
626
+ params.push(...where.params);
627
+ }
628
+ return {
629
+ fromWhere,
630
+ params
631
+ };
632
+ }
633
+ /**
634
+ * Creates/drops FTS5 virtual tables and sync triggers to match desired fulltext indexes.
635
+ */ _syncFtsIndexes(tableName) {
636
+ const ftIndexes = this._getFulltextIndexes();
637
+ const desiredFtsTables = new Set(ftIndexes.map((idx) => this._ftsTableName(idx.name)));
638
+ 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);
639
+ for (const name of existingFts) if (!desiredFtsTables.has(name)) this._dropFtsTable(name);
640
+ const existingSet = new Set(existingFts);
641
+ for (const index of ftIndexes) {
642
+ const ftsTable = this._ftsTableName(index.name);
643
+ if (!existingSet.has(ftsTable)) this._createFtsTable(tableName, ftsTable, index);
644
+ }
645
+ }
646
+ /** Creates an FTS5 virtual table with sync triggers and rebuilds the index. */ _createFtsTable(tableName, ftsTable, index) {
647
+ const fieldNames = index.fields.map((f) => `"${esc(f.name)}"`);
648
+ const fieldList = fieldNames.join(", ");
649
+ const createSql = `CREATE VIRTUAL TABLE IF NOT EXISTS "${esc(ftsTable)}" USING fts5(${fieldList}, content='${tableName.replace(/'/g, "''")}', content_rowid='rowid')`;
650
+ this._log(createSql);
651
+ this.driver.exec(createSql);
652
+ const newFields = index.fields.map((f) => `new."${esc(f.name)}"`).join(", ");
653
+ const oldFields = index.fields.map((f) => `old."${esc(f.name)}"`).join(", ");
654
+ const ef = esc(ftsTable);
655
+ 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`;
656
+ this._log(aiSql);
657
+ this.driver.exec(aiSql);
658
+ 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`;
659
+ this._log(adSql);
660
+ this.driver.exec(adSql);
661
+ 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`;
662
+ this._log(auSql);
663
+ this.driver.exec(auSql);
664
+ const rebuildSql = `INSERT INTO "${ef}"("${ef}") VALUES ('rebuild')`;
665
+ this._log(rebuildSql);
666
+ this.driver.exec(rebuildSql);
667
+ }
668
+ /** Drops an FTS5 virtual table and its sync triggers. */ _dropFtsTable(ftsTable) {
669
+ for (const suffix of [
670
+ "__ai",
671
+ "__ad",
672
+ "__au"
673
+ ]) this.driver.exec(`DROP TRIGGER IF EXISTS "${esc(ftsTable + suffix)}"`);
674
+ const sql = `DROP TABLE IF EXISTS "${esc(ftsTable)}"`;
675
+ this._log(sql);
676
+ this.driver.exec(sql);
677
+ }
678
+ /** Drops all FTS virtual tables and triggers for a content table. */ _dropAllFtsTables(tableName) {
679
+ 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"));
680
+ for (const { name } of ftsTables) this._dropFtsTable(name);
687
681
  }
688
682
  constructor(driver) {
689
- super(), _define_property(this, "driver", void 0), this.driver = driver;
683
+ super(), _define_property(this, "driver", void 0), _define_property(this, "_incrementSeeded", void 0), this.driver = driver, this._incrementSeeded = false;
690
684
  this.driver.exec("PRAGMA foreign_keys = ON");
691
685
  }
692
686
  };
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
- }
700
- }
701
687
  /** Normalizes SQLite PRAGMA dflt_value to match serialized format.
702
688
  * PRAGMA returns `'active'` (SQL-quoted), we store `active` (raw). */ function normalizeSqliteDefault(value) {
703
- if (value == null) return undefined;
689
+ if (value === null) return undefined;
704
690
  if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
705
691
  return value;
706
692
  }
@@ -709,7 +695,7 @@ else if (!field.optional && !field.isPrimaryKey) ddl += ` DEFAULT ${defaultValue
709
695
  //#region packages/db-sqlite/src/index.ts
710
696
  function createAdapter(connection, options) {
711
697
  const driver = new BetterSqlite3Driver(connection, options);
712
- return new __atscript_utils_db.DbSpace(() => new SqliteAdapter(driver));
698
+ return new __atscript_db.DbSpace(() => new SqliteAdapter(driver));
713
699
  }
714
700
 
715
701
  //#endregion