@atscript/db-sql-tools 0.1.127 → 0.1.128
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +77 -7
- package/dist/index.d.cts +50 -3
- package/dist/index.d.mts +50 -3
- package/dist/index.mjs +75 -8
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -28,17 +28,18 @@ const EMPTY_OR = {
|
|
|
28
28
|
/**
|
|
29
29
|
* Creates a dialect-specific filter visitor for `walkFilter`.
|
|
30
30
|
*/
|
|
31
|
-
function createFilterVisitor(dialect) {
|
|
31
|
+
function createFilterVisitor(dialect, options) {
|
|
32
|
+
const columnRef = options?.columnRef ?? ((field) => dialect.quoteIdentifier(field));
|
|
32
33
|
return {
|
|
33
34
|
comparison(field, op, value) {
|
|
34
35
|
if (op === "$geoWithin") {
|
|
35
|
-
if (dialect.geoWithin) return dialect.geoWithin(
|
|
36
|
+
if (dialect.geoWithin) return dialect.geoWithin(columnRef(field), value);
|
|
36
37
|
throw new _atscript_db.DbError("GEO_NOT_SUPPORTED", [{
|
|
37
38
|
path: field,
|
|
38
39
|
message: "$geoWithin is not supported by this adapter"
|
|
39
40
|
}]);
|
|
40
41
|
}
|
|
41
|
-
const col =
|
|
42
|
+
const col = columnRef(field);
|
|
42
43
|
const v = dialect.toParam(value);
|
|
43
44
|
switch (op) {
|
|
44
45
|
case "$eq":
|
|
@@ -330,10 +331,27 @@ const AGG_FN_SQL = {
|
|
|
330
331
|
min: "MIN",
|
|
331
332
|
max: "MAX"
|
|
332
333
|
};
|
|
334
|
+
/** The bare aggregate call, e.g. `SUM("amount")` / `COUNT(*)`. */
|
|
335
|
+
function aggFnSql(dialect, expr) {
|
|
336
|
+
return `${AGG_FN_SQL[expr.$fn] ?? expr.$fn.toUpperCase()}(${expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field)})`;
|
|
337
|
+
}
|
|
333
338
|
function buildAggExpr(dialect, expr) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
339
|
+
return `${aggFnSql(dialect, expr)} AS ${dialect.quoteIdentifier((0, _atscript_db_agg.resolveAlias)(expr))}`;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Renders `$having`. A key that names an aggregate alias (`$as`, else
|
|
343
|
+
* `fn_field`) renders the aggregate expression itself — `SUM("amount") > ?`
|
|
344
|
+
* — because PostgreSQL does not allow a SELECT alias in HAVING (MySQL and
|
|
345
|
+
* SQLite tolerate it, so the expression form keeps all three identical).
|
|
346
|
+
* Other keys (grouped columns) render as plain columns.
|
|
347
|
+
*/
|
|
348
|
+
function buildHaving(dialect, controls) {
|
|
349
|
+
const having = controls.$having;
|
|
350
|
+
const aggregates = controls.$select?.aggregates;
|
|
351
|
+
if (!aggregates?.length) return buildWhere(dialect, having);
|
|
352
|
+
const exprByAlias = /* @__PURE__ */ new Map();
|
|
353
|
+
for (const expr of aggregates) exprByAlias.set((0, _atscript_db_agg.resolveAlias)(expr), aggFnSql(dialect, expr));
|
|
354
|
+
return (0, _uniqu_core.walkFilter)(having, createFilterVisitor(dialect, { columnRef: (field) => exprByAlias.get(field) ?? dialect.quoteIdentifier(field) })) ?? EMPTY_AND;
|
|
337
355
|
}
|
|
338
356
|
/**
|
|
339
357
|
* Builds a SELECT ... GROUP BY statement with aggregate functions.
|
|
@@ -352,7 +370,7 @@ function buildAggregateSelect(dialect, table, where, controls) {
|
|
|
352
370
|
sql += ` GROUP BY ${groupCols}`;
|
|
353
371
|
}
|
|
354
372
|
if (controls.$having) {
|
|
355
|
-
const havingFragment =
|
|
373
|
+
const havingFragment = buildHaving(dialect, controls);
|
|
356
374
|
if (havingFragment.sql !== EMPTY_AND.sql) {
|
|
357
375
|
sql += ` HAVING ${havingFragment.sql}`;
|
|
358
376
|
params.push(...havingFragment.params);
|
|
@@ -433,8 +451,53 @@ function buildSelect(dialect, table, where, controls) {
|
|
|
433
451
|
});
|
|
434
452
|
}
|
|
435
453
|
/**
|
|
454
|
+
* Marker value for {@link buildUpdate}: the column is assigned its DDL
|
|
455
|
+
* `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
|
|
456
|
+
* {@link fillReplacePayload} for columns whose function default the engine
|
|
457
|
+
* owns; never appears in a patch.
|
|
458
|
+
*/
|
|
459
|
+
const SQL_DEFAULT = Symbol("SQL_DEFAULT");
|
|
460
|
+
/**
|
|
461
|
+
* The columns a full replace assigns on a SQL adapter: every non-ignored
|
|
462
|
+
* descriptor (the same set `CREATE TABLE` emits) except the primary key —
|
|
463
|
+
* the row is matched by the filter, and an omitted PK must never be nulled
|
|
464
|
+
* or re-defaulted. Static value defaults are filled SDK-side before the
|
|
465
|
+
* adapter sees the row, so only native function defaults are flagged.
|
|
466
|
+
*/
|
|
467
|
+
function replaceColumnsFor(fields, nativeFns) {
|
|
468
|
+
const out = [];
|
|
469
|
+
for (const fd of fields) {
|
|
470
|
+
if (fd.ignored || fd.isPrimaryKey) continue;
|
|
471
|
+
const def = fd.defaultValue;
|
|
472
|
+
out.push({
|
|
473
|
+
name: fd.physicalName,
|
|
474
|
+
useDefault: def?.kind === "fn" && nativeFns.has(def.fn)
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return out;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Turns a (physical-name) replace payload into a FULL row assignment: every
|
|
481
|
+
* column in `columns` the payload omits becomes `null` — or {@link SQL_DEFAULT}
|
|
482
|
+
* when the engine owns its function default — so an UPDATE-based replace never
|
|
483
|
+
* retains a value the caller left out. This is the SQL counterpart of the
|
|
484
|
+
* whole-document replace the memory and MongoDB adapters do natively. The
|
|
485
|
+
* version column is excluded (`buildUpdate` appends the OCC bump itself).
|
|
486
|
+
* Returns a new object; `data` is not mutated.
|
|
487
|
+
*/
|
|
488
|
+
function fillReplacePayload(data, columns, versionColumn) {
|
|
489
|
+
const full = { ...data };
|
|
490
|
+
for (const col of columns) {
|
|
491
|
+
if (col.name === versionColumn || col.name in full) continue;
|
|
492
|
+
full[col.name] = col.useDefault ? SQL_DEFAULT : null;
|
|
493
|
+
}
|
|
494
|
+
return full;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
436
497
|
* Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
|
|
437
498
|
*
|
|
499
|
+
* A value of {@link SQL_DEFAULT} renders as `<col> = DEFAULT` (full replace).
|
|
500
|
+
*
|
|
438
501
|
* Optimistic concurrency control (OCC) hooks:
|
|
439
502
|
* - `versionColumn` — when supplied, the builder appends
|
|
440
503
|
* `<col> = <col> + 1` to the SET list. The bump is **mandatory** whenever
|
|
@@ -450,6 +513,10 @@ function buildUpdate(dialect, table, data, where, limit, ops, versionColumn, exp
|
|
|
450
513
|
const setClauses = [];
|
|
451
514
|
const params = [];
|
|
452
515
|
for (const [key, value] of Object.entries(data)) {
|
|
516
|
+
if (value === SQL_DEFAULT) {
|
|
517
|
+
setClauses.push(`${dialect.quoteIdentifier(key)} = DEFAULT`);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
453
520
|
setClauses.push(`${dialect.quoteIdentifier(key)} = ?`);
|
|
454
521
|
params.push(dialect.toValue(value));
|
|
455
522
|
}
|
|
@@ -583,6 +650,7 @@ exports.AGG_FN_SQL = AGG_FN_SQL;
|
|
|
583
650
|
exports.EMPTY_AND = EMPTY_AND;
|
|
584
651
|
exports.EMPTY_OR = EMPTY_OR;
|
|
585
652
|
exports.GEO_DISTANCE_ALIAS = GEO_DISTANCE_ALIAS;
|
|
653
|
+
exports.SQL_DEFAULT = SQL_DEFAULT;
|
|
586
654
|
exports.buildAggregateCount = buildAggregateCount;
|
|
587
655
|
exports.buildAggregateSelect = buildAggregateSelect;
|
|
588
656
|
exports.buildCreateView = buildCreateView;
|
|
@@ -597,6 +665,7 @@ exports.buildWhere = buildWhere;
|
|
|
597
665
|
exports.createFilterVisitor = createFilterVisitor;
|
|
598
666
|
exports.defaultValueForType = defaultValueForType;
|
|
599
667
|
exports.defaultValueToSqlLiteral = defaultValueToSqlLiteral;
|
|
668
|
+
exports.fillReplacePayload = fillReplacePayload;
|
|
600
669
|
exports.finalizeParams = finalizeParams;
|
|
601
670
|
exports.geoWindowFromControls = geoWindowFromControls;
|
|
602
671
|
exports.normalizeGeoPointValue = normalizeGeoPointValue;
|
|
@@ -605,5 +674,6 @@ exports.queryNodeToSql = queryNodeToSql;
|
|
|
605
674
|
exports.queryOpToSql = queryOpToSql;
|
|
606
675
|
exports.refActionToSql = refActionToSql;
|
|
607
676
|
exports.renameGeoDistance = renameGeoDistance;
|
|
677
|
+
exports.replaceColumnsFor = replaceColumnsFor;
|
|
608
678
|
exports.sqlStringLiteral = sqlStringLiteral;
|
|
609
679
|
exports.toSqlValue = toSqlValue;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FilterExpr, FilterVisitor } from "@uniqu/core";
|
|
2
|
-
import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbReferentialAction, TFieldOps, TViewColumnMapping, TViewPlan, UniquSelect } from "@atscript/db";
|
|
2
|
+
import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbReferentialAction, TFieldOps, TViewColumnMapping, TViewPlan, UniquSelect } from "@atscript/db";
|
|
3
3
|
|
|
4
4
|
//#region src/dialect.d.ts
|
|
5
5
|
interface TSqlFragment {
|
|
@@ -45,10 +45,19 @@ declare const EMPTY_AND: TSqlFragment;
|
|
|
45
45
|
declare const EMPTY_OR: TSqlFragment;
|
|
46
46
|
//#endregion
|
|
47
47
|
//#region src/filter-builder.d.ts
|
|
48
|
+
interface TFilterVisitorOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Renders a filter key as its SQL operand. Defaults to
|
|
51
|
+
* `dialect.quoteIdentifier(field)`; the aggregate builder overrides it so a
|
|
52
|
+
* `$having` key that names an aggregate alias renders the aggregate
|
|
53
|
+
* expression (`SUM("amount")`) — PostgreSQL rejects SELECT aliases in HAVING.
|
|
54
|
+
*/
|
|
55
|
+
columnRef?: (field: string) => string;
|
|
56
|
+
}
|
|
48
57
|
/**
|
|
49
58
|
* Creates a dialect-specific filter visitor for `walkFilter`.
|
|
50
59
|
*/
|
|
51
|
-
declare function createFilterVisitor(dialect: SqlDialect): FilterVisitor<TSqlFragment>;
|
|
60
|
+
declare function createFilterVisitor(dialect: SqlDialect, options?: TFilterVisitorOptions): FilterVisitor<TSqlFragment>;
|
|
52
61
|
/**
|
|
53
62
|
* Translates a filter expression into a parameterized SQL WHERE clause.
|
|
54
63
|
*/
|
|
@@ -113,9 +122,47 @@ declare function buildInsert(dialect: SqlDialect, table: string, data: Record<st
|
|
|
113
122
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
114
123
|
*/
|
|
115
124
|
declare function buildSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls?: DbControls): TSqlFragment;
|
|
125
|
+
/**
|
|
126
|
+
* Marker value for {@link buildUpdate}: the column is assigned its DDL
|
|
127
|
+
* `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
|
|
128
|
+
* {@link fillReplacePayload} for columns whose function default the engine
|
|
129
|
+
* owns; never appears in a patch.
|
|
130
|
+
*/
|
|
131
|
+
declare const SQL_DEFAULT: unique symbol;
|
|
132
|
+
/** One physical column a full replace must assign. */
|
|
133
|
+
interface TReplaceColumn {
|
|
134
|
+
/** Physical column name. */
|
|
135
|
+
name: string;
|
|
136
|
+
/**
|
|
137
|
+
* `true` when the engine owns this column's function default (`now`,
|
|
138
|
+
* `uuid`, `increment` listed in the adapter's `nativeDefaultFns()`): an
|
|
139
|
+
* omitted value re-applies the DDL `DEFAULT` instead of storing NULL.
|
|
140
|
+
*/
|
|
141
|
+
useDefault: boolean;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The columns a full replace assigns on a SQL adapter: every non-ignored
|
|
145
|
+
* descriptor (the same set `CREATE TABLE` emits) except the primary key —
|
|
146
|
+
* the row is matched by the filter, and an omitted PK must never be nulled
|
|
147
|
+
* or re-defaulted. Static value defaults are filled SDK-side before the
|
|
148
|
+
* adapter sees the row, so only native function defaults are flagged.
|
|
149
|
+
*/
|
|
150
|
+
declare function replaceColumnsFor(fields: readonly TDbFieldMeta[], nativeFns: ReadonlySet<TDbDefaultFn>): TReplaceColumn[];
|
|
151
|
+
/**
|
|
152
|
+
* Turns a (physical-name) replace payload into a FULL row assignment: every
|
|
153
|
+
* column in `columns` the payload omits becomes `null` — or {@link SQL_DEFAULT}
|
|
154
|
+
* when the engine owns its function default — so an UPDATE-based replace never
|
|
155
|
+
* retains a value the caller left out. This is the SQL counterpart of the
|
|
156
|
+
* whole-document replace the memory and MongoDB adapters do natively. The
|
|
157
|
+
* version column is excluded (`buildUpdate` appends the OCC bump itself).
|
|
158
|
+
* Returns a new object; `data` is not mutated.
|
|
159
|
+
*/
|
|
160
|
+
declare function fillReplacePayload(data: Record<string, unknown>, columns: readonly TReplaceColumn[], versionColumn?: string): Record<string, unknown>;
|
|
116
161
|
/**
|
|
117
162
|
* Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
|
|
118
163
|
*
|
|
164
|
+
* A value of {@link SQL_DEFAULT} renders as `<col> = DEFAULT` (full replace).
|
|
165
|
+
*
|
|
119
166
|
* Optimistic concurrency control (OCC) hooks:
|
|
120
167
|
* - `versionColumn` — when supplied, the builder appends
|
|
121
168
|
* `<col> = <col> + 1` to the SET list. The bump is **mandatory** whenever
|
|
@@ -186,4 +233,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
186
233
|
flags: string;
|
|
187
234
|
};
|
|
188
235
|
//#endregion
|
|
189
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, type SqlDialect, type TGeoCircle, type TGeoWindow, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, sqlStringLiteral, toSqlValue };
|
|
236
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoWindow, type TReplaceColumn, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, toSqlValue };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FilterExpr, FilterVisitor } from "@uniqu/core";
|
|
2
|
-
import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbReferentialAction, TFieldOps, TViewColumnMapping, TViewPlan, UniquSelect } from "@atscript/db";
|
|
2
|
+
import { AtscriptQueryFieldRef, AtscriptQueryNode, DbControls, TDbDefaultFn, TDbFieldMeta, TDbReferentialAction, TFieldOps, TViewColumnMapping, TViewPlan, UniquSelect } from "@atscript/db";
|
|
3
3
|
|
|
4
4
|
//#region src/dialect.d.ts
|
|
5
5
|
interface TSqlFragment {
|
|
@@ -45,10 +45,19 @@ declare const EMPTY_AND: TSqlFragment;
|
|
|
45
45
|
declare const EMPTY_OR: TSqlFragment;
|
|
46
46
|
//#endregion
|
|
47
47
|
//#region src/filter-builder.d.ts
|
|
48
|
+
interface TFilterVisitorOptions {
|
|
49
|
+
/**
|
|
50
|
+
* Renders a filter key as its SQL operand. Defaults to
|
|
51
|
+
* `dialect.quoteIdentifier(field)`; the aggregate builder overrides it so a
|
|
52
|
+
* `$having` key that names an aggregate alias renders the aggregate
|
|
53
|
+
* expression (`SUM("amount")`) — PostgreSQL rejects SELECT aliases in HAVING.
|
|
54
|
+
*/
|
|
55
|
+
columnRef?: (field: string) => string;
|
|
56
|
+
}
|
|
48
57
|
/**
|
|
49
58
|
* Creates a dialect-specific filter visitor for `walkFilter`.
|
|
50
59
|
*/
|
|
51
|
-
declare function createFilterVisitor(dialect: SqlDialect): FilterVisitor<TSqlFragment>;
|
|
60
|
+
declare function createFilterVisitor(dialect: SqlDialect, options?: TFilterVisitorOptions): FilterVisitor<TSqlFragment>;
|
|
52
61
|
/**
|
|
53
62
|
* Translates a filter expression into a parameterized SQL WHERE clause.
|
|
54
63
|
*/
|
|
@@ -113,9 +122,47 @@ declare function buildInsert(dialect: SqlDialect, table: string, data: Record<st
|
|
|
113
122
|
* Builds a SELECT statement with optional sort, limit, offset, projection.
|
|
114
123
|
*/
|
|
115
124
|
declare function buildSelect(dialect: SqlDialect, table: string, where: TSqlFragment, controls?: DbControls): TSqlFragment;
|
|
125
|
+
/**
|
|
126
|
+
* Marker value for {@link buildUpdate}: the column is assigned its DDL
|
|
127
|
+
* `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
|
|
128
|
+
* {@link fillReplacePayload} for columns whose function default the engine
|
|
129
|
+
* owns; never appears in a patch.
|
|
130
|
+
*/
|
|
131
|
+
declare const SQL_DEFAULT: unique symbol;
|
|
132
|
+
/** One physical column a full replace must assign. */
|
|
133
|
+
interface TReplaceColumn {
|
|
134
|
+
/** Physical column name. */
|
|
135
|
+
name: string;
|
|
136
|
+
/**
|
|
137
|
+
* `true` when the engine owns this column's function default (`now`,
|
|
138
|
+
* `uuid`, `increment` listed in the adapter's `nativeDefaultFns()`): an
|
|
139
|
+
* omitted value re-applies the DDL `DEFAULT` instead of storing NULL.
|
|
140
|
+
*/
|
|
141
|
+
useDefault: boolean;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The columns a full replace assigns on a SQL adapter: every non-ignored
|
|
145
|
+
* descriptor (the same set `CREATE TABLE` emits) except the primary key —
|
|
146
|
+
* the row is matched by the filter, and an omitted PK must never be nulled
|
|
147
|
+
* or re-defaulted. Static value defaults are filled SDK-side before the
|
|
148
|
+
* adapter sees the row, so only native function defaults are flagged.
|
|
149
|
+
*/
|
|
150
|
+
declare function replaceColumnsFor(fields: readonly TDbFieldMeta[], nativeFns: ReadonlySet<TDbDefaultFn>): TReplaceColumn[];
|
|
151
|
+
/**
|
|
152
|
+
* Turns a (physical-name) replace payload into a FULL row assignment: every
|
|
153
|
+
* column in `columns` the payload omits becomes `null` — or {@link SQL_DEFAULT}
|
|
154
|
+
* when the engine owns its function default — so an UPDATE-based replace never
|
|
155
|
+
* retains a value the caller left out. This is the SQL counterpart of the
|
|
156
|
+
* whole-document replace the memory and MongoDB adapters do natively. The
|
|
157
|
+
* version column is excluded (`buildUpdate` appends the OCC bump itself).
|
|
158
|
+
* Returns a new object; `data` is not mutated.
|
|
159
|
+
*/
|
|
160
|
+
declare function fillReplacePayload(data: Record<string, unknown>, columns: readonly TReplaceColumn[], versionColumn?: string): Record<string, unknown>;
|
|
116
161
|
/**
|
|
117
162
|
* Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
|
|
118
163
|
*
|
|
164
|
+
* A value of {@link SQL_DEFAULT} renders as `<col> = DEFAULT` (full replace).
|
|
165
|
+
*
|
|
119
166
|
* Optimistic concurrency control (OCC) hooks:
|
|
120
167
|
* - `versionColumn` — when supplied, the builder appends
|
|
121
168
|
* `<col> = <col> + 1` to the SET list. The bump is **mandatory** whenever
|
|
@@ -186,4 +233,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
186
233
|
flags: string;
|
|
187
234
|
};
|
|
188
235
|
//#endregion
|
|
189
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, type SqlDialect, type TGeoCircle, type TGeoWindow, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, sqlStringLiteral, toSqlValue };
|
|
236
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TFilterVisitorOptions, type TGeoCircle, type TGeoWindow, type TReplaceColumn, type TSqlFragment, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, toSqlValue };
|
package/dist/index.mjs
CHANGED
|
@@ -27,17 +27,18 @@ const EMPTY_OR = {
|
|
|
27
27
|
/**
|
|
28
28
|
* Creates a dialect-specific filter visitor for `walkFilter`.
|
|
29
29
|
*/
|
|
30
|
-
function createFilterVisitor(dialect) {
|
|
30
|
+
function createFilterVisitor(dialect, options) {
|
|
31
|
+
const columnRef = options?.columnRef ?? ((field) => dialect.quoteIdentifier(field));
|
|
31
32
|
return {
|
|
32
33
|
comparison(field, op, value) {
|
|
33
34
|
if (op === "$geoWithin") {
|
|
34
|
-
if (dialect.geoWithin) return dialect.geoWithin(
|
|
35
|
+
if (dialect.geoWithin) return dialect.geoWithin(columnRef(field), value);
|
|
35
36
|
throw new DbError("GEO_NOT_SUPPORTED", [{
|
|
36
37
|
path: field,
|
|
37
38
|
message: "$geoWithin is not supported by this adapter"
|
|
38
39
|
}]);
|
|
39
40
|
}
|
|
40
|
-
const col =
|
|
41
|
+
const col = columnRef(field);
|
|
41
42
|
const v = dialect.toParam(value);
|
|
42
43
|
switch (op) {
|
|
43
44
|
case "$eq":
|
|
@@ -329,10 +330,27 @@ const AGG_FN_SQL = {
|
|
|
329
330
|
min: "MIN",
|
|
330
331
|
max: "MAX"
|
|
331
332
|
};
|
|
333
|
+
/** The bare aggregate call, e.g. `SUM("amount")` / `COUNT(*)`. */
|
|
334
|
+
function aggFnSql(dialect, expr) {
|
|
335
|
+
return `${AGG_FN_SQL[expr.$fn] ?? expr.$fn.toUpperCase()}(${expr.$field === "*" ? "*" : dialect.quoteIdentifier(expr.$field)})`;
|
|
336
|
+
}
|
|
332
337
|
function buildAggExpr(dialect, expr) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
338
|
+
return `${aggFnSql(dialect, expr)} AS ${dialect.quoteIdentifier(resolveAlias(expr))}`;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Renders `$having`. A key that names an aggregate alias (`$as`, else
|
|
342
|
+
* `fn_field`) renders the aggregate expression itself — `SUM("amount") > ?`
|
|
343
|
+
* — because PostgreSQL does not allow a SELECT alias in HAVING (MySQL and
|
|
344
|
+
* SQLite tolerate it, so the expression form keeps all three identical).
|
|
345
|
+
* Other keys (grouped columns) render as plain columns.
|
|
346
|
+
*/
|
|
347
|
+
function buildHaving(dialect, controls) {
|
|
348
|
+
const having = controls.$having;
|
|
349
|
+
const aggregates = controls.$select?.aggregates;
|
|
350
|
+
if (!aggregates?.length) return buildWhere(dialect, having);
|
|
351
|
+
const exprByAlias = /* @__PURE__ */ new Map();
|
|
352
|
+
for (const expr of aggregates) exprByAlias.set(resolveAlias(expr), aggFnSql(dialect, expr));
|
|
353
|
+
return walkFilter(having, createFilterVisitor(dialect, { columnRef: (field) => exprByAlias.get(field) ?? dialect.quoteIdentifier(field) })) ?? EMPTY_AND;
|
|
336
354
|
}
|
|
337
355
|
/**
|
|
338
356
|
* Builds a SELECT ... GROUP BY statement with aggregate functions.
|
|
@@ -351,7 +369,7 @@ function buildAggregateSelect(dialect, table, where, controls) {
|
|
|
351
369
|
sql += ` GROUP BY ${groupCols}`;
|
|
352
370
|
}
|
|
353
371
|
if (controls.$having) {
|
|
354
|
-
const havingFragment =
|
|
372
|
+
const havingFragment = buildHaving(dialect, controls);
|
|
355
373
|
if (havingFragment.sql !== EMPTY_AND.sql) {
|
|
356
374
|
sql += ` HAVING ${havingFragment.sql}`;
|
|
357
375
|
params.push(...havingFragment.params);
|
|
@@ -432,8 +450,53 @@ function buildSelect(dialect, table, where, controls) {
|
|
|
432
450
|
});
|
|
433
451
|
}
|
|
434
452
|
/**
|
|
453
|
+
* Marker value for {@link buildUpdate}: the column is assigned its DDL
|
|
454
|
+
* `DEFAULT` (`SET "col" = DEFAULT`, no bound parameter). Produced by
|
|
455
|
+
* {@link fillReplacePayload} for columns whose function default the engine
|
|
456
|
+
* owns; never appears in a patch.
|
|
457
|
+
*/
|
|
458
|
+
const SQL_DEFAULT = Symbol("SQL_DEFAULT");
|
|
459
|
+
/**
|
|
460
|
+
* The columns a full replace assigns on a SQL adapter: every non-ignored
|
|
461
|
+
* descriptor (the same set `CREATE TABLE` emits) except the primary key —
|
|
462
|
+
* the row is matched by the filter, and an omitted PK must never be nulled
|
|
463
|
+
* or re-defaulted. Static value defaults are filled SDK-side before the
|
|
464
|
+
* adapter sees the row, so only native function defaults are flagged.
|
|
465
|
+
*/
|
|
466
|
+
function replaceColumnsFor(fields, nativeFns) {
|
|
467
|
+
const out = [];
|
|
468
|
+
for (const fd of fields) {
|
|
469
|
+
if (fd.ignored || fd.isPrimaryKey) continue;
|
|
470
|
+
const def = fd.defaultValue;
|
|
471
|
+
out.push({
|
|
472
|
+
name: fd.physicalName,
|
|
473
|
+
useDefault: def?.kind === "fn" && nativeFns.has(def.fn)
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return out;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Turns a (physical-name) replace payload into a FULL row assignment: every
|
|
480
|
+
* column in `columns` the payload omits becomes `null` — or {@link SQL_DEFAULT}
|
|
481
|
+
* when the engine owns its function default — so an UPDATE-based replace never
|
|
482
|
+
* retains a value the caller left out. This is the SQL counterpart of the
|
|
483
|
+
* whole-document replace the memory and MongoDB adapters do natively. The
|
|
484
|
+
* version column is excluded (`buildUpdate` appends the OCC bump itself).
|
|
485
|
+
* Returns a new object; `data` is not mutated.
|
|
486
|
+
*/
|
|
487
|
+
function fillReplacePayload(data, columns, versionColumn) {
|
|
488
|
+
const full = { ...data };
|
|
489
|
+
for (const col of columns) {
|
|
490
|
+
if (col.name === versionColumn || col.name in full) continue;
|
|
491
|
+
full[col.name] = col.useDefault ? SQL_DEFAULT : null;
|
|
492
|
+
}
|
|
493
|
+
return full;
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
435
496
|
* Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
|
|
436
497
|
*
|
|
498
|
+
* A value of {@link SQL_DEFAULT} renders as `<col> = DEFAULT` (full replace).
|
|
499
|
+
*
|
|
437
500
|
* Optimistic concurrency control (OCC) hooks:
|
|
438
501
|
* - `versionColumn` — when supplied, the builder appends
|
|
439
502
|
* `<col> = <col> + 1` to the SET list. The bump is **mandatory** whenever
|
|
@@ -449,6 +512,10 @@ function buildUpdate(dialect, table, data, where, limit, ops, versionColumn, exp
|
|
|
449
512
|
const setClauses = [];
|
|
450
513
|
const params = [];
|
|
451
514
|
for (const [key, value] of Object.entries(data)) {
|
|
515
|
+
if (value === SQL_DEFAULT) {
|
|
516
|
+
setClauses.push(`${dialect.quoteIdentifier(key)} = DEFAULT`);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
452
519
|
setClauses.push(`${dialect.quoteIdentifier(key)} = ?`);
|
|
453
520
|
params.push(dialect.toValue(value));
|
|
454
521
|
}
|
|
@@ -578,4 +645,4 @@ function parseRegexString(value) {
|
|
|
578
645
|
};
|
|
579
646
|
}
|
|
580
647
|
//#endregion
|
|
581
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, sqlStringLiteral, toSqlValue };
|
|
648
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, SQL_DEFAULT, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildProjection, buildSelect, buildUpdate, buildWhere, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, fillReplacePayload, finalizeParams, geoWindowFromControls, normalizeGeoPointValue, parseRegexString, queryNodeToSql, queryOpToSql, refActionToSql, renameGeoDistance, replaceColumnsFor, sqlStringLiteral, toSqlValue };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-sql-tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.128",
|
|
4
4
|
"description": "Shared SQL builder utilities for @atscript database adapters.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@uniqu/core": "^0.1.
|
|
41
|
-
"unplugin-atscript": "^0.1.
|
|
40
|
+
"@uniqu/core": "^0.1.8",
|
|
41
|
+
"unplugin-atscript": "^0.1.92"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@uniqu/core": "^0.1.
|
|
45
|
-
"@atscript/db": "^0.1.
|
|
44
|
+
"@uniqu/core": "^0.1.8",
|
|
45
|
+
"@atscript/db": "^0.1.128"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "vp pack",
|