@atscript/db-sql-tools 0.1.150 → 0.1.152
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 +174 -2
- package/dist/index.d.cts +28 -1
- package/dist/index.d.mts +28 -1
- package/dist/index.mjs +174 -3
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -6,15 +6,133 @@ let _atscript_db_agg = require("@atscript/db/agg");
|
|
|
6
6
|
/**
|
|
7
7
|
* Replaces positional `?` placeholders with dialect-specific numbered placeholders
|
|
8
8
|
* (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
|
|
9
|
+
*
|
|
10
|
+
* Only a `?` in plain SQL text is a placeholder: one inside a string literal
|
|
11
|
+
* (`'…'`, `E'…'`, `$tag$…$tag$`), a quoted identifier (`"…"`) or a comment
|
|
12
|
+
* (`-- …`, `/* … *\/`) is left alone, and so are PostgreSQL's jsonb operators
|
|
13
|
+
* `?|` / `?&` (a placeholder directly followed by `|` / `&` is only ever
|
|
14
|
+
* `?||` / `?&&`, the concatenation / overlap). `??` is an escaped literal
|
|
15
|
+
* `?` — how a builder writes the bare jsonb `?` operator and any other
|
|
16
|
+
* operator that contains `?` (`@?` → `@??`, `?#` → `??#`, `?-` → `??-`).
|
|
17
|
+
* Block comments nest, as in PostgreSQL.
|
|
9
18
|
*/
|
|
10
19
|
function finalizeParams(dialect, fragment) {
|
|
11
|
-
|
|
20
|
+
const placeholder = dialect.paramPlaceholder;
|
|
21
|
+
if (!placeholder) return fragment;
|
|
22
|
+
const s = fragment.sql;
|
|
23
|
+
if (s.indexOf("?") === -1) return fragment;
|
|
24
|
+
let out = "";
|
|
25
|
+
let last = 0;
|
|
12
26
|
let idx = 0;
|
|
27
|
+
const n = s.length;
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < n) {
|
|
30
|
+
const c = s.charCodeAt(i);
|
|
31
|
+
if (c === QMARK) {
|
|
32
|
+
const next = s.charCodeAt(i + 1);
|
|
33
|
+
if (next === QMARK) {
|
|
34
|
+
out += s.slice(last, i + 1);
|
|
35
|
+
last = i + 2;
|
|
36
|
+
i += 2;
|
|
37
|
+
} else if (next === PIPE && s.charCodeAt(i + 2) !== PIPE || next === AMP && s.charCodeAt(i + 2) !== AMP) i += 2;
|
|
38
|
+
else {
|
|
39
|
+
out += s.slice(last, i) + placeholder(++idx);
|
|
40
|
+
last = ++i;
|
|
41
|
+
}
|
|
42
|
+
} else if (c === DQUOTE) i = skipQuotedIdentifier(s, i);
|
|
43
|
+
else if (c === SQUOTE) i = skipStringLiteral(s, i);
|
|
44
|
+
else if (c === DASH && s.charCodeAt(i + 1) === DASH) {
|
|
45
|
+
const end = s.indexOf("\n", i + 2);
|
|
46
|
+
i = end === -1 ? n : end + 1;
|
|
47
|
+
} else if (c === SLASH && s.charCodeAt(i + 1) === STAR) i = skipBlockComment(s, i);
|
|
48
|
+
else if (c === DOLLAR) i = skipDollarQuoted(s, i);
|
|
49
|
+
else i++;
|
|
50
|
+
}
|
|
13
51
|
return {
|
|
14
|
-
sql:
|
|
52
|
+
sql: last === 0 ? s : out + s.slice(last),
|
|
15
53
|
params: fragment.params
|
|
16
54
|
};
|
|
17
55
|
}
|
|
56
|
+
const QMARK = 63;
|
|
57
|
+
const PIPE = 124;
|
|
58
|
+
const AMP = 38;
|
|
59
|
+
const BACKSLASH = 92;
|
|
60
|
+
const SQUOTE = 39;
|
|
61
|
+
const DQUOTE = 34;
|
|
62
|
+
const DASH = 45;
|
|
63
|
+
const SLASH = 47;
|
|
64
|
+
const STAR = 42;
|
|
65
|
+
const DOLLAR = 36;
|
|
66
|
+
/** Index just past the `"…"` identifier opening at `at` (`""` escapes a quote). */
|
|
67
|
+
function skipQuotedIdentifier(s, at) {
|
|
68
|
+
let i = at + 1;
|
|
69
|
+
for (;;) {
|
|
70
|
+
const end = s.indexOf("\"", i);
|
|
71
|
+
if (end === -1) return s.length;
|
|
72
|
+
if (s.charCodeAt(end + 1) !== DQUOTE) return end + 1;
|
|
73
|
+
i = end + 2;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Index just past the `'…'` literal opening at `at` (`''` escapes a quote; in
|
|
78
|
+
* a PostgreSQL `E'…'` escape string a backslash escapes the next char too).
|
|
79
|
+
*/
|
|
80
|
+
function skipStringLiteral(s, at) {
|
|
81
|
+
const prev = at > 0 ? s.charCodeAt(at - 1) : 0;
|
|
82
|
+
const escapes = (prev === 69 || prev === 101) && (at < 2 || !isIdentChar(s.charCodeAt(at - 2)));
|
|
83
|
+
let i = at + 1;
|
|
84
|
+
for (;;) {
|
|
85
|
+
const end = s.indexOf("'", i);
|
|
86
|
+
if (end === -1) return s.length;
|
|
87
|
+
if (escapes) {
|
|
88
|
+
let k = end - 1;
|
|
89
|
+
while (k > at && s.charCodeAt(k) === BACKSLASH) k--;
|
|
90
|
+
if ((end - 1 - k) % 2 === 1) {
|
|
91
|
+
i = end + 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (s.charCodeAt(end + 1) !== SQUOTE) return end + 1;
|
|
96
|
+
i = end + 2;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Index just past the `/* … *\/` comment opening at `at` (comments nest, as in PostgreSQL). */
|
|
100
|
+
function skipBlockComment(s, at) {
|
|
101
|
+
let depth = 1;
|
|
102
|
+
let i = at + 2;
|
|
103
|
+
while (i < s.length) {
|
|
104
|
+
const c = s.charCodeAt(i);
|
|
105
|
+
if (c === STAR && s.charCodeAt(i + 1) === SLASH) {
|
|
106
|
+
i += 2;
|
|
107
|
+
if (--depth === 0) return i;
|
|
108
|
+
} else if (c === SLASH && s.charCodeAt(i + 1) === STAR) {
|
|
109
|
+
depth++;
|
|
110
|
+
i += 2;
|
|
111
|
+
} else i++;
|
|
112
|
+
}
|
|
113
|
+
return s.length;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Index just past the dollar-quoted string (`$$…$$`, `$tag$…$tag$`) opening at
|
|
117
|
+
* `at`, or `at + 1` when the `$` opens none (a `$1` placeholder, a `$` inside
|
|
118
|
+
* an identifier).
|
|
119
|
+
*/
|
|
120
|
+
function skipDollarQuoted(s, at) {
|
|
121
|
+
if (at > 0 && isIdentChar(s.charCodeAt(at - 1))) return at + 1;
|
|
122
|
+
let j = at + 1;
|
|
123
|
+
const first = s.charCodeAt(j);
|
|
124
|
+
if (first !== DOLLAR) {
|
|
125
|
+
if (!(first >= 65 && first <= 90 || first >= 97 && first <= 122 || first === 95)) return at + 1;
|
|
126
|
+
while (j < s.length && isIdentChar(s.charCodeAt(j)) && s.charCodeAt(j) !== DOLLAR) j++;
|
|
127
|
+
if (s.charCodeAt(j) !== DOLLAR) return at + 1;
|
|
128
|
+
}
|
|
129
|
+
const tag = s.slice(at, j + 1);
|
|
130
|
+
const end = s.indexOf(tag, j + 1);
|
|
131
|
+
return end === -1 ? s.length : end + tag.length;
|
|
132
|
+
}
|
|
133
|
+
function isIdentChar(c) {
|
|
134
|
+
return c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c === 36;
|
|
135
|
+
}
|
|
18
136
|
/**
|
|
19
137
|
* How HAVING references a grouped NON-column expression (a calendar bucket, a
|
|
20
138
|
* view's JSON-extracted dimension): its quoted SELECT `alias` on dialects that
|
|
@@ -952,6 +1070,59 @@ function buildInsert(dialect, table, data) {
|
|
|
952
1070
|
});
|
|
953
1071
|
}
|
|
954
1072
|
/**
|
|
1073
|
+
* Single-row INSERT builder that reuses the statement text per column
|
|
1074
|
+
* signature — the same SQL {@link buildInsert} renders, built once per
|
|
1075
|
+
* distinct key list (in order) of the rows of one table instead of per row.
|
|
1076
|
+
* Bounded: once `max` signatures are held the cache starts over.
|
|
1077
|
+
* @since 0.1.151
|
|
1078
|
+
*/
|
|
1079
|
+
var InsertSqlCache = class {
|
|
1080
|
+
_dialect;
|
|
1081
|
+
_max;
|
|
1082
|
+
_sql = /* @__PURE__ */ new Map();
|
|
1083
|
+
_table;
|
|
1084
|
+
/** The previous row's keys and SQL — a batch of one shape skips the signature. */
|
|
1085
|
+
_lastKeys;
|
|
1086
|
+
_lastSql = "";
|
|
1087
|
+
constructor(_dialect, _max = 64) {
|
|
1088
|
+
this._dialect = _dialect;
|
|
1089
|
+
this._max = _max;
|
|
1090
|
+
}
|
|
1091
|
+
build(table, data) {
|
|
1092
|
+
if (table !== this._table) {
|
|
1093
|
+
this._sql.clear();
|
|
1094
|
+
this._table = table;
|
|
1095
|
+
this._lastKeys = void 0;
|
|
1096
|
+
}
|
|
1097
|
+
const keys = Object.keys(data);
|
|
1098
|
+
let sql;
|
|
1099
|
+
if (this._lastKeys !== void 0 && sameKeys(this._lastKeys, keys)) sql = this._lastSql;
|
|
1100
|
+
else {
|
|
1101
|
+
const signature = keys.join("\0");
|
|
1102
|
+
sql = this._sql.get(signature);
|
|
1103
|
+
if (sql === void 0) {
|
|
1104
|
+
sql = buildInsert(this._dialect, table, data).sql;
|
|
1105
|
+
if (this._sql.size >= this._max) this._sql.clear();
|
|
1106
|
+
this._sql.set(signature, sql);
|
|
1107
|
+
}
|
|
1108
|
+
this._lastKeys = keys;
|
|
1109
|
+
this._lastSql = sql;
|
|
1110
|
+
}
|
|
1111
|
+
const dialect = this._dialect;
|
|
1112
|
+
const params = [];
|
|
1113
|
+
for (const key of keys) params.push(dialect.toValue(data[key]));
|
|
1114
|
+
return {
|
|
1115
|
+
sql,
|
|
1116
|
+
params
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
function sameKeys(a, b) {
|
|
1121
|
+
if (a.length !== b.length) return false;
|
|
1122
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
1123
|
+
return true;
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
955
1126
|
* The columns of a multi-row INSERT: the union of the rows' keys, in
|
|
956
1127
|
* first-seen order (rows may differ in shape — an optional field omitted on
|
|
957
1128
|
* some).
|
|
@@ -1459,6 +1630,7 @@ exports.AGG_FN_SQL = AGG_FN_SQL;
|
|
|
1459
1630
|
exports.EMPTY_AND = EMPTY_AND;
|
|
1460
1631
|
exports.EMPTY_OR = EMPTY_OR;
|
|
1461
1632
|
exports.GEO_DISTANCE_ALIAS = GEO_DISTANCE_ALIAS;
|
|
1633
|
+
exports.InsertSqlCache = InsertSqlCache;
|
|
1462
1634
|
exports.PARTITION_ROW_NUMBER_ALIAS = PARTITION_ROW_NUMBER_ALIAS;
|
|
1463
1635
|
exports.SEARCH_SOURCE_ALIAS = SEARCH_SOURCE_ALIAS;
|
|
1464
1636
|
exports.SQL_DEFAULT = SQL_DEFAULT;
|
package/dist/index.d.cts
CHANGED
|
@@ -150,6 +150,15 @@ interface SqlDialect {
|
|
|
150
150
|
/**
|
|
151
151
|
* Replaces positional `?` placeholders with dialect-specific numbered placeholders
|
|
152
152
|
* (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
|
|
153
|
+
*
|
|
154
|
+
* Only a `?` in plain SQL text is a placeholder: one inside a string literal
|
|
155
|
+
* (`'…'`, `E'…'`, `$tag$…$tag$`), a quoted identifier (`"…"`) or a comment
|
|
156
|
+
* (`-- …`, `/* … *\/`) is left alone, and so are PostgreSQL's jsonb operators
|
|
157
|
+
* `?|` / `?&` (a placeholder directly followed by `|` / `&` is only ever
|
|
158
|
+
* `?||` / `?&&`, the concatenation / overlap). `??` is an escaped literal
|
|
159
|
+
* `?` — how a builder writes the bare jsonb `?` operator and any other
|
|
160
|
+
* operator that contains `?` (`@?` → `@??`, `?#` → `??#`, `?-` → `??-`).
|
|
161
|
+
* Block comments nest, as in PostgreSQL.
|
|
153
162
|
*/
|
|
154
163
|
declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TSqlFragment;
|
|
155
164
|
/**
|
|
@@ -404,6 +413,24 @@ declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TV
|
|
|
404
413
|
* Builds an INSERT statement.
|
|
405
414
|
*/
|
|
406
415
|
declare function buildInsert(dialect: SqlDialect, table: string, data: Record<string, unknown>): TSqlFragment;
|
|
416
|
+
/**
|
|
417
|
+
* Single-row INSERT builder that reuses the statement text per column
|
|
418
|
+
* signature — the same SQL {@link buildInsert} renders, built once per
|
|
419
|
+
* distinct key list (in order) of the rows of one table instead of per row.
|
|
420
|
+
* Bounded: once `max` signatures are held the cache starts over.
|
|
421
|
+
* @since 0.1.151
|
|
422
|
+
*/
|
|
423
|
+
declare class InsertSqlCache {
|
|
424
|
+
private readonly _dialect;
|
|
425
|
+
private readonly _max;
|
|
426
|
+
private readonly _sql;
|
|
427
|
+
private _table?;
|
|
428
|
+
/** The previous row's keys and SQL — a batch of one shape skips the signature. */
|
|
429
|
+
private _lastKeys?;
|
|
430
|
+
private _lastSql;
|
|
431
|
+
constructor(_dialect: SqlDialect, _max?: number);
|
|
432
|
+
build(table: string, data: Record<string, unknown>): TSqlFragment;
|
|
433
|
+
}
|
|
407
434
|
/**
|
|
408
435
|
* The columns of a multi-row INSERT: the union of the rows' keys, in
|
|
409
436
|
* first-seen order (rows may differ in shape — an optional field omitted on
|
|
@@ -638,4 +665,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
638
665
|
flags: string;
|
|
639
666
|
};
|
|
640
667
|
//#endregion
|
|
641
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
|
|
668
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
|
package/dist/index.d.mts
CHANGED
|
@@ -150,6 +150,15 @@ interface SqlDialect {
|
|
|
150
150
|
/**
|
|
151
151
|
* Replaces positional `?` placeholders with dialect-specific numbered placeholders
|
|
152
152
|
* (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
|
|
153
|
+
*
|
|
154
|
+
* Only a `?` in plain SQL text is a placeholder: one inside a string literal
|
|
155
|
+
* (`'…'`, `E'…'`, `$tag$…$tag$`), a quoted identifier (`"…"`) or a comment
|
|
156
|
+
* (`-- …`, `/* … *\/`) is left alone, and so are PostgreSQL's jsonb operators
|
|
157
|
+
* `?|` / `?&` (a placeholder directly followed by `|` / `&` is only ever
|
|
158
|
+
* `?||` / `?&&`, the concatenation / overlap). `??` is an escaped literal
|
|
159
|
+
* `?` — how a builder writes the bare jsonb `?` operator and any other
|
|
160
|
+
* operator that contains `?` (`@?` → `@??`, `?#` → `??#`, `?-` → `??-`).
|
|
161
|
+
* Block comments nest, as in PostgreSQL.
|
|
153
162
|
*/
|
|
154
163
|
declare function finalizeParams(dialect: SqlDialect, fragment: TSqlFragment): TSqlFragment;
|
|
155
164
|
/**
|
|
@@ -404,6 +413,24 @@ declare function buildCreateView(dialect: SqlDialect, viewName: string, plan: TV
|
|
|
404
413
|
* Builds an INSERT statement.
|
|
405
414
|
*/
|
|
406
415
|
declare function buildInsert(dialect: SqlDialect, table: string, data: Record<string, unknown>): TSqlFragment;
|
|
416
|
+
/**
|
|
417
|
+
* Single-row INSERT builder that reuses the statement text per column
|
|
418
|
+
* signature — the same SQL {@link buildInsert} renders, built once per
|
|
419
|
+
* distinct key list (in order) of the rows of one table instead of per row.
|
|
420
|
+
* Bounded: once `max` signatures are held the cache starts over.
|
|
421
|
+
* @since 0.1.151
|
|
422
|
+
*/
|
|
423
|
+
declare class InsertSqlCache {
|
|
424
|
+
private readonly _dialect;
|
|
425
|
+
private readonly _max;
|
|
426
|
+
private readonly _sql;
|
|
427
|
+
private _table?;
|
|
428
|
+
/** The previous row's keys and SQL — a batch of one shape skips the signature. */
|
|
429
|
+
private _lastKeys?;
|
|
430
|
+
private _lastSql;
|
|
431
|
+
constructor(_dialect: SqlDialect, _max?: number);
|
|
432
|
+
build(table: string, data: Record<string, unknown>): TSqlFragment;
|
|
433
|
+
}
|
|
407
434
|
/**
|
|
408
435
|
* The columns of a multi-row INSERT: the union of the rows' keys, in
|
|
409
436
|
* first-seen order (rows may differ in shape — an optional field omitted on
|
|
@@ -638,4 +665,4 @@ declare function parseRegexString(value: unknown): {
|
|
|
638
665
|
flags: string;
|
|
639
666
|
};
|
|
640
667
|
//#endregion
|
|
641
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
|
|
668
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, type SqlDialect, type TArithFailure, type TFilterVisitorOptions, type TGeoCircle, type TGeoSearchControls, type TGeoWindow, type TReplaceColumn, type TSqlFragment, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
|
package/dist/index.mjs
CHANGED
|
@@ -5,15 +5,133 @@ import { assertAggregateFn, resolveAlias } from "@atscript/db/agg";
|
|
|
5
5
|
/**
|
|
6
6
|
* Replaces positional `?` placeholders with dialect-specific numbered placeholders
|
|
7
7
|
* (e.g. `$1, $2, ...` for PostgreSQL). No-op when `dialect.paramPlaceholder` is not set.
|
|
8
|
+
*
|
|
9
|
+
* Only a `?` in plain SQL text is a placeholder: one inside a string literal
|
|
10
|
+
* (`'…'`, `E'…'`, `$tag$…$tag$`), a quoted identifier (`"…"`) or a comment
|
|
11
|
+
* (`-- …`, `/* … *\/`) is left alone, and so are PostgreSQL's jsonb operators
|
|
12
|
+
* `?|` / `?&` (a placeholder directly followed by `|` / `&` is only ever
|
|
13
|
+
* `?||` / `?&&`, the concatenation / overlap). `??` is an escaped literal
|
|
14
|
+
* `?` — how a builder writes the bare jsonb `?` operator and any other
|
|
15
|
+
* operator that contains `?` (`@?` → `@??`, `?#` → `??#`, `?-` → `??-`).
|
|
16
|
+
* Block comments nest, as in PostgreSQL.
|
|
8
17
|
*/
|
|
9
18
|
function finalizeParams(dialect, fragment) {
|
|
10
|
-
|
|
19
|
+
const placeholder = dialect.paramPlaceholder;
|
|
20
|
+
if (!placeholder) return fragment;
|
|
21
|
+
const s = fragment.sql;
|
|
22
|
+
if (s.indexOf("?") === -1) return fragment;
|
|
23
|
+
let out = "";
|
|
24
|
+
let last = 0;
|
|
11
25
|
let idx = 0;
|
|
26
|
+
const n = s.length;
|
|
27
|
+
let i = 0;
|
|
28
|
+
while (i < n) {
|
|
29
|
+
const c = s.charCodeAt(i);
|
|
30
|
+
if (c === QMARK) {
|
|
31
|
+
const next = s.charCodeAt(i + 1);
|
|
32
|
+
if (next === QMARK) {
|
|
33
|
+
out += s.slice(last, i + 1);
|
|
34
|
+
last = i + 2;
|
|
35
|
+
i += 2;
|
|
36
|
+
} else if (next === PIPE && s.charCodeAt(i + 2) !== PIPE || next === AMP && s.charCodeAt(i + 2) !== AMP) i += 2;
|
|
37
|
+
else {
|
|
38
|
+
out += s.slice(last, i) + placeholder(++idx);
|
|
39
|
+
last = ++i;
|
|
40
|
+
}
|
|
41
|
+
} else if (c === DQUOTE) i = skipQuotedIdentifier(s, i);
|
|
42
|
+
else if (c === SQUOTE) i = skipStringLiteral(s, i);
|
|
43
|
+
else if (c === DASH && s.charCodeAt(i + 1) === DASH) {
|
|
44
|
+
const end = s.indexOf("\n", i + 2);
|
|
45
|
+
i = end === -1 ? n : end + 1;
|
|
46
|
+
} else if (c === SLASH && s.charCodeAt(i + 1) === STAR) i = skipBlockComment(s, i);
|
|
47
|
+
else if (c === DOLLAR) i = skipDollarQuoted(s, i);
|
|
48
|
+
else i++;
|
|
49
|
+
}
|
|
12
50
|
return {
|
|
13
|
-
sql:
|
|
51
|
+
sql: last === 0 ? s : out + s.slice(last),
|
|
14
52
|
params: fragment.params
|
|
15
53
|
};
|
|
16
54
|
}
|
|
55
|
+
const QMARK = 63;
|
|
56
|
+
const PIPE = 124;
|
|
57
|
+
const AMP = 38;
|
|
58
|
+
const BACKSLASH = 92;
|
|
59
|
+
const SQUOTE = 39;
|
|
60
|
+
const DQUOTE = 34;
|
|
61
|
+
const DASH = 45;
|
|
62
|
+
const SLASH = 47;
|
|
63
|
+
const STAR = 42;
|
|
64
|
+
const DOLLAR = 36;
|
|
65
|
+
/** Index just past the `"…"` identifier opening at `at` (`""` escapes a quote). */
|
|
66
|
+
function skipQuotedIdentifier(s, at) {
|
|
67
|
+
let i = at + 1;
|
|
68
|
+
for (;;) {
|
|
69
|
+
const end = s.indexOf("\"", i);
|
|
70
|
+
if (end === -1) return s.length;
|
|
71
|
+
if (s.charCodeAt(end + 1) !== DQUOTE) return end + 1;
|
|
72
|
+
i = end + 2;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Index just past the `'…'` literal opening at `at` (`''` escapes a quote; in
|
|
77
|
+
* a PostgreSQL `E'…'` escape string a backslash escapes the next char too).
|
|
78
|
+
*/
|
|
79
|
+
function skipStringLiteral(s, at) {
|
|
80
|
+
const prev = at > 0 ? s.charCodeAt(at - 1) : 0;
|
|
81
|
+
const escapes = (prev === 69 || prev === 101) && (at < 2 || !isIdentChar(s.charCodeAt(at - 2)));
|
|
82
|
+
let i = at + 1;
|
|
83
|
+
for (;;) {
|
|
84
|
+
const end = s.indexOf("'", i);
|
|
85
|
+
if (end === -1) return s.length;
|
|
86
|
+
if (escapes) {
|
|
87
|
+
let k = end - 1;
|
|
88
|
+
while (k > at && s.charCodeAt(k) === BACKSLASH) k--;
|
|
89
|
+
if ((end - 1 - k) % 2 === 1) {
|
|
90
|
+
i = end + 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (s.charCodeAt(end + 1) !== SQUOTE) return end + 1;
|
|
95
|
+
i = end + 2;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Index just past the `/* … *\/` comment opening at `at` (comments nest, as in PostgreSQL). */
|
|
99
|
+
function skipBlockComment(s, at) {
|
|
100
|
+
let depth = 1;
|
|
101
|
+
let i = at + 2;
|
|
102
|
+
while (i < s.length) {
|
|
103
|
+
const c = s.charCodeAt(i);
|
|
104
|
+
if (c === STAR && s.charCodeAt(i + 1) === SLASH) {
|
|
105
|
+
i += 2;
|
|
106
|
+
if (--depth === 0) return i;
|
|
107
|
+
} else if (c === SLASH && s.charCodeAt(i + 1) === STAR) {
|
|
108
|
+
depth++;
|
|
109
|
+
i += 2;
|
|
110
|
+
} else i++;
|
|
111
|
+
}
|
|
112
|
+
return s.length;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Index just past the dollar-quoted string (`$$…$$`, `$tag$…$tag$`) opening at
|
|
116
|
+
* `at`, or `at + 1` when the `$` opens none (a `$1` placeholder, a `$` inside
|
|
117
|
+
* an identifier).
|
|
118
|
+
*/
|
|
119
|
+
function skipDollarQuoted(s, at) {
|
|
120
|
+
if (at > 0 && isIdentChar(s.charCodeAt(at - 1))) return at + 1;
|
|
121
|
+
let j = at + 1;
|
|
122
|
+
const first = s.charCodeAt(j);
|
|
123
|
+
if (first !== DOLLAR) {
|
|
124
|
+
if (!(first >= 65 && first <= 90 || first >= 97 && first <= 122 || first === 95)) return at + 1;
|
|
125
|
+
while (j < s.length && isIdentChar(s.charCodeAt(j)) && s.charCodeAt(j) !== DOLLAR) j++;
|
|
126
|
+
if (s.charCodeAt(j) !== DOLLAR) return at + 1;
|
|
127
|
+
}
|
|
128
|
+
const tag = s.slice(at, j + 1);
|
|
129
|
+
const end = s.indexOf(tag, j + 1);
|
|
130
|
+
return end === -1 ? s.length : end + tag.length;
|
|
131
|
+
}
|
|
132
|
+
function isIdentChar(c) {
|
|
133
|
+
return c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122 || c === 95 || c === 36;
|
|
134
|
+
}
|
|
17
135
|
/**
|
|
18
136
|
* How HAVING references a grouped NON-column expression (a calendar bucket, a
|
|
19
137
|
* view's JSON-extracted dimension): its quoted SELECT `alias` on dialects that
|
|
@@ -951,6 +1069,59 @@ function buildInsert(dialect, table, data) {
|
|
|
951
1069
|
});
|
|
952
1070
|
}
|
|
953
1071
|
/**
|
|
1072
|
+
* Single-row INSERT builder that reuses the statement text per column
|
|
1073
|
+
* signature — the same SQL {@link buildInsert} renders, built once per
|
|
1074
|
+
* distinct key list (in order) of the rows of one table instead of per row.
|
|
1075
|
+
* Bounded: once `max` signatures are held the cache starts over.
|
|
1076
|
+
* @since 0.1.151
|
|
1077
|
+
*/
|
|
1078
|
+
var InsertSqlCache = class {
|
|
1079
|
+
_dialect;
|
|
1080
|
+
_max;
|
|
1081
|
+
_sql = /* @__PURE__ */ new Map();
|
|
1082
|
+
_table;
|
|
1083
|
+
/** The previous row's keys and SQL — a batch of one shape skips the signature. */
|
|
1084
|
+
_lastKeys;
|
|
1085
|
+
_lastSql = "";
|
|
1086
|
+
constructor(_dialect, _max = 64) {
|
|
1087
|
+
this._dialect = _dialect;
|
|
1088
|
+
this._max = _max;
|
|
1089
|
+
}
|
|
1090
|
+
build(table, data) {
|
|
1091
|
+
if (table !== this._table) {
|
|
1092
|
+
this._sql.clear();
|
|
1093
|
+
this._table = table;
|
|
1094
|
+
this._lastKeys = void 0;
|
|
1095
|
+
}
|
|
1096
|
+
const keys = Object.keys(data);
|
|
1097
|
+
let sql;
|
|
1098
|
+
if (this._lastKeys !== void 0 && sameKeys(this._lastKeys, keys)) sql = this._lastSql;
|
|
1099
|
+
else {
|
|
1100
|
+
const signature = keys.join("\0");
|
|
1101
|
+
sql = this._sql.get(signature);
|
|
1102
|
+
if (sql === void 0) {
|
|
1103
|
+
sql = buildInsert(this._dialect, table, data).sql;
|
|
1104
|
+
if (this._sql.size >= this._max) this._sql.clear();
|
|
1105
|
+
this._sql.set(signature, sql);
|
|
1106
|
+
}
|
|
1107
|
+
this._lastKeys = keys;
|
|
1108
|
+
this._lastSql = sql;
|
|
1109
|
+
}
|
|
1110
|
+
const dialect = this._dialect;
|
|
1111
|
+
const params = [];
|
|
1112
|
+
for (const key of keys) params.push(dialect.toValue(data[key]));
|
|
1113
|
+
return {
|
|
1114
|
+
sql,
|
|
1115
|
+
params
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
function sameKeys(a, b) {
|
|
1120
|
+
if (a.length !== b.length) return false;
|
|
1121
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
954
1125
|
* The columns of a multi-row INSERT: the union of the rows' keys, in
|
|
955
1126
|
* first-seen order (rows may differ in shape — an optional field omitted on
|
|
956
1127
|
* some).
|
|
@@ -1454,4 +1625,4 @@ function parseRegexString(value) {
|
|
|
1454
1625
|
};
|
|
1455
1626
|
}
|
|
1456
1627
|
//#endregion
|
|
1457
|
-
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
|
|
1628
|
+
export { AGG_FN_SQL, EMPTY_AND, EMPTY_OR, GEO_DISTANCE_ALIAS, InsertSqlCache, PARTITION_ROW_NUMBER_ALIAS, SEARCH_SOURCE_ALIAS, SQL_DEFAULT, VECTOR_DISTANCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildProjection, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere, chunkInsertRows, createFilterVisitor, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, finalizeParams, foreignKeySql, geoWindowFromControls, groupKeySql, insertManyColumns, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, orderKeySql, parseRegexString, queryNodeToSql, queryOpToSql, quotedJsonPathSegments, refActionToSql, renameGeoDistance, renderArith, replaceColumnsFor, sqlStringLiteral, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource };
|
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.152",
|
|
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.14",
|
|
41
|
+
"unplugin-atscript": "^0.1.102"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@uniqu/core": "^0.1.
|
|
45
|
-
"@atscript/db": "^0.1.
|
|
44
|
+
"@uniqu/core": "^0.1.14",
|
|
45
|
+
"@atscript/db": "^0.1.152"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "vp pack",
|