@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 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
@@ -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
+ }