@atscript/db-sql-tools 0.1.39 → 0.1.40

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://atscript.moost.org/logo.svg" alt="Atscript" width="120" />
2
+ <img src="https://atscript.dev/logo.svg" alt="Atscript" width="120" />
3
3
  </p>
4
4
 
5
5
  <h1 align="center">@atscript/db-sql-tools</h1>
@@ -9,7 +9,7 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <a href="https://atscript.moost.org">Documentation</a> · <a href="https://atscript.moost.org/db/adapters/">DB Adapters</a>
12
+ <a href="https://db.atscript.dev">Documentation</a> · <a href="https://db.atscript.dev/adapters/">DB Adapters</a>
13
13
  </p>
14
14
 
15
15
  ---
@@ -35,8 +35,8 @@ pnpm add @atscript/db-sql-tools
35
35
 
36
36
  ## Documentation
37
37
 
38
- - [DB Adapters Guide](https://atscript.moost.org/db/adapters/)
39
- - [Full Documentation](https://atscript.moost.org)
38
+ - [DB Adapters Guide](https://db.atscript.dev/adapters/)
39
+ - [Full Documentation](https://db.atscript.dev)
40
40
 
41
41
  ## License
42
42
 
package/dist/index.cjs CHANGED
@@ -1,37 +1,16 @@
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
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _uniqu_core = require("@uniqu/core");
3
+ let _atscript_db_agg = require("@atscript/db/agg");
4
+ //#region src/dialect.ts
5
+ /**
6
+ * Replaces positional `?` placeholders with dialect-specific numbered placeholders
7
+ * (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
8
+ */
29
9
  function finalizeParams(dialect, fragment) {
30
10
  if (!dialect.paramPlaceholder) return fragment;
31
11
  let idx = 0;
32
- const sql = fragment.sql.replace(/\?/g, () => dialect.paramPlaceholder(++idx));
33
12
  return {
34
- sql,
13
+ sql: fragment.sql.replace(/\?/g, () => dialect.paramPlaceholder(++idx)),
35
14
  params: fragment.params
36
15
  };
37
16
  }
@@ -43,16 +22,18 @@ const EMPTY_OR = {
43
22
  sql: "0=1",
44
23
  params: []
45
24
  };
46
-
47
25
  //#endregion
48
- //#region packages/db-sql-tools/src/filter-builder.ts
26
+ //#region src/filter-builder.ts
27
+ /**
28
+ * Creates a dialect-specific filter visitor for `walkFilter`.
29
+ */
49
30
  function createFilterVisitor(dialect) {
50
31
  return {
51
32
  comparison(field, op, value) {
52
33
  const col = dialect.quoteIdentifier(field);
53
34
  const v = dialect.toParam(value);
54
35
  switch (op) {
55
- case "$eq": {
36
+ case "$eq":
56
37
  if (v === null) return {
57
38
  sql: `${col} IS NULL`,
58
39
  params: []
@@ -61,8 +42,7 @@ function createFilterVisitor(dialect) {
61
42
  sql: `${col} = ?`,
62
43
  params: [v]
63
44
  };
64
- }
65
- case "$ne": {
45
+ case "$ne":
66
46
  if (v === null) return {
67
47
  sql: `${col} IS NOT NULL`,
68
48
  params: []
@@ -71,7 +51,6 @@ function createFilterVisitor(dialect) {
71
51
  sql: `${col} != ?`,
72
52
  params: [v]
73
53
  };
74
- }
75
54
  case "$gt": return {
76
55
  sql: `${col} > ?`,
77
56
  params: [v]
@@ -91,18 +70,16 @@ function createFilterVisitor(dialect) {
91
70
  case "$in": {
92
71
  const arr = value.map((x) => dialect.toParam(x));
93
72
  if (arr.length === 0) return EMPTY_OR;
94
- const placeholders = arr.map(() => "?").join(", ");
95
73
  return {
96
- sql: `${col} IN (${placeholders})`,
74
+ sql: `${col} IN (${arr.map(() => "?").join(", ")})`,
97
75
  params: arr
98
76
  };
99
77
  }
100
78
  case "$nin": {
101
79
  const arr = value.map((x) => dialect.toParam(x));
102
80
  if (arr.length === 0) return EMPTY_AND;
103
- const placeholders = arr.map(() => "?").join(", ");
104
81
  return {
105
- sql: `${col} NOT IN (${placeholders})`,
82
+ sql: `${col} NOT IN (${arr.map(() => "?").join(", ")})`,
106
83
  params: arr
107
84
  };
108
85
  }
@@ -114,7 +91,7 @@ function createFilterVisitor(dialect) {
114
91
  params: []
115
92
  };
116
93
  case "$regex": return dialect.regex(col, value);
117
- default: throw new Error(`Unsupported filter operator: ${op}`);
94
+ default: throw new Error(`Unsupported filter operator: ${String(op)}`);
118
95
  }
119
96
  },
120
97
  and(children) {
@@ -139,7 +116,7 @@ function createFilterVisitor(dialect) {
139
116
  }
140
117
  };
141
118
  }
142
- const visitorCache = new WeakMap();
119
+ const visitorCache = /* @__PURE__ */ new WeakMap();
143
120
  function getVisitor(dialect) {
144
121
  let visitor = visitorCache.get(dialect);
145
122
  if (!visitor) {
@@ -148,18 +125,22 @@ function getVisitor(dialect) {
148
125
  }
149
126
  return visitor;
150
127
  }
128
+ /**
129
+ * Translates a filter expression into a parameterized SQL WHERE clause.
130
+ */
151
131
  function buildWhere(dialect, filter) {
152
132
  if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
153
- return (0, __uniqu_core.walkFilter)(filter, getVisitor(dialect)) ?? EMPTY_AND;
133
+ return (0, _uniqu_core.walkFilter)(filter, getVisitor(dialect)) ?? EMPTY_AND;
154
134
  }
155
-
156
135
  //#endregion
157
- //#region packages/db-sql-tools/src/common.ts
136
+ //#region src/common.ts
137
+ /** Formats a string value as a SQL literal with single-quote escaping. */
158
138
  function sqlStringLiteral(value) {
159
139
  return `'${value.replace(/'/g, "''")}'`;
160
140
  }
141
+ /** Converts a JS value to a SQL-bindable parameter. Objects/arrays -> JSON, booleans -> 0/1. */
161
142
  function toSqlValue(value) {
162
- if (value === undefined) return null;
143
+ if (value === void 0) return null;
163
144
  if (value === null) return null;
164
145
  if (typeof value === "object") return JSON.stringify(value);
165
146
  if (typeof value === "boolean") return value ? 1 : 0;
@@ -174,6 +155,7 @@ function refActionToSql(action) {
174
155
  default: return "NO ACTION";
175
156
  }
176
157
  }
158
+ /** Returns a safe SQL DEFAULT literal for a given design type. */
177
159
  function defaultValueForType(designType) {
178
160
  switch (designType) {
179
161
  case "number":
@@ -183,6 +165,11 @@ function defaultValueForType(designType) {
183
165
  default: return "''";
184
166
  }
185
167
  }
168
+ /**
169
+ * Converts a stored default value string to a SQL DEFAULT literal,
170
+ * respecting the field's designType. Booleans become 0/1, numbers stay unquoted,
171
+ * strings are single-quote-escaped.
172
+ */
186
173
  function defaultValueToSqlLiteral(designType, value) {
187
174
  switch (designType) {
188
175
  case "boolean": return value === "true" || value === "1" ? "1" : "0";
@@ -203,27 +190,23 @@ const queryOpToSql = {
203
190
  $lt: "<",
204
191
  $lte: "<="
205
192
  };
193
+ /**
194
+ * Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
195
+ */
206
196
  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
- }
197
+ if ("$and" in node) return node.$and.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" AND ");
198
+ if ("$or" in node) return `(${node.$or.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" OR ")})`;
215
199
  if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef)})`;
216
200
  const comp = node;
217
201
  const leftSql = resolveFieldRef(comp.left);
218
202
  const sqlOp = queryOpToSql[comp.op] || "=";
219
203
  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`;
204
+ if (comp.right === null || comp.right === void 0) return comp.op === "$ne" ? `${leftSql} IS NOT NULL` : `${leftSql} IS NULL`;
221
205
  if (typeof comp.right === "string") return `${leftSql} ${sqlOp} '${comp.right.replace(/'/g, "''")}'`;
222
206
  return `${leftSql} ${sqlOp} ${comp.right}`;
223
207
  }
224
-
225
208
  //#endregion
226
- //#region packages/db-sql-tools/src/agg.ts
209
+ //#region src/agg.ts
227
210
  const AGG_FN_SQL = {
228
211
  sum: "SUM",
229
212
  avg: "AVG",
@@ -233,18 +216,19 @@ const AGG_FN_SQL = {
233
216
  };
234
217
  function buildAggExpr(dialect, expr) {
235
218
  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}`;
219
+ const alias = dialect.quoteIdentifier((0, _atscript_db_agg.resolveAlias)(expr));
220
+ return `${fn}(${expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field)}) AS ${alias}`;
239
221
  }
222
+ /**
223
+ * Builds a SELECT ... GROUP BY statement with aggregate functions.
224
+ */
240
225
  function buildAggregateSelect(dialect, table, where, controls) {
241
226
  const selectParts = [];
242
227
  const plainFields = controls.$select?.asArray;
243
228
  if (plainFields) for (const f of plainFields) selectParts.push(dialect.quoteIdentifier(f));
244
229
  const aggregates = controls.$select?.aggregates;
245
230
  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}`;
231
+ let sql = `SELECT ${selectParts.length > 0 ? selectParts.join(", ") : "*"} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
248
232
  const params = [...where.params];
249
233
  const groupBy = controls.$groupBy;
250
234
  if (groupBy?.length) {
@@ -263,12 +247,12 @@ function buildAggregateSelect(dialect, table, where, controls) {
263
247
  for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
264
248
  if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
265
249
  }
266
- if (controls.$limit !== undefined) {
250
+ if (controls.$limit !== void 0) {
267
251
  sql += ` LIMIT ?`;
268
252
  params.push(controls.$limit);
269
253
  }
270
- if (controls.$skip !== undefined) {
271
- if (controls.$limit === undefined) sql += ` LIMIT ${dialect.unlimitedLimit}`;
254
+ if (controls.$skip !== void 0) {
255
+ if (controls.$limit === void 0) sql += ` LIMIT ${dialect.unlimitedLimit}`;
272
256
  sql += ` OFFSET ?`;
273
257
  params.push(controls.$skip);
274
258
  }
@@ -277,25 +261,27 @@ function buildAggregateSelect(dialect, table, where, controls) {
277
261
  params
278
262
  });
279
263
  }
264
+ /**
265
+ * Builds a COUNT query for the number of distinct groups.
266
+ * Returns `{ count: N }` when executed.
267
+ */
280
268
  function buildAggregateCount(dialect, table, where, controls) {
281
269
  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
- }
270
+ if (!groupFields?.length) return finalizeParams(dialect, {
271
+ sql: `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`,
272
+ params: where.params
273
+ });
289
274
  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
275
  return finalizeParams(dialect, {
292
- sql,
276
+ sql: `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM (SELECT 1 FROM ${dialect.quoteTable(table)} WHERE ${where.sql} GROUP BY ${groupCols}) AS ${dialect.quoteIdentifier("_groups")}`,
293
277
  params: where.params
294
278
  });
295
279
  }
296
-
297
280
  //#endregion
298
- //#region packages/db-sql-tools/src/sql-builder.ts
281
+ //#region src/sql-builder.ts
282
+ /**
283
+ * Builds an INSERT statement.
284
+ */
299
285
  function buildInsert(dialect, table, data) {
300
286
  const keys = Object.keys(data);
301
287
  const cols = keys.map((k) => dialect.quoteIdentifier(k)).join(", ");
@@ -305,21 +291,23 @@ function buildInsert(dialect, table, data) {
305
291
  params: keys.map((k) => dialect.toValue(data[k]))
306
292
  });
307
293
  }
294
+ /**
295
+ * Builds a SELECT statement with optional sort, limit, offset, projection.
296
+ */
308
297
  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}`;
298
+ let sql = `SELECT ${buildProjection(dialect, controls?.$select)} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
311
299
  const params = [...where.params];
312
300
  if (controls?.$sort) {
313
301
  const orderParts = [];
314
302
  for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
315
303
  if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
316
304
  }
317
- if (controls?.$limit !== undefined) {
305
+ if (controls?.$limit !== void 0) {
318
306
  sql += ` LIMIT ?`;
319
307
  params.push(controls.$limit);
320
308
  }
321
- if (controls?.$skip !== undefined) {
322
- if (controls.$limit === undefined) sql += ` LIMIT ${dialect.unlimitedLimit}`;
309
+ if (controls?.$skip !== void 0) {
310
+ if (controls.$limit === void 0) sql += ` LIMIT ${dialect.unlimitedLimit}`;
323
311
  sql += ` OFFSET ?`;
324
312
  params.push(controls.$skip);
325
313
  }
@@ -328,28 +316,47 @@ function buildSelect(dialect, table, where, controls) {
328
316
  params
329
317
  });
330
318
  }
331
- function buildUpdate(dialect, table, data, where, limit) {
319
+ /**
320
+ * Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
321
+ */
322
+ function buildUpdate(dialect, table, data, where, limit, ops) {
332
323
  const setClauses = [];
333
324
  const params = [];
334
325
  for (const [key, value] of Object.entries(data)) {
335
326
  setClauses.push(`${dialect.quoteIdentifier(key)} = ?`);
336
327
  params.push(dialect.toValue(value));
337
328
  }
329
+ if (ops?.inc) for (const key in ops.inc) {
330
+ const col = dialect.quoteIdentifier(key);
331
+ setClauses.push(`${col} = ${col} + ?`);
332
+ params.push(ops.inc[key]);
333
+ }
334
+ if (ops?.mul) for (const key in ops.mul) {
335
+ const col = dialect.quoteIdentifier(key);
336
+ setClauses.push(`${col} = ${col} * ?`);
337
+ params.push(ops.mul[key]);
338
+ }
338
339
  let sql = `UPDATE ${dialect.quoteTable(table)} SET ${setClauses.join(", ")} WHERE ${where.sql}`;
339
- if (limit !== undefined) sql += ` LIMIT ${limit}`;
340
+ if (limit !== void 0) sql += ` LIMIT ${limit}`;
340
341
  return finalizeParams(dialect, {
341
342
  sql,
342
343
  params: [...params, ...where.params]
343
344
  });
344
345
  }
346
+ /**
347
+ * Builds a DELETE ... WHERE statement with optional LIMIT.
348
+ */
345
349
  function buildDelete(dialect, table, where, limit) {
346
350
  let sql = `DELETE FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
347
- if (limit !== undefined) sql += ` LIMIT ${limit}`;
351
+ if (limit !== void 0) sql += ` LIMIT ${limit}`;
348
352
  return finalizeParams(dialect, {
349
353
  sql,
350
354
  params: where.params
351
355
  });
352
356
  }
357
+ /**
358
+ * Builds a column projection (SELECT clause fields).
359
+ */
353
360
  function buildProjection(dialect, select) {
354
361
  const fields = select?.asArray;
355
362
  if (!fields) return "*";
@@ -360,11 +367,13 @@ function buildProjection(dialect, select) {
360
367
  }
361
368
  return sql || "*";
362
369
  }
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})`;
370
+ /** Builds the SQL expression for a single aggregate column. */
371
+ function buildAggColExpr(dialect, c) {
372
+ return `${AGG_FN_SQL[c.aggFn] ?? c.aggFn.toUpperCase()}(${c.aggField === "*" ? "*" : `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`})`;
367
373
  }
374
+ /**
375
+ * Builds a CREATE VIEW statement from a view plan and column mappings.
376
+ */
368
377
  function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
369
378
  const selectCols = columns.map((c) => {
370
379
  if (c.aggFn) return `${buildAggColExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
@@ -379,15 +388,14 @@ function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
379
388
  const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
380
389
  sql += ` WHERE ${whereClause}`;
381
390
  }
382
- const hasAggregates = columns.some((c) => c.aggFn);
383
- if (hasAggregates) {
391
+ if (columns.some((c) => c.aggFn)) {
384
392
  const dimensionCols = columns.filter((c) => !c.aggFn);
385
393
  if (dimensionCols.length > 0) {
386
394
  const groupByCols = dimensionCols.map((c) => `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`).join(", ");
387
395
  sql += ` GROUP BY ${groupByCols}`;
388
396
  }
389
397
  if (plan.having) {
390
- const columnMap = new Map();
398
+ const columnMap = /* @__PURE__ */ new Map();
391
399
  for (const c of columns) columnMap.set(c.viewColumn, c);
392
400
  const havingResolver = (ref) => {
393
401
  if (!ref.type) {
@@ -403,26 +411,25 @@ function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
403
411
  }
404
412
  return sql;
405
413
  }
406
-
407
414
  //#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
415
+ exports.AGG_FN_SQL = AGG_FN_SQL;
416
+ exports.EMPTY_AND = EMPTY_AND;
417
+ exports.EMPTY_OR = EMPTY_OR;
418
+ exports.buildAggregateCount = buildAggregateCount;
419
+ exports.buildAggregateSelect = buildAggregateSelect;
420
+ exports.buildCreateView = buildCreateView;
421
+ exports.buildDelete = buildDelete;
422
+ exports.buildInsert = buildInsert;
423
+ exports.buildProjection = buildProjection;
424
+ exports.buildSelect = buildSelect;
425
+ exports.buildUpdate = buildUpdate;
426
+ exports.buildWhere = buildWhere;
427
+ exports.createFilterVisitor = createFilterVisitor;
428
+ exports.defaultValueForType = defaultValueForType;
429
+ exports.defaultValueToSqlLiteral = defaultValueToSqlLiteral;
430
+ exports.finalizeParams = finalizeParams;
431
+ exports.queryNodeToSql = queryNodeToSql;
432
+ exports.queryOpToSql = queryOpToSql;
433
+ exports.refActionToSql = refActionToSql;
434
+ exports.sqlStringLiteral = sqlStringLiteral;
435
+ exports.toSqlValue = toSqlValue;
@@ -1,27 +1,28 @@
1
- import { FilterExpr, FilterVisitor } from '@uniqu/core';
2
- import { TViewPlan, TViewColumnMapping, AtscriptQueryFieldRef, UniquSelect, DbControls, AtscriptQueryNode, TDbReferentialAction } from '@atscript/db';
1
+ import { FilterExpr, FilterVisitor } from "@uniqu/core";
2
+ import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbReferentialAction, TFieldOps, TViewColumnMapping, TViewPlan, UniquSelect } from "@atscript/db";
3
3
 
4
+ //#region src/dialect.d.ts
4
5
  interface TSqlFragment {
5
- sql: string;
6
- params: unknown[];
6
+ sql: string;
7
+ params: unknown[];
7
8
  }
8
9
  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;
10
+ /** Quotes a column/table name */
11
+ quoteIdentifier(name: string): string;
12
+ /** Quotes a possibly schema-qualified table name */
13
+ quoteTable(name: string): string;
14
+ /** SQL literal for unlimited LIMIT (SQLite: '-1', MySQL: '18446744073709551615') */
15
+ unlimitedLimit: string;
16
+ /** Convert JS value to SQL-bindable param for DML */
17
+ toValue(value: unknown): unknown;
18
+ /** Convert JS value to SQL-bindable param for filters (lighter) */
19
+ toParam(value: unknown): unknown;
20
+ /** Handle $regex filter */
21
+ regex(quotedCol: string, value: unknown): TSqlFragment;
22
+ /** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
23
+ createViewPrefix: string;
24
+ /** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
25
+ paramPlaceholder?: (index: number) => string;
25
26
  }
26
27
  /**
27
28
  * Replaces positional `?` placeholders with dialect-specific numbered placeholders
@@ -30,7 +31,8 @@ interface SqlDialect {
30
31
  declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TSqlFragment;
31
32
  declare const EMPTY_AND: TSqlFragment;
32
33
  declare const EMPTY_OR: TSqlFragment;
33
-
34
+ //#endregion
35
+ //#region src/filter-builder.d.ts
34
36
  /**
35
37
  * Creates a dialect-specific filter visitor for `walkFilter`.
36
38
  */
@@ -39,7 +41,8 @@ declare function createFilterVisitor(dialect: SqlDialect): FilterVisitor<TSqlFra
39
41
  * Translates a filter expression into a parameterized SQL WHERE clause.
40
42
  */
41
43
  declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragment;
42
-
44
+ //#endregion
45
+ //#region src/sql-builder.d.ts
43
46
  /**
44
47
  * Builds an INSERT statement.
45
48
  */
@@ -51,7 +54,7 @@ declare function buildSelect(dialect: SqlDialect, table: string, where: TSqlFrag
51
54
  /**
52
55
  * Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
53
56
  */
54
- declare function buildUpdate(dialect: SqlDialect, table: string, data: Record<string, unknown>, where: TSqlFragment, limit?: number): TSqlFragment;
57
+ declare function buildUpdate(dialect: SqlDialect, table: string, data: Record<string, unknown>, where: TSqlFragment, limit?: number, ops?: TFieldOps): TSqlFragment;
55
58
  /**
56
59
  * Builds a DELETE ... WHERE statement with optional LIMIT.
57
60
  */
@@ -64,7 +67,8 @@ declare function buildProjection(dialect: SqlDialect, select?: UniquSelect): str
64
67
  * Builds a CREATE VIEW statement from a view plan and column mappings.
65
68
  */
66
69
  declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
67
-
70
+ //#endregion
71
+ //#region src/common.d.ts
68
72
  /** Formats a string value as a SQL literal with single-quote escaping. */
69
73
  declare function sqlStringLiteral(value: string): string;
70
74
  /** Converts a JS value to a SQL-bindable parameter. Objects/arrays -> JSON, booleans -> 0/1. */
@@ -83,7 +87,8 @@ declare const queryOpToSql: Record<string, string>;
83
87
  * Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
84
88
  */
85
89
  declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
86
-
90
+ //#endregion
91
+ //#region src/agg.d.ts
87
92
  declare const AGG_FN_SQL: Record<string, string>;
88
93
  /**
89
94
  * Builds a SELECT ... GROUP BY statement with aggregate functions.
@@ -94,6 +99,5 @@ declare function buildAggregateSelect(dialect: SqlDialect, table: string, where:
94
99
  * Returns `{ count: N }` when executed.
95
100
  */
96
101
  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 };
102
+ //#endregion
103
+ export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, type SqlDialect, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, queryNodeToSql, queryOpToSql, refActionToSql, sqlStringLiteral, toSqlValue };
@@ -0,0 +1,103 @@
1
+ import { FilterExpr, FilterVisitor } from "@uniqu/core";
2
+ import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbReferentialAction, TFieldOps, TViewColumnMapping, TViewPlan, UniquSelect } from "@atscript/db";
3
+
4
+ //#region src/dialect.d.ts
5
+ interface TSqlFragment {
6
+ sql: string;
7
+ params: unknown[];
8
+ }
9
+ interface SqlDialect {
10
+ /** Quotes a column/table name */
11
+ quoteIdentifier(name: string): string;
12
+ /** Quotes a possibly schema-qualified table name */
13
+ quoteTable(name: string): string;
14
+ /** SQL literal for unlimited LIMIT (SQLite: '-1', MySQL: '18446744073709551615') */
15
+ unlimitedLimit: string;
16
+ /** Convert JS value to SQL-bindable param for DML */
17
+ toValue(value: unknown): unknown;
18
+ /** Convert JS value to SQL-bindable param for filters (lighter) */
19
+ toParam(value: unknown): unknown;
20
+ /** Handle $regex filter */
21
+ regex(quotedCol: string, value: unknown): TSqlFragment;
22
+ /** e.g. 'CREATE VIEW IF NOT EXISTS' or 'CREATE OR REPLACE VIEW' */
23
+ createViewPrefix: string;
24
+ /** Returns a parameter placeholder for the given 1-based index. When absent, '?' is used. */
25
+ paramPlaceholder?: (index: number) => string;
26
+ }
27
+ /**
28
+ * Replaces positional `?` placeholders with dialect-specific numbered placeholders
29
+ * (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
30
+ */
31
+ declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TSqlFragment;
32
+ declare const EMPTY_AND: TSqlFragment;
33
+ declare const EMPTY_OR: TSqlFragment;
34
+ //#endregion
35
+ //#region src/filter-builder.d.ts
36
+ /**
37
+ * Creates a dialect-specific filter visitor for `walkFilter`.
38
+ */
39
+ declare function createFilterVisitor(dialect: SqlDialect): FilterVisitor<TSqlFragment>;
40
+ /**
41
+ * Translates a filter expression into a parameterized SQL WHERE clause.
42
+ */
43
+ declare function buildWhere(dialect: SqlDialect, filter: FilterExpr): TSqlFragment;
44
+ //#endregion
45
+ //#region src/sql-builder.d.ts
46
+ /**
47
+ * Builds an INSERT statement.
48
+ */
49
+ declare function buildInsert(dialect: SqlDialect, table: string, data: Record<string, unknown>): TSqlFragment;
50
+ /**
51
+ * Builds a SELECT statement with optional sort, limit, offset, projection.
52
+ */
53
+ declare function buildSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls?: DbControls): TSqlFragment;
54
+ /**
55
+ * Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
56
+ */
57
+ declare function buildUpdate(dialect: SqlDialect, table: string, data: Record<string, unknown>, where: TSqlFragment, limit?: number, ops?: TFieldOps): TSqlFragment;
58
+ /**
59
+ * Builds a DELETE ... WHERE statement with optional LIMIT.
60
+ */
61
+ declare function buildDelete(dialect: SqlDialect, table: string, where: TSqlFragment, limit?: number): TSqlFragment;
62
+ /**
63
+ * Builds a column projection (SELECT clause fields).
64
+ */
65
+ declare function buildProjection(dialect: SqlDialect, select?: UniquSelect): string;
66
+ /**
67
+ * Builds a CREATE VIEW statement from a view plan and column mappings.
68
+ */
69
+ declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TViewPlan, columns: TViewColumnMapping[], resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
70
+ //#endregion
71
+ //#region src/common.d.ts
72
+ /** Formats a string value as a SQL literal with single-quote escaping. */
73
+ declare function sqlStringLiteral(value: string): string;
74
+ /** Converts a JS value to a SQL-bindable parameter. Objects/arrays -> JSON, booleans -> 0/1. */
75
+ declare function toSqlValue(value: unknown): unknown;
76
+ declare function refActionToSql(action: TDbReferentialAction): string;
77
+ /** Returns a safe SQL DEFAULT literal for a given design type. */
78
+ declare function defaultValueForType(designType: string): string;
79
+ /**
80
+ * Converts a stored default value string to a SQL DEFAULT literal,
81
+ * respecting the field's designType. Booleans become 0/1, numbers stay unquoted,
82
+ * strings are single-quote-escaped.
83
+ */
84
+ declare function defaultValueToSqlLiteral(designType: string, value: string): string;
85
+ declare const queryOpToSql: Record<string, string>;
86
+ /**
87
+ * Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
88
+ */
89
+ declare function queryNodeToSql(node: AtscriptQueryNode, resolveFieldRef: (ref: AtscriptQueryFieldRef) => string): string;
90
+ //#endregion
91
+ //#region src/agg.d.ts
92
+ declare const AGG_FN_SQL: Record<string, string>;
93
+ /**
94
+ * Builds a SELECT ... GROUP BY statement with aggregate functions.
95
+ */
96
+ declare function buildAggregateSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls): TSqlFragment;
97
+ /**
98
+ * Builds a COUNT query for the number of distinct groups.
99
+ * Returns `{ count: N }` when executed.
100
+ */
101
+ declare function buildAggregateCount(dialect: SqlDialect, table: string, where: TSqlFragment, controls: DbControls): TSqlFragment;
102
+ //#endregion
103
+ export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, type SqlDialect, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, queryNodeToSql, queryOpToSql, refActionToSql, sqlStringLiteral, toSqlValue };
package/dist/index.mjs CHANGED
@@ -1,13 +1,15 @@
1
1
  import { walkFilter } from "@uniqu/core";
2
2
  import { resolveAlias } from "@atscript/db/agg";
3
-
4
- //#region packages/db-sql-tools/src/dialect.ts
3
+ //#region src/dialect.ts
4
+ /**
5
+ * Replaces positional `?` placeholders with dialect-specific numbered placeholders
6
+ * (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
7
+ */
5
8
  function finalizeParams(dialect, fragment) {
6
9
  if (!dialect.paramPlaceholder) return fragment;
7
10
  let idx = 0;
8
- const sql = fragment.sql.replace(/\?/g, () => dialect.paramPlaceholder(++idx));
9
11
  return {
10
- sql,
12
+ sql: fragment.sql.replace(/\?/g, () => dialect.paramPlaceholder(++idx)),
11
13
  params: fragment.params
12
14
  };
13
15
  }
@@ -19,16 +21,18 @@ const EMPTY_OR = {
19
21
  sql: "0=1",
20
22
  params: []
21
23
  };
22
-
23
24
  //#endregion
24
- //#region packages/db-sql-tools/src/filter-builder.ts
25
+ //#region src/filter-builder.ts
26
+ /**
27
+ * Creates a dialect-specific filter visitor for `walkFilter`.
28
+ */
25
29
  function createFilterVisitor(dialect) {
26
30
  return {
27
31
  comparison(field, op, value) {
28
32
  const col = dialect.quoteIdentifier(field);
29
33
  const v = dialect.toParam(value);
30
34
  switch (op) {
31
- case "$eq": {
35
+ case "$eq":
32
36
  if (v === null) return {
33
37
  sql: `${col} IS NULL`,
34
38
  params: []
@@ -37,8 +41,7 @@ function createFilterVisitor(dialect) {
37
41
  sql: `${col} = ?`,
38
42
  params: [v]
39
43
  };
40
- }
41
- case "$ne": {
44
+ case "$ne":
42
45
  if (v === null) return {
43
46
  sql: `${col} IS NOT NULL`,
44
47
  params: []
@@ -47,7 +50,6 @@ function createFilterVisitor(dialect) {
47
50
  sql: `${col} != ?`,
48
51
  params: [v]
49
52
  };
50
- }
51
53
  case "$gt": return {
52
54
  sql: `${col} > ?`,
53
55
  params: [v]
@@ -67,18 +69,16 @@ function createFilterVisitor(dialect) {
67
69
  case "$in": {
68
70
  const arr = value.map((x) => dialect.toParam(x));
69
71
  if (arr.length === 0) return EMPTY_OR;
70
- const placeholders = arr.map(() => "?").join(", ");
71
72
  return {
72
- sql: `${col} IN (${placeholders})`,
73
+ sql: `${col} IN (${arr.map(() => "?").join(", ")})`,
73
74
  params: arr
74
75
  };
75
76
  }
76
77
  case "$nin": {
77
78
  const arr = value.map((x) => dialect.toParam(x));
78
79
  if (arr.length === 0) return EMPTY_AND;
79
- const placeholders = arr.map(() => "?").join(", ");
80
80
  return {
81
- sql: `${col} NOT IN (${placeholders})`,
81
+ sql: `${col} NOT IN (${arr.map(() => "?").join(", ")})`,
82
82
  params: arr
83
83
  };
84
84
  }
@@ -90,7 +90,7 @@ function createFilterVisitor(dialect) {
90
90
  params: []
91
91
  };
92
92
  case "$regex": return dialect.regex(col, value);
93
- default: throw new Error(`Unsupported filter operator: ${op}`);
93
+ default: throw new Error(`Unsupported filter operator: ${String(op)}`);
94
94
  }
95
95
  },
96
96
  and(children) {
@@ -115,7 +115,7 @@ function createFilterVisitor(dialect) {
115
115
  }
116
116
  };
117
117
  }
118
- const visitorCache = new WeakMap();
118
+ const visitorCache = /* @__PURE__ */ new WeakMap();
119
119
  function getVisitor(dialect) {
120
120
  let visitor = visitorCache.get(dialect);
121
121
  if (!visitor) {
@@ -124,18 +124,22 @@ function getVisitor(dialect) {
124
124
  }
125
125
  return visitor;
126
126
  }
127
+ /**
128
+ * Translates a filter expression into a parameterized SQL WHERE clause.
129
+ */
127
130
  function buildWhere(dialect, filter) {
128
131
  if (!filter || Object.keys(filter).length === 0) return EMPTY_AND;
129
132
  return walkFilter(filter, getVisitor(dialect)) ?? EMPTY_AND;
130
133
  }
131
-
132
134
  //#endregion
133
- //#region packages/db-sql-tools/src/common.ts
135
+ //#region src/common.ts
136
+ /** Formats a string value as a SQL literal with single-quote escaping. */
134
137
  function sqlStringLiteral(value) {
135
138
  return `'${value.replace(/'/g, "''")}'`;
136
139
  }
140
+ /** Converts a JS value to a SQL-bindable parameter. Objects/arrays -> JSON, booleans -> 0/1. */
137
141
  function toSqlValue(value) {
138
- if (value === undefined) return null;
142
+ if (value === void 0) return null;
139
143
  if (value === null) return null;
140
144
  if (typeof value === "object") return JSON.stringify(value);
141
145
  if (typeof value === "boolean") return value ? 1 : 0;
@@ -150,6 +154,7 @@ function refActionToSql(action) {
150
154
  default: return "NO ACTION";
151
155
  }
152
156
  }
157
+ /** Returns a safe SQL DEFAULT literal for a given design type. */
153
158
  function defaultValueForType(designType) {
154
159
  switch (designType) {
155
160
  case "number":
@@ -159,6 +164,11 @@ function defaultValueForType(designType) {
159
164
  default: return "''";
160
165
  }
161
166
  }
167
+ /**
168
+ * Converts a stored default value string to a SQL DEFAULT literal,
169
+ * respecting the field's designType. Booleans become 0/1, numbers stay unquoted,
170
+ * strings are single-quote-escaped.
171
+ */
162
172
  function defaultValueToSqlLiteral(designType, value) {
163
173
  switch (designType) {
164
174
  case "boolean": return value === "true" || value === "1" ? "1" : "0";
@@ -179,27 +189,23 @@ const queryOpToSql = {
179
189
  $lt: "<",
180
190
  $lte: "<="
181
191
  };
192
+ /**
193
+ * Renders an AtscriptQueryNode tree to raw SQL (no parameters -- for DDL use only).
194
+ */
182
195
  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
- }
196
+ if ("$and" in node) return node.$and.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" AND ");
197
+ if ("$or" in node) return `(${node.$or.map((n) => queryNodeToSql(n, resolveFieldRef)).join(" OR ")})`;
191
198
  if ("$not" in node) return `NOT (${queryNodeToSql(node.$not, resolveFieldRef)})`;
192
199
  const comp = node;
193
200
  const leftSql = resolveFieldRef(comp.left);
194
201
  const sqlOp = queryOpToSql[comp.op] || "=";
195
202
  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`;
203
+ if (comp.right === null || comp.right === void 0) return comp.op === "$ne" ? `${leftSql} IS NOT NULL` : `${leftSql} IS NULL`;
197
204
  if (typeof comp.right === "string") return `${leftSql} ${sqlOp} '${comp.right.replace(/'/g, "''")}'`;
198
205
  return `${leftSql} ${sqlOp} ${comp.right}`;
199
206
  }
200
-
201
207
  //#endregion
202
- //#region packages/db-sql-tools/src/agg.ts
208
+ //#region src/agg.ts
203
209
  const AGG_FN_SQL = {
204
210
  sum: "SUM",
205
211
  avg: "AVG",
@@ -210,17 +216,18 @@ const AGG_FN_SQL = {
210
216
  function buildAggExpr(dialect, expr) {
211
217
  const fn = AGG_FN_SQL[expr.$fn] ?? expr.$fn.toUpperCase();
212
218
  const alias = dialect.quoteIdentifier(resolveAlias(expr));
213
- const field = expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field);
214
- return `${fn}(${field}) AS ${alias}`;
219
+ return `${fn}(${expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field)}) AS ${alias}`;
215
220
  }
221
+ /**
222
+ * Builds a SELECT ... GROUP BY statement with aggregate functions.
223
+ */
216
224
  function buildAggregateSelect(dialect, table, where, controls) {
217
225
  const selectParts = [];
218
226
  const plainFields = controls.$select?.asArray;
219
227
  if (plainFields) for (const f of plainFields) selectParts.push(dialect.quoteIdentifier(f));
220
228
  const aggregates = controls.$select?.aggregates;
221
229
  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}`;
230
+ let sql = `SELECT ${selectParts.length > 0 ? selectParts.join(", ") : "*"} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
224
231
  const params = [...where.params];
225
232
  const groupBy = controls.$groupBy;
226
233
  if (groupBy?.length) {
@@ -239,12 +246,12 @@ function buildAggregateSelect(dialect, table, where, controls) {
239
246
  for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
240
247
  if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
241
248
  }
242
- if (controls.$limit !== undefined) {
249
+ if (controls.$limit !== void 0) {
243
250
  sql += ` LIMIT ?`;
244
251
  params.push(controls.$limit);
245
252
  }
246
- if (controls.$skip !== undefined) {
247
- if (controls.$limit === undefined) sql += ` LIMIT ${dialect.unlimitedLimit}`;
253
+ if (controls.$skip !== void 0) {
254
+ if (controls.$limit === void 0) sql += ` LIMIT ${dialect.unlimitedLimit}`;
248
255
  sql += ` OFFSET ?`;
249
256
  params.push(controls.$skip);
250
257
  }
@@ -253,25 +260,27 @@ function buildAggregateSelect(dialect, table, where, controls) {
253
260
  params
254
261
  });
255
262
  }
263
+ /**
264
+ * Builds a COUNT query for the number of distinct groups.
265
+ * Returns `{ count: N }` when executed.
266
+ */
256
267
  function buildAggregateCount(dialect, table, where, controls) {
257
268
  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
- }
269
+ if (!groupFields?.length) return finalizeParams(dialect, {
270
+ sql: `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`,
271
+ params: where.params
272
+ });
265
273
  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
274
  return finalizeParams(dialect, {
268
- sql,
275
+ sql: `SELECT COUNT(*) AS ${dialect.quoteIdentifier("count")} FROM (SELECT 1 FROM ${dialect.quoteTable(table)} WHERE ${where.sql} GROUP BY ${groupCols}) AS ${dialect.quoteIdentifier("_groups")}`,
269
276
  params: where.params
270
277
  });
271
278
  }
272
-
273
279
  //#endregion
274
- //#region packages/db-sql-tools/src/sql-builder.ts
280
+ //#region src/sql-builder.ts
281
+ /**
282
+ * Builds an INSERT statement.
283
+ */
275
284
  function buildInsert(dialect, table, data) {
276
285
  const keys = Object.keys(data);
277
286
  const cols = keys.map((k) => dialect.quoteIdentifier(k)).join(", ");
@@ -281,21 +290,23 @@ function buildInsert(dialect, table, data) {
281
290
  params: keys.map((k) => dialect.toValue(data[k]))
282
291
  });
283
292
  }
293
+ /**
294
+ * Builds a SELECT statement with optional sort, limit, offset, projection.
295
+ */
284
296
  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}`;
297
+ let sql = `SELECT ${buildProjection(dialect, controls?.$select)} FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
287
298
  const params = [...where.params];
288
299
  if (controls?.$sort) {
289
300
  const orderParts = [];
290
301
  for (const [col, dir] of Object.entries(controls.$sort)) orderParts.push(`${dialect.quoteIdentifier(col)} ${dir === -1 ? "DESC" : "ASC"}`);
291
302
  if (orderParts.length > 0) sql += ` ORDER BY ${orderParts.join(", ")}`;
292
303
  }
293
- if (controls?.$limit !== undefined) {
304
+ if (controls?.$limit !== void 0) {
294
305
  sql += ` LIMIT ?`;
295
306
  params.push(controls.$limit);
296
307
  }
297
- if (controls?.$skip !== undefined) {
298
- if (controls.$limit === undefined) sql += ` LIMIT ${dialect.unlimitedLimit}`;
308
+ if (controls?.$skip !== void 0) {
309
+ if (controls.$limit === void 0) sql += ` LIMIT ${dialect.unlimitedLimit}`;
299
310
  sql += ` OFFSET ?`;
300
311
  params.push(controls.$skip);
301
312
  }
@@ -304,28 +315,47 @@ function buildSelect(dialect, table, where, controls) {
304
315
  params
305
316
  });
306
317
  }
307
- function buildUpdate(dialect, table, data, where, limit) {
318
+ /**
319
+ * Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
320
+ */
321
+ function buildUpdate(dialect, table, data, where, limit, ops) {
308
322
  const setClauses = [];
309
323
  const params = [];
310
324
  for (const [key, value] of Object.entries(data)) {
311
325
  setClauses.push(`${dialect.quoteIdentifier(key)} = ?`);
312
326
  params.push(dialect.toValue(value));
313
327
  }
328
+ if (ops?.inc) for (const key in ops.inc) {
329
+ const col = dialect.quoteIdentifier(key);
330
+ setClauses.push(`${col} = ${col} + ?`);
331
+ params.push(ops.inc[key]);
332
+ }
333
+ if (ops?.mul) for (const key in ops.mul) {
334
+ const col = dialect.quoteIdentifier(key);
335
+ setClauses.push(`${col} = ${col} * ?`);
336
+ params.push(ops.mul[key]);
337
+ }
314
338
  let sql = `UPDATE ${dialect.quoteTable(table)} SET ${setClauses.join(", ")} WHERE ${where.sql}`;
315
- if (limit !== undefined) sql += ` LIMIT ${limit}`;
339
+ if (limit !== void 0) sql += ` LIMIT ${limit}`;
316
340
  return finalizeParams(dialect, {
317
341
  sql,
318
342
  params: [...params, ...where.params]
319
343
  });
320
344
  }
345
+ /**
346
+ * Builds a DELETE ... WHERE statement with optional LIMIT.
347
+ */
321
348
  function buildDelete(dialect, table, where, limit) {
322
349
  let sql = `DELETE FROM ${dialect.quoteTable(table)} WHERE ${where.sql}`;
323
- if (limit !== undefined) sql += ` LIMIT ${limit}`;
350
+ if (limit !== void 0) sql += ` LIMIT ${limit}`;
324
351
  return finalizeParams(dialect, {
325
352
  sql,
326
353
  params: where.params
327
354
  });
328
355
  }
356
+ /**
357
+ * Builds a column projection (SELECT clause fields).
358
+ */
329
359
  function buildProjection(dialect, select) {
330
360
  const fields = select?.asArray;
331
361
  if (!fields) return "*";
@@ -336,11 +366,13 @@ function buildProjection(dialect, select) {
336
366
  }
337
367
  return sql || "*";
338
368
  }
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})`;
369
+ /** Builds the SQL expression for a single aggregate column. */
370
+ function buildAggColExpr(dialect, c) {
371
+ return `${AGG_FN_SQL[c.aggFn] ?? c.aggFn.toUpperCase()}(${c.aggField === "*" ? "*" : `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`})`;
343
372
  }
373
+ /**
374
+ * Builds a CREATE VIEW statement from a view plan and column mappings.
375
+ */
344
376
  function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
345
377
  const selectCols = columns.map((c) => {
346
378
  if (c.aggFn) return `${buildAggColExpr(dialect, c)} AS ${dialect.quoteIdentifier(c.viewColumn)}`;
@@ -355,15 +387,14 @@ function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
355
387
  const whereClause = queryNodeToSql(plan.filter, resolveFieldRef);
356
388
  sql += ` WHERE ${whereClause}`;
357
389
  }
358
- const hasAggregates = columns.some((c) => c.aggFn);
359
- if (hasAggregates) {
390
+ if (columns.some((c) => c.aggFn)) {
360
391
  const dimensionCols = columns.filter((c) => !c.aggFn);
361
392
  if (dimensionCols.length > 0) {
362
393
  const groupByCols = dimensionCols.map((c) => `${dialect.quoteIdentifier(c.sourceTable)}.${dialect.quoteIdentifier(c.sourceColumn)}`).join(", ");
363
394
  sql += ` GROUP BY ${groupByCols}`;
364
395
  }
365
396
  if (plan.having) {
366
- const columnMap = new Map();
397
+ const columnMap = /* @__PURE__ */ new Map();
367
398
  for (const c of columns) columnMap.set(c.viewColumn, c);
368
399
  const havingResolver = (ref) => {
369
400
  if (!ref.type) {
@@ -379,6 +410,5 @@ function buildCreateView(dialect, viewName, plan, columns, resolveFieldRef) {
379
410
  }
380
411
  return sql;
381
412
  }
382
-
383
413
  //#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 };
414
+ 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 CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "@atscript/db-sql-tools",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "description": "Shared SQL builder utilities for @atscript database adapters.",
5
5
  "keywords": [
6
6
  "atscript",
7
7
  "database",
8
8
  "sql"
9
9
  ],
10
- "homepage": "https://github.com/moostjs/atscript/tree/main/packages/db-sql-tools#readme",
10
+ "homepage": "https://github.com/moostjs/atscript-db/tree/main/packages/db-sql-tools#readme",
11
11
  "bugs": {
12
- "url": "https://github.com/moostjs/atscript/issues"
12
+ "url": "https://github.com/moostjs/atscript-db/issues"
13
13
  },
14
14
  "license": "MIT",
15
15
  "author": "Artem Maltsev",
16
16
  "repository": {
17
17
  "type": "git",
18
- "url": "git+https://github.com/moostjs/atscript.git",
18
+ "url": "git+https://github.com/moostjs/atscript-db.git",
19
19
  "directory": "packages/db-sql-tools"
20
20
  },
21
21
  "files": [
@@ -23,24 +23,31 @@
23
23
  ],
24
24
  "type": "module",
25
25
  "main": "dist/index.mjs",
26
- "types": "dist/index.d.ts",
26
+ "module": "./dist/index.mjs",
27
+ "types": "dist/index.d.mts",
27
28
  "exports": {
28
29
  ".": {
29
- "types": "./dist/index.d.ts",
30
+ "types": "./dist/index.d.mts",
30
31
  "import": "./dist/index.mjs",
31
32
  "require": "./dist/index.cjs"
32
33
  },
33
34
  "./package.json": "./package.json"
34
35
  },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
35
39
  "devDependencies": {
36
- "vitest": "3.2.4"
40
+ "@uniqu/core": "^0.1.2",
41
+ "unplugin-atscript": "^0.1.39"
37
42
  },
38
43
  "peerDependencies": {
39
44
  "@uniqu/core": "^0.1.2",
40
- "@atscript/db": "^0.1.39"
45
+ "@atscript/db": "^0.1.40"
41
46
  },
42
47
  "scripts": {
43
- "pub": "pnpm publish --access public",
44
- "test": "vitest"
48
+ "build": "vp pack",
49
+ "dev": "vp pack --watch",
50
+ "test": "vp test",
51
+ "check": "vp check"
45
52
  }
46
53
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
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.