@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 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
- if (!dialect.paramPlaceholder) return fragment;
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: fragment.sql.replace(/\?/g, () => dialect.paramPlaceholder(++idx)),
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
- if (!dialect.paramPlaceholder) return fragment;
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: fragment.sql.replace(/\?/g, () => dialect.paramPlaceholder(++idx)),
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.150",
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.13",
41
- "unplugin-atscript": "^0.1.101"
40
+ "@uniqu/core": "^0.1.14",
41
+ "unplugin-atscript": "^0.1.102"
42
42
  },
43
43
  "peerDependencies": {
44
- "@uniqu/core": "^0.1.13",
45
- "@atscript/db": "^0.1.150"
44
+ "@uniqu/core": "^0.1.14",
45
+ "@atscript/db": "^0.1.152"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "vp pack",