@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/README.md +29 -183
- package/dist/index.cjs +281 -295
- package/dist/index.d.ts +39 -6
- package/dist/index.mjs +287 -308
- package/package.json +7 -6
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
|
|
26
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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)
|
|
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 ${
|
|
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
|
-
|
|
344
|
-
return (0, __uniqu_core.walkFilter)(filter, sqlVisitor) ?? EMPTY_AND;
|
|
184
|
+
return (0, __atscript_db_sql_tools.buildWhere)(sqliteDialect, filter);
|
|
345
185
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
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 (
|
|
415
|
-
if (
|
|
416
|
-
if (
|
|
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:
|
|
243
|
+
message: error.message
|
|
419
244
|
}]);
|
|
420
|
-
const uniqueMatch =
|
|
421
|
-
if (uniqueMatch) throw new
|
|
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:
|
|
248
|
+
message: error.message
|
|
424
249
|
}]);
|
|
425
|
-
if (
|
|
250
|
+
if (error.message.includes("UNIQUE constraint failed")) throw new __atscript_db.DbError("CONFLICT", [{
|
|
426
251
|
path: "",
|
|
427
|
-
message:
|
|
252
|
+
message: error.message
|
|
428
253
|
}]);
|
|
429
254
|
}
|
|
430
|
-
throw
|
|
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(
|
|
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
|
|
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 ${
|
|
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" ?
|
|
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
|
|
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
|
|
698
|
+
return new __atscript_db.DbSpace(() => new SqliteAdapter(driver));
|
|
713
699
|
}
|
|
714
700
|
|
|
715
701
|
//#endregion
|