@atscript/db-sql-tools 0.1.38
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/LICENSE +21 -0
- package/dist/index.cjs +428 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.mjs +384 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Artem Maltsev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
//#region rolldown:runtime
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
const __uniqu_core = __toESM(require("@uniqu/core"));
|
|
26
|
+
const __atscript_db_agg = __toESM(require("@atscript/db/agg"));
|
|
27
|
+
|
|
28
|
+
//#region packages/db-sql-tools/src/dialect.ts
|
|
29
|
+
function finalizeParams(dialect, fragment) {
|
|
30
|
+
if (!dialect.paramPlaceholder) return fragment;
|
|
31
|
+
let idx = 0;
|
|
32
|
+
const sql = fragment.sql.replace(/\?/g, () => dialect.paramPlaceholder(++idx));
|
|
33
|
+
return {
|
|
34
|
+
sql,
|
|
35
|
+
params: fragment.params
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const EMPTY_AND = {
|
|
39
|
+
sql: "1=1",
|
|
40
|
+
params: []
|
|
41
|
+
};
|
|
42
|
+
const EMPTY_OR = {
|
|
43
|
+
sql: "0=1",
|
|
44
|
+
params: []
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region packages/db-sql-tools/src/filter-builder.ts
|
|
49
|
+
function createFilterVisitor(dialect) {
|
|
50
|
+
return {
|
|
51
|
+
comparison(field, op, value) {
|
|
52
|
+
const col = dialect.quoteIdentifier(field);
|
|
53
|
+
const v = dialect.toParam(value);
|
|
54
|
+
switch (op) {
|
|
55
|
+
case "$eq": {
|
|
56
|
+
if (v === null) return {
|
|
57
|
+
sql: `${col} IS NULL`,
|
|
58
|
+
params: []
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
sql: `${col} = ?`,
|
|
62
|
+
params: [v]
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
case "$ne": {
|
|
66
|
+
if (v === null) return {
|
|
67
|
+
sql: `${col} IS NOT NULL`,
|
|
68
|
+
params: []
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
sql: `${col} != ?`,
|
|
72
|
+
params: [v]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
case "$gt": return {
|
|
76
|
+
sql: `${col} > ?`,
|
|
77
|
+
params: [v]
|
|
78
|
+
};
|
|
79
|
+
case "$gte": return {
|
|
80
|
+
sql: `${col} >= ?`,
|
|
81
|
+
params: [v]
|
|
82
|
+
};
|
|
83
|
+
case "$lt": return {
|
|
84
|
+
sql: `${col} < ?`,
|
|
85
|
+
params: [v]
|
|
86
|
+
};
|
|
87
|
+
case "$lte": return {
|
|
88
|
+
sql: `${col} <= ?`,
|
|
89
|
+
params: [v]
|
|
90
|
+
};
|
|
91
|
+
case "$in": {
|
|
92
|
+
const arr = value.map((x) => dialect.toParam(x));
|
|
93
|
+
if (arr.length === 0) return EMPTY_OR;
|
|
94
|
+
const placeholders = arr.map(() => "?").join(", ");
|
|
95
|
+
return {
|
|
96
|
+
sql: `${col} IN (${placeholders})`,
|
|
97
|
+
params: arr
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
case "$nin": {
|
|
101
|
+
const arr = value.map((x) => dialect.toParam(x));
|
|
102
|
+
if (arr.length === 0) return EMPTY_AND;
|
|
103
|
+
const placeholders = arr.map(() => "?").join(", ");
|
|
104
|
+
return {
|
|
105
|
+
sql: `${col} NOT IN (${placeholders})`,
|
|
106
|
+
params: arr
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
case "$exists": return value ? {
|
|
110
|
+
sql: `${col} IS NOT NULL`,
|
|
111
|
+
params: []
|
|
112
|
+
} : {
|
|
113
|
+
sql: `${col} IS NULL`,
|
|
114
|
+
params: []
|
|
115
|
+
};
|
|
116
|
+
case "$regex": return dialect.regex(col, value);
|
|
117
|
+
default: throw new Error(`Unsupported filter operator: ${op}`);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
and(children) {
|
|
121
|
+
if (children.length === 0) return EMPTY_AND;
|
|
122
|
+
return {
|
|
123
|
+
sql: children.map((c) => c.sql).join(" AND "),
|
|
124
|
+
params: children.flatMap((c) => c.params)
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
or(children) {
|
|
128
|
+
if (children.length === 0) return EMPTY_OR;
|
|
129
|
+
return {
|
|
130
|
+
sql: `(${children.map((c) => c.sql).join(" OR ")})`,
|
|
131
|
+
params: children.flatMap((c) => c.params)
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
not(child) {
|
|
135
|
+
return {
|
|
136
|
+
sql: `NOT (${child.sql})`,
|
|
137
|
+
params: child.params
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const visitorCache = new WeakMap();
|
|
143
|
+
function getVisitor(dialect) {
|
|
144
|
+
let visitor = visitorCache.get(dialect);
|
|
145
|
+
if (!visitor) {
|
|
146
|
+
visitor = createFilterVisitor(dialect);
|
|
147
|
+
visitorCache.set(dialect, visitor);
|
|
148
|
+
}
|
|
149
|
+
return visitor;
|
|
150
|
+
}
|
|
151
|
+
function buildWhere(dialect, filter) {
|
|
152
|
+
if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
|
|
153
|
+
return (0, __uniqu_core.walkFilter)(filter, getVisitor(dialect)) ?? EMPTY_AND;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region packages/db-sql-tools/src/common.ts
|
|
158
|
+
function sqlStringLiteral(value) {
|
|
159
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
160
|
+
}
|
|
161
|
+
function toSqlValue(value) {
|
|
162
|
+
if (value === undefined) return null;
|
|
163
|
+
if (value === null) return null;
|
|
164
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
165
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
function refActionToSql(action) {
|
|
169
|
+
switch (action) {
|
|
170
|
+
case "cascade": return "CASCADE";
|
|
171
|
+
case "restrict": return "RESTRICT";
|
|
172
|
+
case "setNull": return "SET NULL";
|
|
173
|
+
case "setDefault": return "SET DEFAULT";
|
|
174
|
+
default: return "NO ACTION";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function defaultValueForType(designType) {
|
|
178
|
+
switch (designType) {
|
|
179
|
+
case "number":
|
|
180
|
+
case "integer": return "0";
|
|
181
|
+
case "boolean": return "0";
|
|
182
|
+
case "decimal": return "'0'";
|
|
183
|
+
default: return "''";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function defaultValueToSqlLiteral(designType, value) {
|
|
187
|
+
switch (designType) {
|
|
188
|
+
case "boolean": return value === "true" || value === "1" ? "1" : "0";
|
|
189
|
+
case "number":
|
|
190
|
+
case "integer":
|
|
191
|
+
case "decimal": {
|
|
192
|
+
const n = Number(value);
|
|
193
|
+
return Number.isFinite(n) ? String(n) : "0";
|
|
194
|
+
}
|
|
195
|
+
default: return sqlStringLiteral(value);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const queryOpToSql = {
|
|
199
|
+
$eq: "=",
|
|
200
|
+
$ne: "!=",
|
|
201
|
+
$gt: ">",
|
|
202
|
+
$gte: ">=",
|
|
203
|
+
$lt: "<",
|
|
204
|
+
$lte: "<="
|
|
205
|
+
};
|
|
206
|
+
function queryNodeToSql(node, resolveFieldRef) {
|
|
207
|
+
if ("$and" in node) {
|
|
208
|
+
const children = node.$and;
|
|
209
|
+
return children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" AND ");
|
|
210
|
+
}
|
|
211
|
+
if ("$or" in node) {
|
|
212
|
+
const children = node.$or;
|
|
213
|
+
return `(${children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" OR ")})`;
|
|
214
|
+
}
|
|
215
|
+
if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef)})`;
|
|
216
|
+
const comp = node;
|
|
217
|
+
const leftSql = resolveFieldRef(comp.left);
|
|
218
|
+
const sqlOp = queryOpToSql[comp.op] || "=";
|
|
219
|
+
if (comp.right && typeof comp.right === "object" && "field" in comp.right) return `${leftSql} ${sqlOp} ${resolveFieldRef(comp.right)}`;
|
|
220
|
+
if (comp.right === null || comp.right === undefined) return comp.op === "$ne" ? `${leftSql} IS NOT NULL` : `${leftSql} IS NULL`;
|
|
221
|
+
if (typeof comp.right === "string") return `${leftSql} ${sqlOp} '${comp.right.replace(/'/g, "''")}'`;
|
|
222
|
+
return `${leftSql} ${sqlOp} ${comp.right}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region packages/db-sql-tools/src/agg.ts
|
|
227
|
+
const AGG_FN_SQL = {
|
|
228
|
+
sum: "SUM",
|
|
229
|
+
avg: "AVG",
|
|
230
|
+
count: "COUNT",
|
|
231
|
+
min: "MIN",
|
|
232
|
+
max: "MAX"
|
|
233
|
+
};
|
|
234
|
+
function buildAggExpr(dialect, expr) {
|
|
235
|
+
const fn = AGG_FN_SQL[expr.$fn] ?? expr.$fn.toUpperCase();
|
|
236
|
+
const alias = dialect.quoteIdentifier((0, __atscript_db_agg.resolveAlias)(expr));
|
|
237
|
+
const field = expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field);
|
|
238
|
+
return `${fn}(${field}) AS ${alias}`;
|
|
239
|
+
}
|
|
240
|
+
function buildAggregateSelect(dialect, table, where, controls) {
|
|
241
|
+
const selectParts = [];
|
|
242
|
+
const plainFields = controls.$select?.asArray;
|
|
243
|
+
if (plainFields) for (const f of plainFields) selectParts.push(dialect.quoteIdentifier(f));
|
|
244
|
+
const aggregates = controls.$select?.aggregates;
|
|
245
|
+
if (aggregates) for (const expr of aggregates) selectParts.push(buildAggExpr(dialect, expr));
|
|
246
|
+
const cols = selectParts.length > 0 ? selectParts.join(", ") : "*";
|
|
247
|
+
let sql = `SELECT ${cols} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
248
|
+
const params = [...where.params];
|
|
249
|
+
const groupBy = controls.$groupBy;
|
|
250
|
+
if (groupBy?.length) {
|
|
251
|
+
const groupCols = groupBy.map((f) => dialect.quoteIdentifier(f)).join(", ");
|
|
252
|
+
sql += ` GROUP BY ${groupCols}`;
|
|
253
|
+
}
|
|
254
|
+
if (controls.$having) {
|
|
255
|
+
const havingFragment = buildWhere(dialect, controls.$having);
|
|
256
|
+
if (havingFragment.sql !== EMPTY_AND.sql) {
|
|
257
|
+
sql += ` HAVING ${havingFragment.sql}`;
|
|
258
|
+
params.push(...havingFragment.params);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (controls.$sort) {
|
|
262
|
+
const orderParts = [];
|
|
263
|
+
for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
|
|
264
|
+
if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
|
|
265
|
+
}
|
|
266
|
+
if (controls.$limit !== undefined) {
|
|
267
|
+
sql += ` LIMIT ?`;
|
|
268
|
+
params.push(controls.$limit);
|
|
269
|
+
}
|
|
270
|
+
if (controls.$skip !== undefined) {
|
|
271
|
+
if (controls.$limit === undefined) sql += ` LIMIT ${dialect.unlimitedLimit}`;
|
|
272
|
+
sql += ` OFFSET ?`;
|
|
273
|
+
params.push(controls.$skip);
|
|
274
|
+
}
|
|
275
|
+
return finalizeParams(dialect, {
|
|
276
|
+
sql,
|
|
277
|
+
params
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function buildAggregateCount(dialect, table, where, controls) {
|
|
281
|
+
const groupFields = controls.$groupBy;
|
|
282
|
+
if (!groupFields?.length) {
|
|
283
|
+
const sql$1 = `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
284
|
+
return finalizeParams(dialect, {
|
|
285
|
+
sql: sql$1,
|
|
286
|
+
params: where.params
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const groupCols = groupFields.map((f) => dialect.quoteIdentifier(f)).join(", ");
|
|
290
|
+
const sql = `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM (SELECT 1 FROM ${dialect.quoteTable(table)} WHERE ${where.sql} GROUP BY ${groupCols}) AS ${dialect.quoteIdentifier("_groups")}`;
|
|
291
|
+
return finalizeParams(dialect, {
|
|
292
|
+
sql,
|
|
293
|
+
params: where.params
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region packages/db-sql-tools/src/sql-builder.ts
|
|
299
|
+
function buildInsert(dialect, table, data) {
|
|
300
|
+
const keys = Object.keys(data);
|
|
301
|
+
const cols = keys.map((k) => dialect.quoteIdentifier(k)).join(", ");
|
|
302
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
303
|
+
return finalizeParams(dialect, {
|
|
304
|
+
sql: `INSERT INTO ${dialect.quoteTable(table)} (${cols}) VALUES (${placeholders})`,
|
|
305
|
+
params: keys.map((k) => dialect.toValue(data[k]))
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function buildSelect(dialect, table, where, controls) {
|
|
309
|
+
const cols = buildProjection(dialect, controls?.$select);
|
|
310
|
+
let sql = `SELECT ${cols} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
311
|
+
const params = [...where.params];
|
|
312
|
+
if (controls?.$sort) {
|
|
313
|
+
const orderParts = [];
|
|
314
|
+
for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
|
|
315
|
+
if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
|
|
316
|
+
}
|
|
317
|
+
if (controls?.$limit !== undefined) {
|
|
318
|
+
sql += ` LIMIT ?`;
|
|
319
|
+
params.push(controls.$limit);
|
|
320
|
+
}
|
|
321
|
+
if (controls?.$skip !== undefined) {
|
|
322
|
+
if (controls.$limit === undefined) sql += ` LIMIT ${dialect.unlimitedLimit}`;
|
|
323
|
+
sql += ` OFFSET ?`;
|
|
324
|
+
params.push(controls.$skip);
|
|
325
|
+
}
|
|
326
|
+
return finalizeParams(dialect, {
|
|
327
|
+
sql,
|
|
328
|
+
params
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function buildUpdate(dialect, table, data, where, limit) {
|
|
332
|
+
const setClauses = [];
|
|
333
|
+
const params = [];
|
|
334
|
+
for (const [key, value] of Object.entries(data)) {
|
|
335
|
+
setClauses.push(`${dialect.quoteIdentifier(key)} = ?`);
|
|
336
|
+
params.push(dialect.toValue(value));
|
|
337
|
+
}
|
|
338
|
+
let sql = `UPDATE ${dialect.quoteTable(table)} SET ${setClauses.join(", ")} WHERE ${where.sql}`;
|
|
339
|
+
if (limit !== undefined) sql += ` LIMIT ${limit}`;
|
|
340
|
+
return finalizeParams(dialect, {
|
|
341
|
+
sql,
|
|
342
|
+
params: [...params, ...where.params]
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
function buildDelete(dialect, table, where, limit) {
|
|
346
|
+
let sql = `DELETE FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
347
|
+
if (limit !== undefined) sql += ` LIMIT ${limit}`;
|
|
348
|
+
return finalizeParams(dialect, {
|
|
349
|
+
sql,
|
|
350
|
+
params: where.params
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function buildProjection(dialect, select) {
|
|
354
|
+
const fields = select?.asArray;
|
|
355
|
+
if (!fields) return "*";
|
|
356
|
+
let sql = "";
|
|
357
|
+
for (let i = 0; i < fields.length; i++) {
|
|
358
|
+
if (i > 0) sql += ", ";
|
|
359
|
+
sql += dialect.quoteIdentifier(fields[i]);
|
|
360
|
+
}
|
|
361
|
+
return sql || "*";
|
|
362
|
+
}
|
|
363
|
+
/** Builds the SQL expression for a single aggregate column. */ function buildAggColExpr(dialect, c) {
|
|
364
|
+
const fn = AGG_FN_SQL[c.aggFn] ?? c.aggFn.toUpperCase();
|
|
365
|
+
const arg = c.aggField === "*" ? "*" : `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`;
|
|
366
|
+
return `${fn}(${arg})`;
|
|
367
|
+
}
|
|
368
|
+
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
369
|
+
const selectCols = columns.map((c) => {
|
|
370
|
+
if (c.aggFn) return `${buildAggColExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
371
|
+
return `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
372
|
+
}).join(", ");
|
|
373
|
+
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
374
|
+
for (const join of plan.joins) {
|
|
375
|
+
const onClause = queryNodeToSql(join.condition, resolveFieldRef);
|
|
376
|
+
sql += ` JOIN ${dialect.quoteIdentifier(join.targetTable)} ON ${onClause}`;
|
|
377
|
+
}
|
|
378
|
+
if (plan.filter) {
|
|
379
|
+
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
380
|
+
sql += ` WHERE ${whereClause}`;
|
|
381
|
+
}
|
|
382
|
+
const hasAggregates = columns.some((c) => c.aggFn);
|
|
383
|
+
if (hasAggregates) {
|
|
384
|
+
const dimensionCols = columns.filter((c) => !c.aggFn);
|
|
385
|
+
if (dimensionCols.length > 0) {
|
|
386
|
+
const groupByCols = dimensionCols.map((c) => `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`).join(", ");
|
|
387
|
+
sql += ` GROUP BY ${groupByCols}`;
|
|
388
|
+
}
|
|
389
|
+
if (plan.having) {
|
|
390
|
+
const columnMap = new Map();
|
|
391
|
+
for (const c of columns) columnMap.set(c.viewColumn, c);
|
|
392
|
+
const havingResolver = (ref) => {
|
|
393
|
+
if (!ref.type) {
|
|
394
|
+
const col = columnMap.get(ref.field);
|
|
395
|
+
if (col?.aggFn) return buildAggColExpr(dialect, col);
|
|
396
|
+
if (col) return `${dialect.quoteIdentifier(col.sourceTable)}.${dialect.quoteIdentifier(col.sourceColumn)}`;
|
|
397
|
+
}
|
|
398
|
+
return resolveFieldRef(ref);
|
|
399
|
+
};
|
|
400
|
+
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
401
|
+
sql += ` HAVING ${havingClause}`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return sql;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
//#endregion
|
|
408
|
+
exports.AGG_FN_SQL = AGG_FN_SQL
|
|
409
|
+
exports.EMPTY_AND = EMPTY_AND
|
|
410
|
+
exports.EMPTY_OR = EMPTY_OR
|
|
411
|
+
exports.buildAggregateCount = buildAggregateCount
|
|
412
|
+
exports.buildAggregateSelect = buildAggregateSelect
|
|
413
|
+
exports.buildCreateView = buildCreateView
|
|
414
|
+
exports.buildDelete = buildDelete
|
|
415
|
+
exports.buildInsert = buildInsert
|
|
416
|
+
exports.buildProjection = buildProjection
|
|
417
|
+
exports.buildSelect = buildSelect
|
|
418
|
+
exports.buildUpdate = buildUpdate
|
|
419
|
+
exports.buildWhere = buildWhere
|
|
420
|
+
exports.createFilterVisitor = createFilterVisitor
|
|
421
|
+
exports.defaultValueForType = defaultValueForType
|
|
422
|
+
exports.defaultValueToSqlLiteral = defaultValueToSqlLiteral
|
|
423
|
+
exports.finalizeParams = finalizeParams
|
|
424
|
+
exports.queryNodeToSql = queryNodeToSql
|
|
425
|
+
exports.queryOpToSql = queryOpToSql
|
|
426
|
+
exports.refActionToSql = refActionToSql
|
|
427
|
+
exports.sqlStringLiteral = sqlStringLiteral
|
|
428
|
+
exports.toSqlValue = toSqlValue
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { FilterExpr, FilterVisitor } from '@uniqu/core';
|
|
2
|
+
import { TViewPlan, TViewColumnMapping, AtscriptQueryFieldRef, UniquSelect, DbControls, AtscriptQueryNode, TDbReferentialAction } from '@atscript/db';
|
|
3
|
+
|
|
4
|
+
interface TSqlFragment {
|
|
5
|
+
sql: string;
|
|
6
|
+
params: unknown[];
|
|
7
|
+
}
|
|
8
|
+
interface SqlDialect {
|
|
9
|
+
/** Quotes a column/table name */
|
|
10
|
+
quoteIdentifier(name: string): string;
|
|
11
|
+
/** Quotes a possibly schema-qualified table name */
|
|
12
|
+
quoteTable(name: string): string;
|
|
13
|
+
/** SQL literal for unlimited LIMIT (SQLite: '-1', MySQL: '18446744073709551615') */
|
|
14
|
+
unlimitedLimit: string;
|
|
15
|
+
/** Convert JS value to SQL-bindable param for DML */
|
|
16
|
+
toValue(value: unknown): unknown;
|
|
17
|
+
/** Convert JS value to SQL-bindable param for filters (lighter) */
|
|
18
|
+
toParam(value: unknown): unknown;
|
|
19
|
+
/** Handle $regex filter */
|
|
20
|
+
regex(quotedCol: string, value: unknown): TSqlFragment;
|
|
21
|
+
/** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
|
|
22
|
+
createViewPrefix: string;
|
|
23
|
+
/** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
|
|
24
|
+
paramPlaceholder?: (index: number) => string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Replaces positional `?` placeholders with dialect-specific numbered placeholders
|
|
28
|
+
* (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
|
|
29
|
+
*/
|
|
30
|
+
declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TSqlFragment;
|
|
31
|
+
declare const EMPTY_AND: TSqlFragment;
|
|
32
|
+
declare const EMPTY_OR: TSqlFragment;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a dialect-specific filter visitor for `walkFilter`.
|
|
36
|
+
*/
|
|
37
|
+
declare function createFilterVisitor(dialect: SqlDialect): FilterVisitor<TSqlFragment>;
|
|
38
|
+
/**
|
|
39
|
+
* Translates a filter expression into a parameterized SQL WHERE clause.
|
|
40
|
+
*/
|
|
41
|
+
declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragment;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Builds an INSERT statement.
|
|
45
|
+
*/
|
|
46
|
+
declare function buildInsert(dialect: SqlDialect, table: string, data: Record<string, unknown>): TSqlFragment;
|
|
47
|
+
/**
|
|
48
|
+
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
49
|
+
*/
|
|
50
|
+
declare function buildSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls?: DbControls): TSqlFragment;
|
|
51
|
+
/**
|
|
52
|
+
* Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
|
|
53
|
+
*/
|
|
54
|
+
declare function buildUpdate(dialect: SqlDialect, table: string, data: Record<string, unknown>, where: TSqlFragment, limit?: number): TSqlFragment;
|
|
55
|
+
/**
|
|
56
|
+
* Builds a DELETE ... WHERE statement with optional LIMIT.
|
|
57
|
+
*/
|
|
58
|
+
declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFragment, limit?: number): TSqlFragment;
|
|
59
|
+
/**
|
|
60
|
+
* Builds a column projection (SELECT clause fields).
|
|
61
|
+
*/
|
|
62
|
+
declare function buildProjection(dialect: SqlDialect, select?: UniquSelect): string;
|
|
63
|
+
/**
|
|
64
|
+
* Builds a CREATE VIEW statement from a view plan and column mappings.
|
|
65
|
+
*/
|
|
66
|
+
declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
67
|
+
|
|
68
|
+
/** Formats a string value as a SQL literal with single-quote escaping. */
|
|
69
|
+
declare function sqlStringLiteral(value: string): string;
|
|
70
|
+
/** Converts a JS value to a SQL-bindable parameter. Objects/arrays -> JSON, booleans -> 0/1. */
|
|
71
|
+
declare function toSqlValue(value: unknown): unknown;
|
|
72
|
+
declare function refActionToSql(action: TDbReferentialAction): string;
|
|
73
|
+
/** Returns a safe SQL DEFAULT literal for a given design type. */
|
|
74
|
+
declare function defaultValueForType(designType: string): string;
|
|
75
|
+
/**
|
|
76
|
+
* Converts a stored default value string to a SQL DEFAULT literal,
|
|
77
|
+
* respecting the field's designType. Booleans become 0/1, numbers stay unquoted,
|
|
78
|
+
* strings are single-quote-escaped.
|
|
79
|
+
*/
|
|
80
|
+
declare function defaultValueToSqlLiteral(designType: string, value: string): string;
|
|
81
|
+
declare const queryOpToSql: Record<string, string>;
|
|
82
|
+
/**
|
|
83
|
+
* Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
|
|
84
|
+
*/
|
|
85
|
+
declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
|
|
86
|
+
|
|
87
|
+
declare const AGG_FN_SQL: Record<string, string>;
|
|
88
|
+
/**
|
|
89
|
+
* Builds a SELECT ... GROUP BY statement with aggregate functions.
|
|
90
|
+
*/
|
|
91
|
+
declare function buildAggregateSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls): TSqlFragment;
|
|
92
|
+
/**
|
|
93
|
+
* Builds a COUNT query for the number of distinct groups.
|
|
94
|
+
* Returns `{ count: N }` when executed.
|
|
95
|
+
*/
|
|
96
|
+
declare function buildAggregateCount(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls): TSqlFragment;
|
|
97
|
+
|
|
98
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, queryNodeToSql, queryOpToSql, refActionToSql, sqlStringLiteral, toSqlValue };
|
|
99
|
+
export type { SqlDialect, TSqlFragment };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { walkFilter } from "@uniqu/core";
|
|
2
|
+
import { resolveAlias } from "@atscript/db/agg";
|
|
3
|
+
|
|
4
|
+
//#region packages/db-sql-tools/src/dialect.ts
|
|
5
|
+
function finalizeParams(dialect, fragment) {
|
|
6
|
+
if (!dialect.paramPlaceholder) return fragment;
|
|
7
|
+
let idx = 0;
|
|
8
|
+
const sql = fragment.sql.replace(/\?/g, () => dialect.paramPlaceholder(++idx));
|
|
9
|
+
return {
|
|
10
|
+
sql,
|
|
11
|
+
params: fragment.params
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const EMPTY_AND = {
|
|
15
|
+
sql: "1=1",
|
|
16
|
+
params: []
|
|
17
|
+
};
|
|
18
|
+
const EMPTY_OR = {
|
|
19
|
+
sql: "0=1",
|
|
20
|
+
params: []
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region packages/db-sql-tools/src/filter-builder.ts
|
|
25
|
+
function createFilterVisitor(dialect) {
|
|
26
|
+
return {
|
|
27
|
+
comparison(field, op, value) {
|
|
28
|
+
const col = dialect.quoteIdentifier(field);
|
|
29
|
+
const v = dialect.toParam(value);
|
|
30
|
+
switch (op) {
|
|
31
|
+
case "$eq": {
|
|
32
|
+
if (v === null) return {
|
|
33
|
+
sql: `${col} IS NULL`,
|
|
34
|
+
params: []
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
sql: `${col} = ?`,
|
|
38
|
+
params: [v]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
case "$ne": {
|
|
42
|
+
if (v === null) return {
|
|
43
|
+
sql: `${col} IS NOT NULL`,
|
|
44
|
+
params: []
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
sql: `${col} != ?`,
|
|
48
|
+
params: [v]
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
case "$gt": return {
|
|
52
|
+
sql: `${col} > ?`,
|
|
53
|
+
params: [v]
|
|
54
|
+
};
|
|
55
|
+
case "$gte": return {
|
|
56
|
+
sql: `${col} >= ?`,
|
|
57
|
+
params: [v]
|
|
58
|
+
};
|
|
59
|
+
case "$lt": return {
|
|
60
|
+
sql: `${col} < ?`,
|
|
61
|
+
params: [v]
|
|
62
|
+
};
|
|
63
|
+
case "$lte": return {
|
|
64
|
+
sql: `${col} <= ?`,
|
|
65
|
+
params: [v]
|
|
66
|
+
};
|
|
67
|
+
case "$in": {
|
|
68
|
+
const arr = value.map((x) => dialect.toParam(x));
|
|
69
|
+
if (arr.length === 0) return EMPTY_OR;
|
|
70
|
+
const placeholders = arr.map(() => "?").join(", ");
|
|
71
|
+
return {
|
|
72
|
+
sql: `${col} IN (${placeholders})`,
|
|
73
|
+
params: arr
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
case "$nin": {
|
|
77
|
+
const arr = value.map((x) => dialect.toParam(x));
|
|
78
|
+
if (arr.length === 0) return EMPTY_AND;
|
|
79
|
+
const placeholders = arr.map(() => "?").join(", ");
|
|
80
|
+
return {
|
|
81
|
+
sql: `${col} NOT IN (${placeholders})`,
|
|
82
|
+
params: arr
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
case "$exists": return value ? {
|
|
86
|
+
sql: `${col} IS NOT NULL`,
|
|
87
|
+
params: []
|
|
88
|
+
} : {
|
|
89
|
+
sql: `${col} IS NULL`,
|
|
90
|
+
params: []
|
|
91
|
+
};
|
|
92
|
+
case "$regex": return dialect.regex(col, value);
|
|
93
|
+
default: throw new Error(`Unsupported filter operator: ${op}`);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
and(children) {
|
|
97
|
+
if (children.length === 0) return EMPTY_AND;
|
|
98
|
+
return {
|
|
99
|
+
sql: children.map((c) => c.sql).join(" AND "),
|
|
100
|
+
params: children.flatMap((c) => c.params)
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
or(children) {
|
|
104
|
+
if (children.length === 0) return EMPTY_OR;
|
|
105
|
+
return {
|
|
106
|
+
sql: `(${children.map((c) => c.sql).join(" OR ")})`,
|
|
107
|
+
params: children.flatMap((c) => c.params)
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
not(child) {
|
|
111
|
+
return {
|
|
112
|
+
sql: `NOT (${child.sql})`,
|
|
113
|
+
params: child.params
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const visitorCache = new WeakMap();
|
|
119
|
+
function getVisitor(dialect) {
|
|
120
|
+
let visitor = visitorCache.get(dialect);
|
|
121
|
+
if (!visitor) {
|
|
122
|
+
visitor = createFilterVisitor(dialect);
|
|
123
|
+
visitorCache.set(dialect, visitor);
|
|
124
|
+
}
|
|
125
|
+
return visitor;
|
|
126
|
+
}
|
|
127
|
+
function buildWhere(dialect, filter) {
|
|
128
|
+
if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
|
|
129
|
+
return walkFilter(filter, getVisitor(dialect)) ?? EMPTY_AND;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region packages/db-sql-tools/src/common.ts
|
|
134
|
+
function sqlStringLiteral(value) {
|
|
135
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
136
|
+
}
|
|
137
|
+
function toSqlValue(value) {
|
|
138
|
+
if (value === undefined) return null;
|
|
139
|
+
if (value === null) return null;
|
|
140
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
141
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
function refActionToSql(action) {
|
|
145
|
+
switch (action) {
|
|
146
|
+
case "cascade": return "CASCADE";
|
|
147
|
+
case "restrict": return "RESTRICT";
|
|
148
|
+
case "setNull": return "SET NULL";
|
|
149
|
+
case "setDefault": return "SET DEFAULT";
|
|
150
|
+
default: return "NO ACTION";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function defaultValueForType(designType) {
|
|
154
|
+
switch (designType) {
|
|
155
|
+
case "number":
|
|
156
|
+
case "integer": return "0";
|
|
157
|
+
case "boolean": return "0";
|
|
158
|
+
case "decimal": return "'0'";
|
|
159
|
+
default: return "''";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function defaultValueToSqlLiteral(designType, value) {
|
|
163
|
+
switch (designType) {
|
|
164
|
+
case "boolean": return value === "true" || value === "1" ? "1" : "0";
|
|
165
|
+
case "number":
|
|
166
|
+
case "integer":
|
|
167
|
+
case "decimal": {
|
|
168
|
+
const n = Number(value);
|
|
169
|
+
return Number.isFinite(n) ? String(n) : "0";
|
|
170
|
+
}
|
|
171
|
+
default: return sqlStringLiteral(value);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const queryOpToSql = {
|
|
175
|
+
$eq: "=",
|
|
176
|
+
$ne: "!=",
|
|
177
|
+
$gt: ">",
|
|
178
|
+
$gte: ">=",
|
|
179
|
+
$lt: "<",
|
|
180
|
+
$lte: "<="
|
|
181
|
+
};
|
|
182
|
+
function queryNodeToSql(node, resolveFieldRef) {
|
|
183
|
+
if ("$and" in node) {
|
|
184
|
+
const children = node.$and;
|
|
185
|
+
return children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" AND ");
|
|
186
|
+
}
|
|
187
|
+
if ("$or" in node) {
|
|
188
|
+
const children = node.$or;
|
|
189
|
+
return `(${children.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" OR ")})`;
|
|
190
|
+
}
|
|
191
|
+
if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef)})`;
|
|
192
|
+
const comp = node;
|
|
193
|
+
const leftSql = resolveFieldRef(comp.left);
|
|
194
|
+
const sqlOp = queryOpToSql[comp.op] || "=";
|
|
195
|
+
if (comp.right && typeof comp.right === "object" && "field" in comp.right) return `${leftSql} ${sqlOp} ${resolveFieldRef(comp.right)}`;
|
|
196
|
+
if (comp.right === null || comp.right === undefined) return comp.op === "$ne" ? `${leftSql} IS NOT NULL` : `${leftSql} IS NULL`;
|
|
197
|
+
if (typeof comp.right === "string") return `${leftSql} ${sqlOp} '${comp.right.replace(/'/g, "''")}'`;
|
|
198
|
+
return `${leftSql} ${sqlOp} ${comp.right}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region packages/db-sql-tools/src/agg.ts
|
|
203
|
+
const AGG_FN_SQL = {
|
|
204
|
+
sum: "SUM",
|
|
205
|
+
avg: "AVG",
|
|
206
|
+
count: "COUNT",
|
|
207
|
+
min: "MIN",
|
|
208
|
+
max: "MAX"
|
|
209
|
+
};
|
|
210
|
+
function buildAggExpr(dialect, expr) {
|
|
211
|
+
const fn = AGG_FN_SQL[expr.$fn] ?? expr.$fn.toUpperCase();
|
|
212
|
+
const alias = dialect.quoteIdentifier(resolveAlias(expr));
|
|
213
|
+
const field = expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field);
|
|
214
|
+
return `${fn}(${field}) AS ${alias}`;
|
|
215
|
+
}
|
|
216
|
+
function buildAggregateSelect(dialect, table, where, controls) {
|
|
217
|
+
const selectParts = [];
|
|
218
|
+
const plainFields = controls.$select?.asArray;
|
|
219
|
+
if (plainFields) for (const f of plainFields) selectParts.push(dialect.quoteIdentifier(f));
|
|
220
|
+
const aggregates = controls.$select?.aggregates;
|
|
221
|
+
if (aggregates) for (const expr of aggregates) selectParts.push(buildAggExpr(dialect, expr));
|
|
222
|
+
const cols = selectParts.length > 0 ? selectParts.join(", ") : "*";
|
|
223
|
+
let sql = `SELECT ${cols} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
224
|
+
const params = [...where.params];
|
|
225
|
+
const groupBy = controls.$groupBy;
|
|
226
|
+
if (groupBy?.length) {
|
|
227
|
+
const groupCols = groupBy.map((f) => dialect.quoteIdentifier(f)).join(", ");
|
|
228
|
+
sql += ` GROUP BY ${groupCols}`;
|
|
229
|
+
}
|
|
230
|
+
if (controls.$having) {
|
|
231
|
+
const havingFragment = buildWhere(dialect, controls.$having);
|
|
232
|
+
if (havingFragment.sql !== EMPTY_AND.sql) {
|
|
233
|
+
sql += ` HAVING ${havingFragment.sql}`;
|
|
234
|
+
params.push(...havingFragment.params);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (controls.$sort) {
|
|
238
|
+
const orderParts = [];
|
|
239
|
+
for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
|
|
240
|
+
if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
|
|
241
|
+
}
|
|
242
|
+
if (controls.$limit !== undefined) {
|
|
243
|
+
sql += ` LIMIT ?`;
|
|
244
|
+
params.push(controls.$limit);
|
|
245
|
+
}
|
|
246
|
+
if (controls.$skip !== undefined) {
|
|
247
|
+
if (controls.$limit === undefined) sql += ` LIMIT ${dialect.unlimitedLimit}`;
|
|
248
|
+
sql += ` OFFSET ?`;
|
|
249
|
+
params.push(controls.$skip);
|
|
250
|
+
}
|
|
251
|
+
return finalizeParams(dialect, {
|
|
252
|
+
sql,
|
|
253
|
+
params
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
function buildAggregateCount(dialect, table, where, controls) {
|
|
257
|
+
const groupFields = controls.$groupBy;
|
|
258
|
+
if (!groupFields?.length) {
|
|
259
|
+
const sql$1 = `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
260
|
+
return finalizeParams(dialect, {
|
|
261
|
+
sql: sql$1,
|
|
262
|
+
params: where.params
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const groupCols = groupFields.map((f) => dialect.quoteIdentifier(f)).join(", ");
|
|
266
|
+
const sql = `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM (SELECT 1 FROM ${dialect.quoteTable(table)} WHERE ${where.sql} GROUP BY ${groupCols}) AS ${dialect.quoteIdentifier("_groups")}`;
|
|
267
|
+
return finalizeParams(dialect, {
|
|
268
|
+
sql,
|
|
269
|
+
params: where.params
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region packages/db-sql-tools/src/sql-builder.ts
|
|
275
|
+
function buildInsert(dialect, table, data) {
|
|
276
|
+
const keys = Object.keys(data);
|
|
277
|
+
const cols = keys.map((k) => dialect.quoteIdentifier(k)).join(", ");
|
|
278
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
279
|
+
return finalizeParams(dialect, {
|
|
280
|
+
sql: `INSERT INTO ${dialect.quoteTable(table)} (${cols}) VALUES (${placeholders})`,
|
|
281
|
+
params: keys.map((k) => dialect.toValue(data[k]))
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function buildSelect(dialect, table, where, controls) {
|
|
285
|
+
const cols = buildProjection(dialect, controls?.$select);
|
|
286
|
+
let sql = `SELECT ${cols} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
287
|
+
const params = [...where.params];
|
|
288
|
+
if (controls?.$sort) {
|
|
289
|
+
const orderParts = [];
|
|
290
|
+
for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
|
|
291
|
+
if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
|
|
292
|
+
}
|
|
293
|
+
if (controls?.$limit !== undefined) {
|
|
294
|
+
sql += ` LIMIT ?`;
|
|
295
|
+
params.push(controls.$limit);
|
|
296
|
+
}
|
|
297
|
+
if (controls?.$skip !== undefined) {
|
|
298
|
+
if (controls.$limit === undefined) sql += ` LIMIT ${dialect.unlimitedLimit}`;
|
|
299
|
+
sql += ` OFFSET ?`;
|
|
300
|
+
params.push(controls.$skip);
|
|
301
|
+
}
|
|
302
|
+
return finalizeParams(dialect, {
|
|
303
|
+
sql,
|
|
304
|
+
params
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
function buildUpdate(dialect, table, data, where, limit) {
|
|
308
|
+
const setClauses = [];
|
|
309
|
+
const params = [];
|
|
310
|
+
for (const [key, value] of Object.entries(data)) {
|
|
311
|
+
setClauses.push(`${dialect.quoteIdentifier(key)} = ?`);
|
|
312
|
+
params.push(dialect.toValue(value));
|
|
313
|
+
}
|
|
314
|
+
let sql = `UPDATE ${dialect.quoteTable(table)} SET ${setClauses.join(", ")} WHERE ${where.sql}`;
|
|
315
|
+
if (limit !== undefined) sql += ` LIMIT ${limit}`;
|
|
316
|
+
return finalizeParams(dialect, {
|
|
317
|
+
sql,
|
|
318
|
+
params: [...params, ...where.params]
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
function buildDelete(dialect, table, where, limit) {
|
|
322
|
+
let sql = `DELETE FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
|
|
323
|
+
if (limit !== undefined) sql += ` LIMIT ${limit}`;
|
|
324
|
+
return finalizeParams(dialect, {
|
|
325
|
+
sql,
|
|
326
|
+
params: where.params
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function buildProjection(dialect, select) {
|
|
330
|
+
const fields = select?.asArray;
|
|
331
|
+
if (!fields) return "*";
|
|
332
|
+
let sql = "";
|
|
333
|
+
for (let i = 0; i < fields.length; i++) {
|
|
334
|
+
if (i > 0) sql += ", ";
|
|
335
|
+
sql += dialect.quoteIdentifier(fields[i]);
|
|
336
|
+
}
|
|
337
|
+
return sql || "*";
|
|
338
|
+
}
|
|
339
|
+
/** Builds the SQL expression for a single aggregate column. */ function buildAggColExpr(dialect, c) {
|
|
340
|
+
const fn = AGG_FN_SQL[c.aggFn] ?? c.aggFn.toUpperCase();
|
|
341
|
+
const arg = c.aggField === "*" ? "*" : `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`;
|
|
342
|
+
return `${fn}(${arg})`;
|
|
343
|
+
}
|
|
344
|
+
function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
|
|
345
|
+
const selectCols = columns.map((c) => {
|
|
346
|
+
if (c.aggFn) return `${buildAggColExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
347
|
+
return `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
|
|
348
|
+
}).join(", ");
|
|
349
|
+
let sql = `${dialect.createViewPrefix} ${dialect.quoteTable(viewName)} AS SELECT ${selectCols} FROM ${dialect.quoteIdentifier(plan.entryTable)}`;
|
|
350
|
+
for (const join of plan.joins) {
|
|
351
|
+
const onClause = queryNodeToSql(join.condition, resolveFieldRef);
|
|
352
|
+
sql += ` JOIN ${dialect.quoteIdentifier(join.targetTable)} ON ${onClause}`;
|
|
353
|
+
}
|
|
354
|
+
if (plan.filter) {
|
|
355
|
+
const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
|
|
356
|
+
sql += ` WHERE ${whereClause}`;
|
|
357
|
+
}
|
|
358
|
+
const hasAggregates = columns.some((c) => c.aggFn);
|
|
359
|
+
if (hasAggregates) {
|
|
360
|
+
const dimensionCols = columns.filter((c) => !c.aggFn);
|
|
361
|
+
if (dimensionCols.length > 0) {
|
|
362
|
+
const groupByCols = dimensionCols.map((c) => `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`).join(", ");
|
|
363
|
+
sql += ` GROUP BY ${groupByCols}`;
|
|
364
|
+
}
|
|
365
|
+
if (plan.having) {
|
|
366
|
+
const columnMap = new Map();
|
|
367
|
+
for (const c of columns) columnMap.set(c.viewColumn, c);
|
|
368
|
+
const havingResolver = (ref) => {
|
|
369
|
+
if (!ref.type) {
|
|
370
|
+
const col = columnMap.get(ref.field);
|
|
371
|
+
if (col?.aggFn) return buildAggColExpr(dialect, col);
|
|
372
|
+
if (col) return `${dialect.quoteIdentifier(col.sourceTable)}.${dialect.quoteIdentifier(col.sourceColumn)}`;
|
|
373
|
+
}
|
|
374
|
+
return resolveFieldRef(ref);
|
|
375
|
+
};
|
|
376
|
+
const havingClause = queryNodeToSql(plan.having, havingResolver);
|
|
377
|
+
sql += ` HAVING ${havingClause}`;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return sql;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
//#endregion
|
|
384
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, queryNodeToSql, queryOpToSql, refActionToSql, sqlStringLiteral, toSqlValue };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atscript/db-sql-tools",
|
|
3
|
+
"version": "0.1.38",
|
|
4
|
+
"description": "Shared SQL builder utilities for @atscript database adapters.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"atscript",
|
|
7
|
+
"database",
|
|
8
|
+
"sql"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/moostjs/atscript/tree/main/packages/db-sql-tools#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/moostjs/atscript/issues"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Artem Maltsev",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/moostjs/atscript.git",
|
|
19
|
+
"directory": "packages/db-sql-tools"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "dist/index.mjs",
|
|
26
|
+
"types": "dist/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.mjs",
|
|
31
|
+
"require": "./dist/index.cjs"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"vitest": "3.2.4"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@uniqu/core": "^0.1.2",
|
|
40
|
+
"@atscript/db": "^0.1.38"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"pub": "pnpm publish --access public",
|
|
44
|
+
"test": "vitest"
|
|
45
|
+
}
|
|
46
|
+
}
|