@askdb/core 1.0.0-beta.41 → 1.0.0-beta.42
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 +39 -1
- package/dist/ask.d.ts +39 -0
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +175 -10
- package/dist/ask.js.map +1 -1
- package/dist/errors.d.ts +49 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +20 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/logging/log-events.d.ts +13 -0
- package/dist/logging/log-events.d.ts.map +1 -1
- package/dist/logging/log-events.js +13 -0
- package/dist/logging/log-events.js.map +1 -1
- package/dist/sql/bind.d.ts +100 -0
- package/dist/sql/bind.d.ts.map +1 -0
- package/dist/sql/bind.js +560 -0
- package/dist/sql/bind.js.map +1 -0
- package/dist/sql/dialect-spec.d.ts +4 -0
- package/dist/sql/dialect-spec.d.ts.map +1 -1
- package/dist/sql/dialect-spec.js +8 -0
- package/dist/sql/dialect-spec.js.map +1 -1
- package/dist/sql/extract-sql.d.ts +8 -1
- package/dist/sql/extract-sql.d.ts.map +1 -1
- package/dist/sql/extract-sql.js +27 -4
- package/dist/sql/extract-sql.js.map +1 -1
- package/dist/sql/generate.d.ts +10 -0
- package/dist/sql/generate.d.ts.map +1 -1
- package/dist/sql/generate.js +72 -4
- package/dist/sql/generate.js.map +1 -1
- package/dist/sql/index.d.ts +4 -2
- package/dist/sql/index.d.ts.map +1 -1
- package/dist/sql/index.js +4 -2
- package/dist/sql/index.js.map +1 -1
- package/dist/sql/parameter-manifest.d.ts +34 -0
- package/dist/sql/parameter-manifest.d.ts.map +1 -0
- package/dist/sql/parameter-manifest.js +150 -0
- package/dist/sql/parameter-manifest.js.map +1 -0
- package/dist/sql/prompt.d.ts +7 -1
- package/dist/sql/prompt.d.ts.map +1 -1
- package/dist/sql/prompt.js +24 -1
- package/dist/sql/prompt.js.map +1 -1
- package/dist/sql/sensitive-guardrail.d.ts +47 -0
- package/dist/sql/sensitive-guardrail.d.ts.map +1 -0
- package/dist/sql/sensitive-guardrail.js +589 -0
- package/dist/sql/sensitive-guardrail.js.map +1 -0
- package/dist/sql/tenant-placeholders.d.ts +3 -2
- package/dist/sql/tenant-placeholders.d.ts.map +1 -1
- package/dist/sql/tenant-placeholders.js +14 -11
- package/dist/sql/tenant-placeholders.js.map +1 -1
- package/dist/sql/validate.d.ts.map +1 -1
- package/dist/sql/validate.js +1 -47
- package/dist/sql/validate.js.map +1 -1
- package/package.json +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-events.js","sourceRoot":"","sources":["../../src/logging/log-events.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,QAAQ,EAAE,iBAAiB;IAC3B,MAAM,EAAE,eAAe;IACvB,QAAQ,EAAE,iBAAiB;IAC3B,kFAAkF;IAClF,YAAY,EAAE,qBAAqB;IACnC,qBAAqB,EAAE,+BAA+B;IACtD,wBAAwB,EAAE,kCAAkC;IAC5D,yFAAyF;IACzF,uBAAuB,EAAE,iCAAiC;IAC1D,4GAA4G;IAC5G,gCAAgC,EAAE,2CAA2C;IAC7E,yEAAyE;IACzE,cAAc,EAAE,uBAAuB;IACvC;;;OAGG;IACH,qBAAqB,EAAE,+BAA+B;IACtD;;;;OAIG;IACH,wBAAwB,EAAE,kCAAkC;IAC5D,mFAAmF;IACnF,oBAAoB,EAAE,8BAA8B;IACpD,yDAAyD;IACzD,mBAAmB,EAAE,6BAA6B;IAClD,yCAAyC;IACzC,qBAAqB,EAAE,+BAA+B;IACtD,iFAAiF;IACjF,qBAAqB,EAAE,+BAA+B;
|
|
1
|
+
{"version":3,"file":"log-events.js","sourceRoot":"","sources":["../../src/logging/log-events.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,QAAQ,EAAE,iBAAiB;IAC3B,MAAM,EAAE,eAAe;IACvB,QAAQ,EAAE,iBAAiB;IAC3B,kFAAkF;IAClF,YAAY,EAAE,qBAAqB;IACnC,qBAAqB,EAAE,+BAA+B;IACtD,wBAAwB,EAAE,kCAAkC;IAC5D,yFAAyF;IACzF,uBAAuB,EAAE,iCAAiC;IAC1D,4GAA4G;IAC5G,gCAAgC,EAAE,2CAA2C;IAC7E,yEAAyE;IACzE,cAAc,EAAE,uBAAuB;IACvC;;;OAGG;IACH,qBAAqB,EAAE,+BAA+B;IACtD;;;;OAIG;IACH,wBAAwB,EAAE,kCAAkC;IAC5D,mFAAmF;IACnF,oBAAoB,EAAE,8BAA8B;IACpD,yDAAyD;IACzD,mBAAmB,EAAE,6BAA6B;IAClD;;;;;OAKG;IACH,2BAA2B,EAAE,sCAAsC;IACnE,yCAAyC;IACzC,qBAAqB,EAAE,+BAA+B;IACtD,iFAAiF;IACjF,qBAAqB,EAAE,+BAA+B;IACtD;;;;OAIG;IACH,qBAAqB,EAAE,8BAA8B;CAC7C,CAAC"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type BuiltInDialectId, type DialectSpec } from "./dialect-spec.js";
|
|
2
|
+
export type QueryParameterType = "string" | "number" | "boolean" | "date" | "datetime";
|
|
3
|
+
export type QueryParameterValue = string | number | boolean;
|
|
4
|
+
/** One slot in `BoundQuery.params` — a scalar, or an array for Postgres/CockroachDB listBinding. */
|
|
5
|
+
export type QueryParamSlot = QueryParameterValue | QueryParameterValue[];
|
|
6
|
+
/** One parameter as bound for this call — enough to render a form field. */
|
|
7
|
+
export type QueryParameterBinding = {
|
|
8
|
+
name: string;
|
|
9
|
+
placeholder: string;
|
|
10
|
+
type: QueryParameterType;
|
|
11
|
+
cardinality: "one" | "many";
|
|
12
|
+
description?: string;
|
|
13
|
+
value: QueryParameterValue | QueryParameterValue[];
|
|
14
|
+
/** Driver markers this parameter fills in `unboundSql`, in order. */
|
|
15
|
+
markers: string[];
|
|
16
|
+
/** 0-based indices into `params` this parameter occupies, in order. */
|
|
17
|
+
indices: number[];
|
|
18
|
+
source: "question" | "tenant";
|
|
19
|
+
};
|
|
20
|
+
/** Serializable input to bindPreparedQuery(). Definitions and template only — no values. */
|
|
21
|
+
export type PreparedQuery = {
|
|
22
|
+
version: 1;
|
|
23
|
+
dialect: BuiltInDialectId;
|
|
24
|
+
/** SQL with unquoted `:name` / `:tenant_*` placeholders intact. */
|
|
25
|
+
namedSql: string;
|
|
26
|
+
parameters: Array<{
|
|
27
|
+
name: string;
|
|
28
|
+
placeholder: string;
|
|
29
|
+
type: QueryParameterType;
|
|
30
|
+
cardinality: "one" | "many";
|
|
31
|
+
description?: string;
|
|
32
|
+
source: "question" | "tenant";
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
export type BoundQuery = {
|
|
36
|
+
/** Ready to run as-is — every placeholder replaced with an escaped literal. */
|
|
37
|
+
sql: string;
|
|
38
|
+
/** Same statement with driver markers instead of literals. */
|
|
39
|
+
unboundSql: string;
|
|
40
|
+
params: QueryParamSlot[];
|
|
41
|
+
bindings: QueryParameterBinding[];
|
|
42
|
+
};
|
|
43
|
+
export type SqlSpanKind = "code" | "quoted";
|
|
44
|
+
export type SqlSpan = {
|
|
45
|
+
kind: SqlSpanKind;
|
|
46
|
+
start: number;
|
|
47
|
+
end: number;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Tokenize SQL into contiguous code vs quoted regions.
|
|
51
|
+
* Recognizes single-quoted strings (doubled-quote escapes), double-quoted
|
|
52
|
+
* identifiers, PostgreSQL dollar-quoting, MySQL backticks, and SQL Server brackets.
|
|
53
|
+
*/
|
|
54
|
+
export declare function tokenizeSqlSpans(sql: string): SqlSpan[];
|
|
55
|
+
/**
|
|
56
|
+
* Strip quoted regions for keyword / multi-statement checks.
|
|
57
|
+
* Preserves historical replacement markers for quotes the old stripper knew
|
|
58
|
+
* about (`''`, `""`, `$$`); new quote forms (backticks, brackets) become spaces.
|
|
59
|
+
*/
|
|
60
|
+
export declare function stripSqlStringLiterals(sql: string): string;
|
|
61
|
+
export type PlaceholderOccurrence = {
|
|
62
|
+
/** Name without the leading colon, e.g. "state_name" or "tenant_agency_ids". */
|
|
63
|
+
name: string;
|
|
64
|
+
/** Full token including colon, e.g. ":state_name". */
|
|
65
|
+
placeholder: string;
|
|
66
|
+
start: number;
|
|
67
|
+
end: number;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Find `:name` placeholders only outside quoted regions, in source order.
|
|
71
|
+
* A quoted `':name'` is invisible by construction.
|
|
72
|
+
*/
|
|
73
|
+
export declare function scanPlaceholders(sql: string): PlaceholderOccurrence[];
|
|
74
|
+
export declare function scanTenantPlaceholders(sql: string): PlaceholderOccurrence[];
|
|
75
|
+
/**
|
|
76
|
+
* A `many` placeholder must be the sole expression inside `IN (...)` / `NOT IN (...)`,
|
|
77
|
+
* or the sole argument of `= ANY(...)` on array dialects.
|
|
78
|
+
*/
|
|
79
|
+
export declare function isValidListContext(sql: string, occurrence: PlaceholderOccurrence, listBinding: "array" | "expand"): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Escape a scalar value as a SQL literal for the given dialect.
|
|
82
|
+
* Never includes the raw parameter value in thrown error messages.
|
|
83
|
+
*/
|
|
84
|
+
export declare function escapeSqlLiteral(value: QueryParameterValue, dialect: Pick<DialectSpec, "backslashEscapes"> | undefined): string;
|
|
85
|
+
export declare function escapeSqlLiteralList(values: readonly QueryParameterValue[], dialect: Pick<DialectSpec, "backslashEscapes"> | undefined): string;
|
|
86
|
+
/** Quote-doubling only — used when no dialect is supplied (legacy tenant path). */
|
|
87
|
+
export declare function escapeSqlLiteralLegacy(value: string): string;
|
|
88
|
+
export type MarkerStyle = "dollar" | "question" | "atp";
|
|
89
|
+
export declare function markerStyleForDialect(id: BuiltInDialectId): MarkerStyle;
|
|
90
|
+
export declare function formatMarker(style: MarkerStyle, ordinal: number): string;
|
|
91
|
+
/**
|
|
92
|
+
* Pure, synchronous binder. Substitutes placeholders with escaped literals and
|
|
93
|
+
* allocates dialect-correct driver markers. Does not authorize tenant IDs.
|
|
94
|
+
*/
|
|
95
|
+
export declare function bindPreparedQuery(prepared: PreparedQuery, values: Record<string, QueryParameterValue | readonly QueryParameterValue[]>): BoundQuery;
|
|
96
|
+
/**
|
|
97
|
+
* Compare two SQL strings structurally for the parameterize consistency check.
|
|
98
|
+
*/
|
|
99
|
+
export declare function sqlStructurallyEqual(a: string, b: string): boolean;
|
|
100
|
+
//# sourceMappingURL=bind.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bind.d.ts","sourceRoot":"","sources":["../../src/sql/bind.ts"],"names":[],"mappings":"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"}
|
package/dist/sql/bind.js
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import { QueryParameterError, } from "../errors.js";
|
|
2
|
+
import { BUILT_IN_DIALECTS, } from "./dialect-spec.js";
|
|
3
|
+
import { validateSelectSql } from "./validate.js";
|
|
4
|
+
/**
|
|
5
|
+
* Tokenize SQL into contiguous code vs quoted regions.
|
|
6
|
+
* Recognizes single-quoted strings (doubled-quote escapes), double-quoted
|
|
7
|
+
* identifiers, PostgreSQL dollar-quoting, MySQL backticks, and SQL Server brackets.
|
|
8
|
+
*/
|
|
9
|
+
export function tokenizeSqlSpans(sql) {
|
|
10
|
+
const spans = [];
|
|
11
|
+
let i = 0;
|
|
12
|
+
let codeStart = 0;
|
|
13
|
+
const pushCode = (end) => {
|
|
14
|
+
if (end > codeStart)
|
|
15
|
+
spans.push({ kind: "code", start: codeStart, end });
|
|
16
|
+
};
|
|
17
|
+
while (i < sql.length) {
|
|
18
|
+
const ch = sql[i];
|
|
19
|
+
if (ch === "'") {
|
|
20
|
+
pushCode(i);
|
|
21
|
+
const start = i;
|
|
22
|
+
i++;
|
|
23
|
+
while (i < sql.length) {
|
|
24
|
+
if (sql[i] === "'" && sql[i + 1] === "'") {
|
|
25
|
+
i += 2;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (sql[i] === "'") {
|
|
29
|
+
i++;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
i++;
|
|
33
|
+
}
|
|
34
|
+
spans.push({ kind: "quoted", start, end: i });
|
|
35
|
+
codeStart = i;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ch === '"') {
|
|
39
|
+
pushCode(i);
|
|
40
|
+
const start = i;
|
|
41
|
+
i++;
|
|
42
|
+
while (i < sql.length && sql[i] !== '"') {
|
|
43
|
+
if (sql[i] === "\\")
|
|
44
|
+
i++;
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
if (i < sql.length)
|
|
48
|
+
i++;
|
|
49
|
+
spans.push({ kind: "quoted", start, end: i });
|
|
50
|
+
codeStart = i;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === "`") {
|
|
54
|
+
pushCode(i);
|
|
55
|
+
const start = i;
|
|
56
|
+
i++;
|
|
57
|
+
while (i < sql.length && sql[i] !== "`") {
|
|
58
|
+
if (sql[i] === "\\")
|
|
59
|
+
i++;
|
|
60
|
+
i++;
|
|
61
|
+
}
|
|
62
|
+
if (i < sql.length)
|
|
63
|
+
i++;
|
|
64
|
+
spans.push({ kind: "quoted", start, end: i });
|
|
65
|
+
codeStart = i;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (ch === "[") {
|
|
69
|
+
pushCode(i);
|
|
70
|
+
const start = i;
|
|
71
|
+
i++;
|
|
72
|
+
while (i < sql.length && sql[i] !== "]") {
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
if (i < sql.length)
|
|
76
|
+
i++;
|
|
77
|
+
spans.push({ kind: "quoted", start, end: i });
|
|
78
|
+
codeStart = i;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
// Dollar-quoting: $tag$ … $tag$. Do not treat $1 as a dollar quote.
|
|
82
|
+
if (ch === "$" && /^\$\w*\$/.test(sql.slice(i))) {
|
|
83
|
+
const tagMatch = /^\$(\w*)\$/.exec(sql.slice(i));
|
|
84
|
+
const opener = tagMatch[0];
|
|
85
|
+
const closerAt = sql.indexOf(opener, i + opener.length);
|
|
86
|
+
if (closerAt === -1) {
|
|
87
|
+
// Unterminated — treat remainder as code (matches prior fail-open).
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
pushCode(i);
|
|
91
|
+
const end = closerAt + opener.length;
|
|
92
|
+
spans.push({ kind: "quoted", start: i, end });
|
|
93
|
+
i = end;
|
|
94
|
+
codeStart = i;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
pushCode(sql.length);
|
|
100
|
+
return spans;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Strip quoted regions for keyword / multi-statement checks.
|
|
104
|
+
* Preserves historical replacement markers for quotes the old stripper knew
|
|
105
|
+
* about (`''`, `""`, `$$`); new quote forms (backticks, brackets) become spaces.
|
|
106
|
+
*/
|
|
107
|
+
export function stripSqlStringLiterals(sql) {
|
|
108
|
+
// Walk with the same control flow as the historical stripper for ', ", and $$,
|
|
109
|
+
// then additionally skip backticks and brackets — producing equivalent output
|
|
110
|
+
// for previously-supported forms.
|
|
111
|
+
let out = "";
|
|
112
|
+
let i = 0;
|
|
113
|
+
while (i < sql.length) {
|
|
114
|
+
const ch = sql[i];
|
|
115
|
+
if (ch === "'") {
|
|
116
|
+
i++;
|
|
117
|
+
while (i < sql.length) {
|
|
118
|
+
if (sql[i] === "'" && sql[i + 1] === "'") {
|
|
119
|
+
i += 2;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (sql[i] === "'") {
|
|
123
|
+
i++;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
i++;
|
|
127
|
+
}
|
|
128
|
+
out += "''";
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (ch === '"') {
|
|
132
|
+
i++;
|
|
133
|
+
while (i < sql.length && sql[i] !== '"') {
|
|
134
|
+
if (sql[i] === "\\")
|
|
135
|
+
i++;
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
if (i < sql.length)
|
|
139
|
+
i++;
|
|
140
|
+
out += '""';
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (ch === "`") {
|
|
144
|
+
i++;
|
|
145
|
+
while (i < sql.length && sql[i] !== "`") {
|
|
146
|
+
if (sql[i] === "\\")
|
|
147
|
+
i++;
|
|
148
|
+
i++;
|
|
149
|
+
}
|
|
150
|
+
if (i < sql.length)
|
|
151
|
+
i++;
|
|
152
|
+
out += "``";
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (ch === "[") {
|
|
156
|
+
i++;
|
|
157
|
+
while (i < sql.length && sql[i] !== "]") {
|
|
158
|
+
i++;
|
|
159
|
+
}
|
|
160
|
+
if (i < sql.length)
|
|
161
|
+
i++;
|
|
162
|
+
out += "[]";
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (ch === "$" && /^\$\w*\$/.test(sql.slice(i))) {
|
|
166
|
+
// Match historical indexOf-based closer scan for observable parity.
|
|
167
|
+
const end = sql.indexOf("$", i + 1);
|
|
168
|
+
const tagEnd = sql.indexOf("$", end + 1);
|
|
169
|
+
if (tagEnd === -1)
|
|
170
|
+
return out + sql.slice(i);
|
|
171
|
+
i = tagEnd + 1;
|
|
172
|
+
out += "$$";
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
out += ch;
|
|
176
|
+
i++;
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
const PLACEHOLDER_TOKEN_RE = /:([a-z][a-z0-9_]*)/g;
|
|
181
|
+
/**
|
|
182
|
+
* Find `:name` placeholders only outside quoted regions, in source order.
|
|
183
|
+
* A quoted `':name'` is invisible by construction.
|
|
184
|
+
*/
|
|
185
|
+
export function scanPlaceholders(sql) {
|
|
186
|
+
const spans = tokenizeSqlSpans(sql);
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const span of spans) {
|
|
189
|
+
if (span.kind !== "code")
|
|
190
|
+
continue;
|
|
191
|
+
const segment = sql.slice(span.start, span.end);
|
|
192
|
+
PLACEHOLDER_TOKEN_RE.lastIndex = 0;
|
|
193
|
+
let m;
|
|
194
|
+
while ((m = PLACEHOLDER_TOKEN_RE.exec(segment)) !== null) {
|
|
195
|
+
const name = m[1];
|
|
196
|
+
const start = span.start + m.index;
|
|
197
|
+
const end = start + m[0].length;
|
|
198
|
+
out.push({ name, placeholder: m[0], start, end });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
export function scanTenantPlaceholders(sql) {
|
|
204
|
+
return scanPlaceholders(sql).filter((p) => /^tenant_[a-z0-9_]+_ids$/.test(p.name));
|
|
205
|
+
}
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// List-context validation
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
/**
|
|
210
|
+
* A `many` placeholder must be the sole expression inside `IN (...)` / `NOT IN (...)`,
|
|
211
|
+
* or the sole argument of `= ANY(...)` on array dialects.
|
|
212
|
+
*/
|
|
213
|
+
export function isValidListContext(sql, occurrence, listBinding) {
|
|
214
|
+
const before = sql.slice(0, occurrence.start);
|
|
215
|
+
const after = sql.slice(occurrence.end);
|
|
216
|
+
const inBefore = /(?:NOT\s+)?IN\s*\(\s*$/i.exec(before);
|
|
217
|
+
if (inBefore) {
|
|
218
|
+
return /^\s*\)/.test(after);
|
|
219
|
+
}
|
|
220
|
+
if (listBinding === "array") {
|
|
221
|
+
const anyBefore = /=\s*ANY\s*\(\s*$/i.exec(before);
|
|
222
|
+
if (anyBefore) {
|
|
223
|
+
return /^\s*\)/.test(after);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
/** Detect whether an occurrence sits inside IN ( … ) and return the span of `IN (` … `)`. */
|
|
229
|
+
function inListSpan(sql, occ) {
|
|
230
|
+
const before = sql.slice(0, occ.start);
|
|
231
|
+
const after = sql.slice(occ.end);
|
|
232
|
+
const inBefore = /(?:NOT\s+)?IN\s*\(\s*$/i.exec(before);
|
|
233
|
+
if (!inBefore || !/^\s*\)/.test(after))
|
|
234
|
+
return undefined;
|
|
235
|
+
const closeMatch = /^\s*\)/.exec(after);
|
|
236
|
+
return {
|
|
237
|
+
start: occ.start - inBefore[0].length,
|
|
238
|
+
end: occ.end + closeMatch[0].length,
|
|
239
|
+
isNot: /^NOT\s+/i.test(inBefore[0]),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function anySpan(sql, occ) {
|
|
243
|
+
const before = sql.slice(0, occ.start);
|
|
244
|
+
const after = sql.slice(occ.end);
|
|
245
|
+
const anyBefore = /=\s*ANY\s*\(\s*$/i.exec(before);
|
|
246
|
+
if (!anyBefore || !/^\s*\)/.test(after))
|
|
247
|
+
return undefined;
|
|
248
|
+
const closeMatch = /^\s*\)/.exec(after);
|
|
249
|
+
return {
|
|
250
|
+
start: occ.start - anyBefore[0].length,
|
|
251
|
+
end: occ.end + closeMatch[0].length,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// Dialect-aware literal escaping
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/;
|
|
258
|
+
/**
|
|
259
|
+
* Escape a scalar value as a SQL literal for the given dialect.
|
|
260
|
+
* Never includes the raw parameter value in thrown error messages.
|
|
261
|
+
*/
|
|
262
|
+
export function escapeSqlLiteral(value, dialect) {
|
|
263
|
+
if (typeof value === "boolean") {
|
|
264
|
+
return value ? "TRUE" : "FALSE";
|
|
265
|
+
}
|
|
266
|
+
if (typeof value === "number") {
|
|
267
|
+
if (!Number.isFinite(value)) {
|
|
268
|
+
throw paramError("INVALID_VALUE", "Numeric parameter must be a finite number.");
|
|
269
|
+
}
|
|
270
|
+
return String(value);
|
|
271
|
+
}
|
|
272
|
+
if (CONTROL_CHAR_RE.test(value)) {
|
|
273
|
+
throw paramError("INVALID_VALUE", "String parameter contains a disallowed control character.");
|
|
274
|
+
}
|
|
275
|
+
let escaped = value.replace(/'/g, "''");
|
|
276
|
+
if (dialect?.backslashEscapes === true) {
|
|
277
|
+
escaped = escaped.replace(/\\/g, "\\\\");
|
|
278
|
+
}
|
|
279
|
+
return `'${escaped}'`;
|
|
280
|
+
}
|
|
281
|
+
export function escapeSqlLiteralList(values, dialect) {
|
|
282
|
+
return `(${values.map((v) => escapeSqlLiteral(v, dialect)).join(", ")})`;
|
|
283
|
+
}
|
|
284
|
+
/** Quote-doubling only — used when no dialect is supplied (legacy tenant path). */
|
|
285
|
+
export function escapeSqlLiteralLegacy(value) {
|
|
286
|
+
return "'" + value.replace(/'/g, "''") + "'";
|
|
287
|
+
}
|
|
288
|
+
export function markerStyleForDialect(id) {
|
|
289
|
+
switch (id) {
|
|
290
|
+
case "postgres":
|
|
291
|
+
case "cockroachdb":
|
|
292
|
+
return "dollar";
|
|
293
|
+
case "sqlserver":
|
|
294
|
+
return "atp";
|
|
295
|
+
case "mysql":
|
|
296
|
+
case "mariadb":
|
|
297
|
+
case "sqlite":
|
|
298
|
+
return "question";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export function formatMarker(style, ordinal) {
|
|
302
|
+
switch (style) {
|
|
303
|
+
case "dollar":
|
|
304
|
+
return `$${ordinal}`;
|
|
305
|
+
case "question":
|
|
306
|
+
return "?";
|
|
307
|
+
case "atp":
|
|
308
|
+
return `@p${ordinal}`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function listBindingOf(spec) {
|
|
312
|
+
return spec.listBinding ?? "expand";
|
|
313
|
+
}
|
|
314
|
+
function nextMarker(style, counters) {
|
|
315
|
+
switch (style) {
|
|
316
|
+
case "dollar":
|
|
317
|
+
return formatMarker("dollar", counters.dollar++);
|
|
318
|
+
case "atp":
|
|
319
|
+
return formatMarker("atp", counters.atp++);
|
|
320
|
+
case "question":
|
|
321
|
+
return formatMarker("question", 0);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// bindPreparedQuery
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
function paramError(reason, message) {
|
|
328
|
+
return new QueryParameterError(message, reason);
|
|
329
|
+
}
|
|
330
|
+
function lookupValue(values, name, placeholder) {
|
|
331
|
+
if (Object.prototype.hasOwnProperty.call(values, placeholder)) {
|
|
332
|
+
return values[placeholder];
|
|
333
|
+
}
|
|
334
|
+
if (Object.prototype.hasOwnProperty.call(values, name)) {
|
|
335
|
+
return values[name];
|
|
336
|
+
}
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
function assertValueMatchesDeclaration(value, type, cardinality) {
|
|
340
|
+
if (cardinality === "many") {
|
|
341
|
+
if (!Array.isArray(value)) {
|
|
342
|
+
throw paramError("INVALID_VALUE", "List parameter requires a non-empty array value.");
|
|
343
|
+
}
|
|
344
|
+
if (value.length === 0) {
|
|
345
|
+
throw paramError("INVALID_VALUE", "List parameter requires a non-empty array value.");
|
|
346
|
+
}
|
|
347
|
+
return value.map((v) => assertScalar(v, type));
|
|
348
|
+
}
|
|
349
|
+
if (Array.isArray(value)) {
|
|
350
|
+
throw paramError("INVALID_VALUE", "Scalar parameter must not be an array.");
|
|
351
|
+
}
|
|
352
|
+
return assertScalar(value, type);
|
|
353
|
+
}
|
|
354
|
+
function assertScalar(value, type) {
|
|
355
|
+
if (value === null || value === undefined) {
|
|
356
|
+
throw paramError("INVALID_VALUE", "Parameter value must not be null or undefined.");
|
|
357
|
+
}
|
|
358
|
+
if (typeof value === "object") {
|
|
359
|
+
throw paramError("INVALID_VALUE", "Parameter value must be a string, number, or boolean.");
|
|
360
|
+
}
|
|
361
|
+
switch (type) {
|
|
362
|
+
case "string":
|
|
363
|
+
case "date":
|
|
364
|
+
case "datetime":
|
|
365
|
+
if (typeof value !== "string") {
|
|
366
|
+
throw paramError("INVALID_VALUE", `Parameter of type ${type} requires a string value.`);
|
|
367
|
+
}
|
|
368
|
+
if (type === "date" && !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
369
|
+
throw paramError("INVALID_VALUE", "Date parameter must be an ISO date string (YYYY-MM-DD).");
|
|
370
|
+
}
|
|
371
|
+
if (type === "datetime" &&
|
|
372
|
+
!/^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/.test(value)) {
|
|
373
|
+
throw paramError("INVALID_VALUE", "Datetime parameter must be an ISO datetime string.");
|
|
374
|
+
}
|
|
375
|
+
return value;
|
|
376
|
+
case "number":
|
|
377
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
378
|
+
throw paramError("INVALID_VALUE", "Numeric parameter must be a finite number.");
|
|
379
|
+
}
|
|
380
|
+
return value;
|
|
381
|
+
case "boolean":
|
|
382
|
+
if (typeof value !== "boolean") {
|
|
383
|
+
throw paramError("INVALID_VALUE", "Boolean parameter requires a boolean value.");
|
|
384
|
+
}
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Pure, synchronous binder. Substitutes placeholders with escaped literals and
|
|
390
|
+
* allocates dialect-correct driver markers. Does not authorize tenant IDs.
|
|
391
|
+
*/
|
|
392
|
+
export function bindPreparedQuery(prepared, values) {
|
|
393
|
+
const spec = BUILT_IN_DIALECTS[prepared.dialect];
|
|
394
|
+
if (!spec) {
|
|
395
|
+
throw paramError("DIALECT_UNSUPPORTED", `Unknown dialect '${prepared.dialect}'.`);
|
|
396
|
+
}
|
|
397
|
+
const listBinding = listBindingOf(spec);
|
|
398
|
+
const style = markerStyleForDialect(prepared.dialect);
|
|
399
|
+
const namedSql = prepared.namedSql;
|
|
400
|
+
const occurrences = scanPlaceholders(namedSql);
|
|
401
|
+
const declByName = new Map(prepared.parameters.map((p) => [p.name, p]));
|
|
402
|
+
for (const p of prepared.parameters) {
|
|
403
|
+
if (!occurrences.some((o) => o.name === p.name)) {
|
|
404
|
+
throw paramError("UNRESOLVED_PLACEHOLDER", `Declared parameter '${p.name}' does not appear in namedSql.`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const occ of occurrences) {
|
|
408
|
+
if (!declByName.has(occ.name)) {
|
|
409
|
+
throw paramError("UNRESOLVED_PLACEHOLDER", `Placeholder '${occ.placeholder}' has no matching parameter declaration.`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const resolvedValues = new Map();
|
|
413
|
+
for (const p of prepared.parameters) {
|
|
414
|
+
const raw = lookupValue(values, p.name, p.placeholder);
|
|
415
|
+
if (raw === undefined) {
|
|
416
|
+
throw paramError("MISSING_VALUE", `Missing value for parameter '${p.name}'.`);
|
|
417
|
+
}
|
|
418
|
+
resolvedValues.set(p.name, assertValueMatchesDeclaration(raw, p.type, p.cardinality));
|
|
419
|
+
}
|
|
420
|
+
for (const occ of occurrences) {
|
|
421
|
+
const decl = declByName.get(occ.name);
|
|
422
|
+
if (decl.cardinality === "many" && !isValidListContext(namedSql, occ, listBinding)) {
|
|
423
|
+
throw paramError("INVALID_LIST_CONTEXT", `List parameter '${occ.name}' must appear as the sole expression inside IN (...)` +
|
|
424
|
+
(listBinding === "array" ? " or = ANY(...)" : "") +
|
|
425
|
+
".");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const edits = [];
|
|
429
|
+
const params = [];
|
|
430
|
+
const bindings = [];
|
|
431
|
+
const bindingByName = new Map();
|
|
432
|
+
const counters = { dollar: 1, atp: 0 };
|
|
433
|
+
const ensureBinding = (decl, value) => {
|
|
434
|
+
let binding = bindingByName.get(decl.name);
|
|
435
|
+
if (!binding) {
|
|
436
|
+
binding = {
|
|
437
|
+
name: decl.name,
|
|
438
|
+
placeholder: decl.placeholder,
|
|
439
|
+
type: decl.type,
|
|
440
|
+
cardinality: decl.cardinality,
|
|
441
|
+
description: decl.description,
|
|
442
|
+
value,
|
|
443
|
+
markers: [],
|
|
444
|
+
indices: [],
|
|
445
|
+
source: decl.source,
|
|
446
|
+
};
|
|
447
|
+
bindingByName.set(decl.name, binding);
|
|
448
|
+
bindings.push(binding);
|
|
449
|
+
}
|
|
450
|
+
return binding;
|
|
451
|
+
};
|
|
452
|
+
for (const occ of occurrences) {
|
|
453
|
+
const decl = declByName.get(occ.name);
|
|
454
|
+
const value = resolvedValues.get(occ.name);
|
|
455
|
+
if (decl.cardinality === "many") {
|
|
456
|
+
const list = value;
|
|
457
|
+
const listLiteral = escapeSqlLiteralList(list, spec);
|
|
458
|
+
const binding = ensureBinding(decl, value);
|
|
459
|
+
if (listBinding === "array") {
|
|
460
|
+
// One array-typed driver param. unboundSql uses = ANY($n); bound sql uses IN (...).
|
|
461
|
+
const marker = nextMarker(style, counters);
|
|
462
|
+
const indices = [params.length];
|
|
463
|
+
params.push(list);
|
|
464
|
+
binding.markers.push(marker);
|
|
465
|
+
binding.indices.push(...indices);
|
|
466
|
+
const inSp = inListSpan(namedSql, occ);
|
|
467
|
+
const anySp = anySpan(namedSql, occ);
|
|
468
|
+
if (inSp) {
|
|
469
|
+
edits.push({
|
|
470
|
+
start: inSp.start,
|
|
471
|
+
end: inSp.end,
|
|
472
|
+
literal: `${inSp.isNot ? "NOT " : ""}IN ${listLiteral}`,
|
|
473
|
+
marker: inSp.isNot ? `<> ALL(${marker})` : `= ANY(${marker})`,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
else if (anySp) {
|
|
477
|
+
edits.push({
|
|
478
|
+
start: anySp.start,
|
|
479
|
+
end: anySp.end,
|
|
480
|
+
literal: `IN ${listLiteral}`,
|
|
481
|
+
marker: `= ANY(${marker})`,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
else {
|
|
485
|
+
// Validated above; defensive.
|
|
486
|
+
edits.push({
|
|
487
|
+
start: occ.start,
|
|
488
|
+
end: occ.end,
|
|
489
|
+
literal: listLiteral,
|
|
490
|
+
marker,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
// expand: one marker / literal element per value
|
|
496
|
+
const markerTexts = [];
|
|
497
|
+
for (const item of list) {
|
|
498
|
+
const marker = nextMarker(style, counters);
|
|
499
|
+
markerTexts.push(marker);
|
|
500
|
+
binding.indices.push(params.length);
|
|
501
|
+
binding.markers.push(marker);
|
|
502
|
+
params.push(item);
|
|
503
|
+
}
|
|
504
|
+
const inSp = inListSpan(namedSql, occ);
|
|
505
|
+
if (inSp) {
|
|
506
|
+
// Replace only the placeholder — outer IN ( ) stays; emit bare list.
|
|
507
|
+
edits.push({
|
|
508
|
+
start: occ.start,
|
|
509
|
+
end: occ.end,
|
|
510
|
+
literal: list.map((v) => escapeSqlLiteral(v, spec)).join(", "),
|
|
511
|
+
marker: markerTexts.join(", "),
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
edits.push({
|
|
516
|
+
start: occ.start,
|
|
517
|
+
end: occ.end,
|
|
518
|
+
literal: listLiteral,
|
|
519
|
+
marker: `(${markerTexts.join(", ")})`,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
const scalar = value;
|
|
526
|
+
const literal = escapeSqlLiteral(scalar, spec);
|
|
527
|
+
const marker = nextMarker(style, counters);
|
|
528
|
+
const binding = ensureBinding(decl, value);
|
|
529
|
+
binding.markers.push(marker);
|
|
530
|
+
binding.indices.push(params.length);
|
|
531
|
+
params.push(scalar);
|
|
532
|
+
edits.push({ start: occ.start, end: occ.end, literal, marker });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
edits.sort((a, b) => b.start - a.start);
|
|
536
|
+
let sql = namedSql;
|
|
537
|
+
let unboundSql = namedSql;
|
|
538
|
+
for (const edit of edits) {
|
|
539
|
+
sql = sql.slice(0, edit.start) + edit.literal + sql.slice(edit.end);
|
|
540
|
+
unboundSql = unboundSql.slice(0, edit.start) + edit.marker + unboundSql.slice(edit.end);
|
|
541
|
+
}
|
|
542
|
+
if (scanPlaceholders(sql).length > 0 || scanPlaceholders(unboundSql).length > 0) {
|
|
543
|
+
throw paramError("UNRESOLVED_PLACEHOLDER", "One or more placeholders remain after binding.");
|
|
544
|
+
}
|
|
545
|
+
validateSelectSql(spec, sql);
|
|
546
|
+
validateSelectSql(spec, unboundSql);
|
|
547
|
+
return { sql, unboundSql, params, bindings };
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Compare two SQL strings structurally for the parameterize consistency check.
|
|
551
|
+
*/
|
|
552
|
+
export function sqlStructurallyEqual(a, b) {
|
|
553
|
+
const norm = (s) => s
|
|
554
|
+
.trim()
|
|
555
|
+
.replace(/;\s*$/, "")
|
|
556
|
+
.replace(/\s+/g, " ")
|
|
557
|
+
.toLowerCase();
|
|
558
|
+
return norm(a) === norm(b);
|
|
559
|
+
}
|
|
560
|
+
//# sourceMappingURL=bind.js.map
|