@quereus/quereus 4.19.4 → 4.20.0
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 +257 -257
- package/dist/src/common/errors.d.ts +10 -0
- package/dist/src/common/errors.d.ts.map +1 -1
- package/dist/src/common/errors.js +14 -0
- package/dist/src/common/errors.js.map +1 -1
- package/dist/src/common/types.d.ts +12 -0
- package/dist/src/common/types.d.ts.map +1 -1
- package/dist/src/common/types.js.map +1 -1
- package/dist/src/core/database-events.d.ts.map +1 -1
- package/dist/src/core/database-events.js +12 -9
- package/dist/src/core/database-events.js.map +1 -1
- package/dist/src/core/database-transaction.d.ts.map +1 -1
- package/dist/src/core/database-transaction.js +28 -19
- package/dist/src/core/database-transaction.js.map +1 -1
- package/dist/src/core/database.d.ts +58 -6
- package/dist/src/core/database.d.ts.map +1 -1
- package/dist/src/core/database.js +185 -54
- package/dist/src/core/database.js.map +1 -1
- package/dist/src/core/param.d.ts +46 -1
- package/dist/src/core/param.d.ts.map +1 -1
- package/dist/src/core/param.js +87 -7
- package/dist/src/core/param.js.map +1 -1
- package/dist/src/core/statement.d.ts +66 -1
- package/dist/src/core/statement.d.ts.map +1 -1
- package/dist/src/core/statement.js +156 -91
- package/dist/src/core/statement.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/parser/lexer.d.ts.map +1 -1
- package/dist/src/parser/lexer.js +9 -3
- package/dist/src/parser/lexer.js.map +1 -1
- package/dist/src/planner/analysis/change-scope.d.ts.map +1 -1
- package/dist/src/planner/analysis/change-scope.js +7 -20
- package/dist/src/planner/analysis/change-scope.js.map +1 -1
- package/dist/src/planner/analysis/check-extraction.d.ts +62 -0
- package/dist/src/planner/analysis/check-extraction.d.ts.map +1 -1
- package/dist/src/planner/analysis/check-extraction.js +19 -5
- package/dist/src/planner/analysis/check-extraction.js.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
- package/dist/src/planner/analysis/constraint-extractor.js +7 -0
- package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
- package/dist/src/planner/analysis/scalar-param-usage.d.ts +6 -3
- package/dist/src/planner/analysis/scalar-param-usage.d.ts.map +1 -1
- package/dist/src/planner/analysis/scalar-param-usage.js +32 -4
- package/dist/src/planner/analysis/scalar-param-usage.js.map +1 -1
- package/dist/src/planner/building/coercion.js +1 -1
- package/dist/src/planner/building/coercion.js.map +1 -1
- package/dist/src/planner/building/schema-authored-context.d.ts +7 -7
- package/dist/src/planner/building/schema-authored-context.js +7 -7
- package/dist/src/planner/nodes/scalar.d.ts +19 -1
- package/dist/src/planner/nodes/scalar.d.ts.map +1 -1
- package/dist/src/planner/nodes/scalar.js +13 -2
- package/dist/src/planner/nodes/scalar.js.map +1 -1
- package/dist/src/planner/nodes/set-operation-node.js +4 -0
- package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
- package/dist/src/runtime/emit/add-constraint.js +44 -1
- package/dist/src/runtime/emit/add-constraint.js.map +1 -1
- package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
- package/dist/src/runtime/emit/alter-table.js +20 -32
- package/dist/src/runtime/emit/alter-table.js.map +1 -1
- package/dist/src/runtime/emit/ddl-event-scope.d.ts +20 -13
- package/dist/src/runtime/emit/ddl-event-scope.d.ts.map +1 -1
- package/dist/src/runtime/emit/ddl-event-scope.js +20 -13
- package/dist/src/runtime/emit/ddl-event-scope.js.map +1 -1
- package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
- package/dist/src/runtime/emit/materialized-view-helpers.js +9 -4
- package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
- package/dist/src/runtime/emit/parameter.d.ts.map +1 -1
- package/dist/src/runtime/emit/parameter.js +13 -8
- package/dist/src/runtime/emit/parameter.js.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.d.ts +17 -6
- package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
- package/dist/src/runtime/emit/schema-declarative.js +169 -31
- package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
- package/dist/src/schema/catalog-rendering.d.ts +26 -0
- package/dist/src/schema/catalog-rendering.d.ts.map +1 -1
- package/dist/src/schema/catalog-rendering.js +41 -4
- package/dist/src/schema/catalog-rendering.js.map +1 -1
- package/dist/src/schema/catalog.d.ts +23 -0
- package/dist/src/schema/catalog.d.ts.map +1 -1
- package/dist/src/schema/catalog.js +6 -4
- package/dist/src/schema/catalog.js.map +1 -1
- package/dist/src/schema/constraint-builder.d.ts +67 -11
- package/dist/src/schema/constraint-builder.d.ts.map +1 -1
- package/dist/src/schema/constraint-builder.js +115 -12
- package/dist/src/schema/constraint-builder.js.map +1 -1
- package/dist/src/schema/ddl-generator.d.ts +21 -0
- package/dist/src/schema/ddl-generator.d.ts.map +1 -1
- package/dist/src/schema/ddl-generator.js +7 -1
- package/dist/src/schema/ddl-generator.js.map +1 -1
- package/dist/src/schema/rename/self-qualifier-strip.d.ts +27 -0
- package/dist/src/schema/rename/self-qualifier-strip.d.ts.map +1 -1
- package/dist/src/schema/rename/self-qualifier-strip.js +73 -0
- package/dist/src/schema/rename/self-qualifier-strip.js.map +1 -1
- package/dist/src/schema/rename-rewriter.d.ts +5 -5
- package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
- package/dist/src/schema/rename-rewriter.js +5 -5
- package/dist/src/schema/rename-rewriter.js.map +1 -1
- package/dist/src/schema/schema-differ.d.ts +41 -1
- package/dist/src/schema/schema-differ.d.ts.map +1 -1
- package/dist/src/schema/schema-differ.js +529 -36
- package/dist/src/schema/schema-differ.js.map +1 -1
- package/dist/src/types/temporal-ops.d.ts.map +1 -1
- package/dist/src/types/temporal-ops.js +16 -10
- package/dist/src/types/temporal-ops.js.map +1 -1
- package/dist/src/vtab/capabilities.d.ts +5 -2
- package/dist/src/vtab/capabilities.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/manager.d.ts +15 -11
- package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
- package/dist/src/vtab/memory/layer/manager.js +15 -11
- package/dist/src/vtab/memory/layer/manager.js.map +1 -1
- package/dist/src/vtab/module.d.ts +16 -0
- package/dist/src/vtab/module.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/src/core/param.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ScalarType } from '../common/datatype.js';
|
|
2
|
-
import { type SqlParameters } from '../common/types.js';
|
|
2
|
+
import { type SqlParameters, type SqlValue } from '../common/types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Generate type hints for parameters based on their JavaScript values.
|
|
5
5
|
* This is used during planning to assign strong types to parameters.
|
|
@@ -17,6 +17,51 @@ import { type SqlParameters } from '../common/types.js';
|
|
|
17
17
|
* @returns Map of parameter keys to their inferred ScalarTypes
|
|
18
18
|
*/
|
|
19
19
|
export declare function getParameterTypes(params: SqlParameters | undefined): Map<string | number, ScalarType> | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The parameter a bound-args entry names. A caller may spell a named parameter
|
|
22
|
+
* `:name`, `$name` or the bare `name` — the parser accepts `:` and `$` as
|
|
23
|
+
* interchangeable prefixes and keeps only the bare lexeme — and a positional slot
|
|
24
|
+
* `1`, `'1'`, `':1'` or `':01'`. All of those name one parameter, and this is the
|
|
25
|
+
* single function that says so.
|
|
26
|
+
*
|
|
27
|
+
* `@` is deliberately NOT stripped: the lexer has no `@` token, so `@p` can never
|
|
28
|
+
* appear in Quereus SQL and normalizing it would promise a binding for a parameter
|
|
29
|
+
* no statement can reference.
|
|
30
|
+
*/
|
|
31
|
+
export declare function boundKeyToParamKey(key: string): string | number;
|
|
32
|
+
/**
|
|
33
|
+
* Canonicalizes a caller-supplied parameter object/array into the bound-args record
|
|
34
|
+
* every downstream reader agrees on: one key per parameter ({@link boundKeyToParamKey}),
|
|
35
|
+
* one canonical JS form per value ({@link canonicalizeSqlValue}).
|
|
36
|
+
*
|
|
37
|
+
* This is the ONLY place a bound key is decided. Normalizing at ingress is what makes
|
|
38
|
+
* "which spelling names this parameter?" unanswerable-by-divergence downstream: the
|
|
39
|
+
* runtime value lookup, the parameter type map, bind-time validation and change-scope
|
|
40
|
+
* substitution all read the same key because only one was ever written.
|
|
41
|
+
*
|
|
42
|
+
* Two spellings of one parameter in a single object (`{ p: 1, ':p': 2 }`) is a
|
|
43
|
+
* {@link MisuseError}, not a silent pick: either entry could be the one the caller
|
|
44
|
+
* meant, and discarding a value they passed is worse than refusing it. The rule is
|
|
45
|
+
* per-object — repeated `Statement.bind()` calls to one parameter stay last-wins,
|
|
46
|
+
* since each call is a separate statement of intent.
|
|
47
|
+
*
|
|
48
|
+
* Throws before returning anything, so a caller assigning the result keeps `bindAll`'s
|
|
49
|
+
* all-or-nothing contract for free.
|
|
50
|
+
*
|
|
51
|
+
* @param label the caller-facing operation name, used to prefix rejection messages
|
|
52
|
+
*/
|
|
53
|
+
export declare function normalizeBoundParams(params: SqlParameters | SqlValue[], label: string): Record<string | number, SqlValue>;
|
|
54
|
+
/**
|
|
55
|
+
* An empty bound-args record, and the only shape one should ever start from.
|
|
56
|
+
*
|
|
57
|
+
* Null-prototype on purpose: bound args are looked up BY NAME with keys that come
|
|
58
|
+
* from user SQL, so a plain `{}` answers `:toString`, `:constructor`, `:valueOf`
|
|
59
|
+
* and friends with an inherited function instead of reporting the parameter
|
|
60
|
+
* unbound — `select :toString` returned a JS function as a SQL value rather than
|
|
61
|
+
* raising. Dropping the prototype makes that whole class unrepresentable at the
|
|
62
|
+
* source instead of asking every reader to remember `Object.hasOwn`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function emptyBoundParams(): Record<string | number, SqlValue>;
|
|
20
65
|
/**
|
|
21
66
|
* Normalizes a parameter name to a number when it is an all-digits index,
|
|
22
67
|
* matching how positional params are keyed elsewhere (array index + 1,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"param.d.ts","sourceRoot":"","sources":["../../../src/core/param.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,
|
|
1
|
+
{"version":3,"file":"param.d.ts","sourceRoot":"","sources":["../../../src/core/param.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAyC,KAAK,aAAa,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAK9G;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,GAAG,CAAC,MAAM,GAAG,MAAM,EAAE,UAAU,CAAC,GAAG,SAAS,CAsBjH;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAG/D;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CACnC,MAAM,EAAE,aAAa,GAAG,QAAQ,EAAE,EAClC,KAAK,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,CAAC,CAuBnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,CAAC,CAEpE;AAQD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAK/D"}
|
package/dist/src/core/param.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { describeSqlValueViolation, isSqlValue } from '../common/types.js';
|
|
1
2
|
import { inferLogicalTypeFromValue } from '../common/type-inference.js';
|
|
3
|
+
import { MisuseError } from '../common/errors.js';
|
|
4
|
+
import { canonicalizeSqlValue } from '../util/numeric-canonical.js';
|
|
2
5
|
/**
|
|
3
6
|
* Generate type hints for parameters based on their JavaScript values.
|
|
4
7
|
* This is used during planning to assign strong types to parameters.
|
|
@@ -28,18 +31,95 @@ export function getParameterTypes(params) {
|
|
|
28
31
|
}
|
|
29
32
|
else {
|
|
30
33
|
Object.entries(params).forEach(([key, value]) => {
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
results.set(normalizeParamKey(name), getParameterScalarType(value));
|
|
34
|
+
// ParameterScope keys hints by the bare name, or by the 1-based index for a
|
|
35
|
+
// positional slot — which arrives here stringified (`boundArgs[index + 1]`)
|
|
36
|
+
// and has to become a number again or the hint lookup silently misses.
|
|
37
|
+
// Bound args are already normalized and boundKeyToParamKey is idempotent on
|
|
38
|
+
// them; running it anyway keeps this correct for a caller passing a raw object.
|
|
39
|
+
results.set(boundKeyToParamKey(key), getParameterScalarType(value));
|
|
38
40
|
});
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
return results;
|
|
42
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The parameter a bound-args entry names. A caller may spell a named parameter
|
|
47
|
+
* `:name`, `$name` or the bare `name` — the parser accepts `:` and `$` as
|
|
48
|
+
* interchangeable prefixes and keeps only the bare lexeme — and a positional slot
|
|
49
|
+
* `1`, `'1'`, `':1'` or `':01'`. All of those name one parameter, and this is the
|
|
50
|
+
* single function that says so.
|
|
51
|
+
*
|
|
52
|
+
* `@` is deliberately NOT stripped: the lexer has no `@` token, so `@p` can never
|
|
53
|
+
* appear in Quereus SQL and normalizing it would promise a binding for a parameter
|
|
54
|
+
* no statement can reference.
|
|
55
|
+
*/
|
|
56
|
+
export function boundKeyToParamKey(key) {
|
|
57
|
+
const bare = key.startsWith(':') || key.startsWith('$') ? key.substring(1) : key;
|
|
58
|
+
return normalizeParamKey(bare);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Canonicalizes a caller-supplied parameter object/array into the bound-args record
|
|
62
|
+
* every downstream reader agrees on: one key per parameter ({@link boundKeyToParamKey}),
|
|
63
|
+
* one canonical JS form per value ({@link canonicalizeSqlValue}).
|
|
64
|
+
*
|
|
65
|
+
* This is the ONLY place a bound key is decided. Normalizing at ingress is what makes
|
|
66
|
+
* "which spelling names this parameter?" unanswerable-by-divergence downstream: the
|
|
67
|
+
* runtime value lookup, the parameter type map, bind-time validation and change-scope
|
|
68
|
+
* substitution all read the same key because only one was ever written.
|
|
69
|
+
*
|
|
70
|
+
* Two spellings of one parameter in a single object (`{ p: 1, ':p': 2 }`) is a
|
|
71
|
+
* {@link MisuseError}, not a silent pick: either entry could be the one the caller
|
|
72
|
+
* meant, and discarding a value they passed is worse than refusing it. The rule is
|
|
73
|
+
* per-object — repeated `Statement.bind()` calls to one parameter stay last-wins,
|
|
74
|
+
* since each call is a separate statement of intent.
|
|
75
|
+
*
|
|
76
|
+
* Throws before returning anything, so a caller assigning the result keeps `bindAll`'s
|
|
77
|
+
* all-or-nothing contract for free.
|
|
78
|
+
*
|
|
79
|
+
* @param label the caller-facing operation name, used to prefix rejection messages
|
|
80
|
+
*/
|
|
81
|
+
export function normalizeBoundParams(params, label) {
|
|
82
|
+
const out = emptyBoundParams();
|
|
83
|
+
if (Array.isArray(params)) {
|
|
84
|
+
params.forEach((value, index) => {
|
|
85
|
+
assertSqlValue(value, `${label}: invalid value at index ${index}`);
|
|
86
|
+
out[index + 1] = canonicalizeSqlValue(value);
|
|
87
|
+
});
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
// Rejection messages name the CALLER's spelling, so validate while iterating the
|
|
91
|
+
// original entries rather than the normalized record.
|
|
92
|
+
const spellingOf = new Map();
|
|
93
|
+
for (const [key, value] of Object.entries(params)) {
|
|
94
|
+
assertSqlValue(value, `${label}: invalid value for key '${key}'`);
|
|
95
|
+
const paramKey = boundKeyToParamKey(key);
|
|
96
|
+
const previous = spellingOf.get(paramKey);
|
|
97
|
+
if (previous !== undefined) {
|
|
98
|
+
throw new MisuseError(`${label}: parameter '${paramKey}' bound twice, as '${previous}' and '${key}'`);
|
|
99
|
+
}
|
|
100
|
+
spellingOf.set(paramKey, key);
|
|
101
|
+
out[paramKey] = canonicalizeSqlValue(value);
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* An empty bound-args record, and the only shape one should ever start from.
|
|
107
|
+
*
|
|
108
|
+
* Null-prototype on purpose: bound args are looked up BY NAME with keys that come
|
|
109
|
+
* from user SQL, so a plain `{}` answers `:toString`, `:constructor`, `:valueOf`
|
|
110
|
+
* and friends with an inherited function instead of reporting the parameter
|
|
111
|
+
* unbound — `select :toString` returned a JS function as a SQL value rather than
|
|
112
|
+
* raising. Dropping the prototype makes that whole class unrepresentable at the
|
|
113
|
+
* source instead of asking every reader to remember `Object.hasOwn`.
|
|
114
|
+
*/
|
|
115
|
+
export function emptyBoundParams() {
|
|
116
|
+
return Object.create(null);
|
|
117
|
+
}
|
|
118
|
+
function assertSqlValue(value, context) {
|
|
119
|
+
if (!isSqlValue(value)) {
|
|
120
|
+
throw new MisuseError(`${context}: expected SqlValue, got ${describeSqlValueViolation(value)}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
43
123
|
/**
|
|
44
124
|
* Normalizes a parameter name to a number when it is an all-digits index,
|
|
45
125
|
* matching how positional params are keyed elsewhere (array index + 1,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"param.js","sourceRoot":"","sources":["../../../src/core/param.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"param.js","sourceRoot":"","sources":["../../../src/core/param.ts"],"names":[],"mappings":"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"}
|
|
@@ -37,8 +37,23 @@ export declare class Statement {
|
|
|
37
37
|
private needsCompile;
|
|
38
38
|
private columnDefCache;
|
|
39
39
|
private schemaChangeUnsubscriber;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Types of the parameters that have one: the explicit hints handed to `prepare()`,
|
|
42
|
+
* or — when the caller supplied values instead — the type each parameter's FIRST
|
|
43
|
+
* bound value implied. Undefined while no parameter has a type at all; see
|
|
44
|
+
* {@link establishParameterTypes} for why that is not the same as an empty map.
|
|
45
|
+
*/
|
|
41
46
|
private parameterTypes;
|
|
47
|
+
/**
|
|
48
|
+
* True when {@link parameterTypes} came from an explicit hint map handed to
|
|
49
|
+
* `prepare()`. Such a map is CLOSED: a parameter it does not name stays ANY
|
|
50
|
+
* however it is later bound, and nothing is validated against it. That is a
|
|
51
|
+
* deliberate surface, not an oversight — `InternalStatementCache` prepares its FK
|
|
52
|
+
* probes with an EMPTY map precisely to get an affinity-neutral plan and no
|
|
53
|
+
* bind-time validation, so one cached statement can be rebound to an integer key
|
|
54
|
+
* on one row and a text key on the next.
|
|
55
|
+
*/
|
|
56
|
+
private readonly parameterTypesDeclared;
|
|
42
57
|
/**
|
|
43
58
|
* Parameter names/indices used directly as a comparand in a scalar comparison
|
|
44
59
|
* (`= <> < <= > >=` / `IN` / `BETWEEN`) against a non-object scalar operand.
|
|
@@ -51,6 +66,56 @@ export declare class Statement {
|
|
|
51
66
|
nextStatement(): boolean;
|
|
52
67
|
/** Returns the SQL fragment for the current statement, if available. */
|
|
53
68
|
getBlockSql(): string;
|
|
69
|
+
/**
|
|
70
|
+
* Drops the compiled plan and everything derived from it, so the next
|
|
71
|
+
* {@link compile} rebuilds. THE one invalidation set — every caller that
|
|
72
|
+
* invalidates (schema change, statement advance, first bind onto a provisional
|
|
73
|
+
* plan) goes through here so none can forget a member and leave, say, a stale
|
|
74
|
+
* scheduler emitted from a discarded plan.
|
|
75
|
+
*/
|
|
76
|
+
private invalidatePlan;
|
|
77
|
+
/**
|
|
78
|
+
* The parameter types to plan with, covering every parameter that has a bound
|
|
79
|
+
* value by the time this runs.
|
|
80
|
+
*
|
|
81
|
+
* Each parameter's type is established ONCE — by the first value ever bound to it
|
|
82
|
+
* — and is never re-inferred afterwards. That frozen contract is what
|
|
83
|
+
* {@link validateParameterTypes} checks and what lets repeated executions reuse one
|
|
84
|
+
* plan. Extending the map is not re-inferring: a parameter with no type yet has
|
|
85
|
+
* never been planned against anything, so taking its first value's type contradicts
|
|
86
|
+
* nothing already announced.
|
|
87
|
+
*
|
|
88
|
+
* Explicit hints short-circuit all of this — see {@link parameterTypesDeclared}.
|
|
89
|
+
*
|
|
90
|
+
* Returns undefined while NO parameter has a type. That state must not be recorded
|
|
91
|
+
* as an established-but-empty map: an empty map is indistinguishable from "typed,
|
|
92
|
+
* and there are none", and recording it would pin every parameter at ANY even
|
|
93
|
+
* after values arrive.
|
|
94
|
+
*/
|
|
95
|
+
private establishParameterTypes;
|
|
96
|
+
/** True when some bound value belongs to a parameter that has no type yet. */
|
|
97
|
+
private hasUntypedBinding;
|
|
98
|
+
/**
|
|
99
|
+
* Called after every bind. A cached plan announces the parameter types that were
|
|
100
|
+
* established when it was built, so a bind that gives a value to a parameter that
|
|
101
|
+
* plan had NO type for — one compiled before any bind, or compiled while only some
|
|
102
|
+
* of the parameters were bound — no longer describes the query that will run.
|
|
103
|
+
* Discard it; the next compile plans against the newly available types.
|
|
104
|
+
*
|
|
105
|
+
* Costs at most one extra compile per parameter first bound after a compile, and
|
|
106
|
+
* none at all for a statement that binds before it plans.
|
|
107
|
+
*
|
|
108
|
+
* NOTE: `all(params)` / `get(params)` on a statement prepared WITHOUT values read
|
|
109
|
+
* `getColumnNames()` (which compiles) before handing `params` down, so they pay
|
|
110
|
+
* that extra compile on their first execution; `readConcurrency: 'committed'` adds
|
|
111
|
+
* {@link tryRouteConcurrent}'s compile ahead of it. Harmless: a result column's
|
|
112
|
+
* name and the column count come from the projection, and committed-read
|
|
113
|
+
* eligibility from the node kinds in the tree, so neither reads parameter types.
|
|
114
|
+
* If the extra compile ever shows up in a profile, bind at those call sites before
|
|
115
|
+
* reading names and pass `undefined` downstream, as {@link Database.get} already
|
|
116
|
+
* does.
|
|
117
|
+
*/
|
|
118
|
+
private dropStalePlanOnBind;
|
|
54
119
|
/**
|
|
55
120
|
* Binds a user-provided argument value to a declared parameter name/index for the current statement.
|
|
56
121
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statement.d.ts","sourceRoot":"","sources":["../../../src/core/statement.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,QAAQ,EAAc,KAAK,GAAG,EAAE,KAAK,aAAa,EAAE,KAAK,gBAAgB,EAAE,KAAK,YAAY,EAAyC,MAAM,oBAAoB,CAAC;AAE9K,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAkB,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExF,OAAO,KAAK,EAAE,SAAS,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIlE,OAAO,KAAK,EAAE,iBAAiB,EAAkB,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAA0C,KAAK,SAAS,EAAE,KAAK,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAgB/H,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAgB3F;;GAEG;AACH,qBAAa,SAAS;IACrB,SAAgB,EAAE,EAAE,QAAQ,CAAC;IAC7B,SAAgB,WAAW,EAAE,MAAM,CAAC;IACpC,SAAgB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzC,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"statement.d.ts","sourceRoot":"","sources":["../../../src/core/statement.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@ import { Cached } from '../util/cached.js';
|
|
|
13
13
|
import { isAsyncIterable, disconnectVTable } from '../runtime/utils.js';
|
|
14
14
|
import { generateInstructionProgram, serializePlanTree } from '../planner/debug.js';
|
|
15
15
|
import { EmissionContext } from '../runtime/emission-context.js';
|
|
16
|
-
import { getParameterTypes } from './param.js';
|
|
16
|
+
import { boundKeyToParamKey, emptyBoundParams, getParameterTypes, normalizeBoundParams } from './param.js';
|
|
17
17
|
import { rowToObject } from './utils.js';
|
|
18
18
|
import { getPhysicalType, physicalTypeName, PhysicalType } from '../types/logical-type.js';
|
|
19
19
|
import { wrapAsyncIterator } from '../util/async-iterator.js';
|
|
@@ -42,7 +42,7 @@ export class Statement {
|
|
|
42
42
|
astBatchIndex = -1;
|
|
43
43
|
finalized = false;
|
|
44
44
|
busy = false;
|
|
45
|
-
boundArgs =
|
|
45
|
+
boundArgs = emptyBoundParams();
|
|
46
46
|
plan = null;
|
|
47
47
|
emissionContext = null;
|
|
48
48
|
/**
|
|
@@ -64,8 +64,23 @@ export class Statement {
|
|
|
64
64
|
needsCompile = true;
|
|
65
65
|
columnDefCache = new Cached(() => this.getColumnDefs());
|
|
66
66
|
schemaChangeUnsubscriber = null;
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Types of the parameters that have one: the explicit hints handed to `prepare()`,
|
|
69
|
+
* or — when the caller supplied values instead — the type each parameter's FIRST
|
|
70
|
+
* bound value implied. Undefined while no parameter has a type at all; see
|
|
71
|
+
* {@link establishParameterTypes} for why that is not the same as an empty map.
|
|
72
|
+
*/
|
|
68
73
|
parameterTypes = undefined;
|
|
74
|
+
/**
|
|
75
|
+
* True when {@link parameterTypes} came from an explicit hint map handed to
|
|
76
|
+
* `prepare()`. Such a map is CLOSED: a parameter it does not name stays ANY
|
|
77
|
+
* however it is later bound, and nothing is validated against it. That is a
|
|
78
|
+
* deliberate surface, not an oversight — `InternalStatementCache` prepares its FK
|
|
79
|
+
* probes with an EMPTY map precisely to get an affinity-neutral plan and no
|
|
80
|
+
* bind-time validation, so one cached statement can be rebound to an integer key
|
|
81
|
+
* on one row and a text key on the next.
|
|
82
|
+
*/
|
|
83
|
+
parameterTypesDeclared;
|
|
69
84
|
/**
|
|
70
85
|
* Parameter names/indices used directly as a comparand in a scalar comparison
|
|
71
86
|
* (`= <> < <= > >=` / `IN` / `BETWEEN`) against a non-object scalar operand.
|
|
@@ -113,28 +128,25 @@ export class Statement {
|
|
|
113
128
|
this.originalSql = this.astBatch.map(s => astToString(s)).join('; ');
|
|
114
129
|
}
|
|
115
130
|
// Handle explicit parameter types or initial values
|
|
131
|
+
this.parameterTypesDeclared = paramsOrTypes instanceof Map;
|
|
116
132
|
if (paramsOrTypes instanceof Map) {
|
|
117
|
-
// Explicit parameter types provided
|
|
133
|
+
// Explicit parameter types provided — taken as given, never extended.
|
|
118
134
|
this.parameterTypes = paramsOrTypes;
|
|
119
135
|
}
|
|
120
136
|
else if (paramsOrTypes !== undefined) {
|
|
121
|
-
// Initial parameter values
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
for (const [key, value] of Object.entries(paramsOrTypes)) {
|
|
135
|
-
this.boundArgs[key] = canonicalizeSqlValue(value);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
137
|
+
// Initial parameter values. Keys collapse onto one spelling per parameter and
|
|
138
|
+
// values canonicalize (a safe-range bigint narrows to number, R1 —
|
|
139
|
+
// util/numeric-canonical.ts) as they enter boundArgs: per-bind, not per-row,
|
|
140
|
+
// and shared by every ingress site (here, bind, bindAll, Database.exec,
|
|
141
|
+
// change-scope's bindParameters).
|
|
142
|
+
this.boundArgs = normalizeBoundParams(paramsOrTypes, 'prepare');
|
|
143
|
+
// Types follow from those values NOW, not at the first compile: prepare-time
|
|
144
|
+
// values are the declaration, so `prepare(sql, [1, 42]).run([2, 3.14])` must
|
|
145
|
+
// be a type mismatch and not a re-inference. Routed through the shared helper
|
|
146
|
+
// so the "no values is not an empty type map" rule holds here too — inferring
|
|
147
|
+
// inline is what let `prepare(sql, [])` freeze an established-but-EMPTY map
|
|
148
|
+
// and pin every later-bound parameter at ANY.
|
|
149
|
+
this.establishParameterTypes();
|
|
138
150
|
}
|
|
139
151
|
if (this.astBatch.length === 0 && initialAstIndex === 0) {
|
|
140
152
|
// No statements to run, effectively. nextStatement will return false.
|
|
@@ -156,12 +168,11 @@ export class Statement {
|
|
|
156
168
|
throw new MisuseError("Statement busy, reset or complete current iteration first.");
|
|
157
169
|
if (this.astBatchIndex < this.astBatch.length - 1) {
|
|
158
170
|
this.astBatchIndex++;
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
this.parameterTypes = undefined;
|
|
171
|
+
this.invalidatePlan();
|
|
172
|
+
// Inferred types described the statement just left behind; a caller's explicit
|
|
173
|
+
// hint map was given for the whole prepared text and stays in force.
|
|
174
|
+
if (!this.parameterTypesDeclared)
|
|
175
|
+
this.parameterTypes = undefined;
|
|
165
176
|
return true;
|
|
166
177
|
}
|
|
167
178
|
else {
|
|
@@ -175,6 +186,91 @@ export class Statement {
|
|
|
175
186
|
}
|
|
176
187
|
return astToString(this.getAstStatement());
|
|
177
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Drops the compiled plan and everything derived from it, so the next
|
|
191
|
+
* {@link compile} rebuilds. THE one invalidation set — every caller that
|
|
192
|
+
* invalidates (schema change, statement advance, first bind onto a provisional
|
|
193
|
+
* plan) goes through here so none can forget a member and leave, say, a stale
|
|
194
|
+
* scheduler emitted from a discarded plan.
|
|
195
|
+
*/
|
|
196
|
+
invalidatePlan() {
|
|
197
|
+
this.needsCompile = true;
|
|
198
|
+
this.plan = null;
|
|
199
|
+
this.emissionContext = null;
|
|
200
|
+
this.scheduler = null;
|
|
201
|
+
this.columnDefCache.clear();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The parameter types to plan with, covering every parameter that has a bound
|
|
205
|
+
* value by the time this runs.
|
|
206
|
+
*
|
|
207
|
+
* Each parameter's type is established ONCE — by the first value ever bound to it
|
|
208
|
+
* — and is never re-inferred afterwards. That frozen contract is what
|
|
209
|
+
* {@link validateParameterTypes} checks and what lets repeated executions reuse one
|
|
210
|
+
* plan. Extending the map is not re-inferring: a parameter with no type yet has
|
|
211
|
+
* never been planned against anything, so taking its first value's type contradicts
|
|
212
|
+
* nothing already announced.
|
|
213
|
+
*
|
|
214
|
+
* Explicit hints short-circuit all of this — see {@link parameterTypesDeclared}.
|
|
215
|
+
*
|
|
216
|
+
* Returns undefined while NO parameter has a type. That state must not be recorded
|
|
217
|
+
* as an established-but-empty map: an empty map is indistinguishable from "typed,
|
|
218
|
+
* and there are none", and recording it would pin every parameter at ANY even
|
|
219
|
+
* after values arrive.
|
|
220
|
+
*/
|
|
221
|
+
establishParameterTypes() {
|
|
222
|
+
if (this.parameterTypesDeclared)
|
|
223
|
+
return this.parameterTypes;
|
|
224
|
+
const inferred = getParameterTypes(this.boundArgs);
|
|
225
|
+
if (!inferred || inferred.size === 0)
|
|
226
|
+
return this.parameterTypes;
|
|
227
|
+
if (this.parameterTypes === undefined) {
|
|
228
|
+
this.parameterTypes = inferred;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
for (const [key, type] of inferred) {
|
|
232
|
+
if (!this.parameterTypes.has(key))
|
|
233
|
+
this.parameterTypes.set(key, type);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return this.parameterTypes;
|
|
237
|
+
}
|
|
238
|
+
/** True when some bound value belongs to a parameter that has no type yet. */
|
|
239
|
+
hasUntypedBinding() {
|
|
240
|
+
for (const key of Object.keys(this.boundArgs)) {
|
|
241
|
+
if (!this.parameterTypes?.has(boundKeyToParamKey(key)))
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Called after every bind. A cached plan announces the parameter types that were
|
|
248
|
+
* established when it was built, so a bind that gives a value to a parameter that
|
|
249
|
+
* plan had NO type for — one compiled before any bind, or compiled while only some
|
|
250
|
+
* of the parameters were bound — no longer describes the query that will run.
|
|
251
|
+
* Discard it; the next compile plans against the newly available types.
|
|
252
|
+
*
|
|
253
|
+
* Costs at most one extra compile per parameter first bound after a compile, and
|
|
254
|
+
* none at all for a statement that binds before it plans.
|
|
255
|
+
*
|
|
256
|
+
* NOTE: `all(params)` / `get(params)` on a statement prepared WITHOUT values read
|
|
257
|
+
* `getColumnNames()` (which compiles) before handing `params` down, so they pay
|
|
258
|
+
* that extra compile on their first execution; `readConcurrency: 'committed'` adds
|
|
259
|
+
* {@link tryRouteConcurrent}'s compile ahead of it. Harmless: a result column's
|
|
260
|
+
* name and the column count come from the projection, and committed-read
|
|
261
|
+
* eligibility from the node kinds in the tree, so neither reads parameter types.
|
|
262
|
+
* If the extra compile ever shows up in a profile, bind at those call sites before
|
|
263
|
+
* reading names and pass `undefined` downstream, as {@link Database.get} already
|
|
264
|
+
* does.
|
|
265
|
+
*/
|
|
266
|
+
dropStalePlanOnBind() {
|
|
267
|
+
// A declared hint map never grows, so no bind can make the cached plan stale.
|
|
268
|
+
if (!this.plan || this.parameterTypesDeclared)
|
|
269
|
+
return;
|
|
270
|
+
if (!this.hasUntypedBinding())
|
|
271
|
+
return;
|
|
272
|
+
this.invalidatePlan();
|
|
273
|
+
}
|
|
178
274
|
/** @internal Plans the current AST statement */
|
|
179
275
|
compile() {
|
|
180
276
|
if (this.plan && !this.needsCompile)
|
|
@@ -185,23 +281,16 @@ export class Statement {
|
|
|
185
281
|
let plan;
|
|
186
282
|
try {
|
|
187
283
|
const currentAst = this.getAstStatement();
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
// every execution and defeat validateParameterTypes' frozen-type check. Revisit
|
|
197
|
-
// (invalidate the plan on a type-changing bind) if pre-bind introspection ever
|
|
198
|
-
// needs to agree with the executed plan.
|
|
199
|
-
if (this.parameterTypes === undefined) {
|
|
200
|
-
// Infer types from current bound args
|
|
201
|
-
this.parameterTypes = getParameterTypes(this.boundArgs);
|
|
202
|
-
}
|
|
284
|
+
// Types come from the values bound so far, never from emptiness: compiling
|
|
285
|
+
// with nothing bound leaves them undefined rather than freezing an empty (but
|
|
286
|
+
// established) map, which would pin every parameter at ANY for the statement's
|
|
287
|
+
// whole life. A parameter that already has a type keeps it — that frozen
|
|
288
|
+
// contract is what validateParameterTypes checks, and what lets repeated
|
|
289
|
+
// executions reuse one plan — while one bound for the FIRST time after this
|
|
290
|
+
// plan was built has already invalidated it (see dropStalePlanOnBind).
|
|
291
|
+
const parameterTypes = this.establishParameterTypes();
|
|
203
292
|
// Pass parameter types directly to planning
|
|
204
|
-
const { plan: rawPlan, schemaDependencies: dependencies } = this.db._buildPlan([currentAst],
|
|
293
|
+
const { plan: rawPlan, schemaDependencies: dependencies } = this.db._buildPlan([currentAst], parameterTypes, this._schemaPathOverride);
|
|
205
294
|
// Collect array-valued-scalar-param guard targets from the LOGICAL plan,
|
|
206
295
|
// before the access-path optimizer folds `col = ?` comparisons into index
|
|
207
296
|
// seeks (which erases the comparison node). See validateParameterTypes.
|
|
@@ -255,11 +344,7 @@ export class Statement {
|
|
|
255
344
|
(!dep.schemaName || dep.schemaName === event.schemaName));
|
|
256
345
|
if (affectedDependency) {
|
|
257
346
|
log('Schema change invalidated plan for statement: %s %s', event.type, event.objectName);
|
|
258
|
-
this.
|
|
259
|
-
this.plan = null;
|
|
260
|
-
this.emissionContext = null;
|
|
261
|
-
this.scheduler = null;
|
|
262
|
-
this.columnDefCache.clear();
|
|
347
|
+
this.invalidatePlan();
|
|
263
348
|
}
|
|
264
349
|
});
|
|
265
350
|
}
|
|
@@ -296,17 +381,18 @@ export class Statement {
|
|
|
296
381
|
if (!isSqlValue(value)) {
|
|
297
382
|
throw new MisuseError(`bind: invalid value for key '${key}': expected SqlValue, got ${describeSqlValueViolation(value)}`);
|
|
298
383
|
}
|
|
299
|
-
if (typeof key
|
|
300
|
-
if (key < 1)
|
|
301
|
-
throw new RangeError(`Argument index ${key} out of range (must be >= 1)`);
|
|
302
|
-
this.boundArgs[key] = canonicalizeSqlValue(value);
|
|
303
|
-
}
|
|
304
|
-
else if (typeof key === 'string') {
|
|
305
|
-
this.boundArgs[key] = canonicalizeSqlValue(value);
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
384
|
+
if (typeof key !== 'number' && typeof key !== 'string') {
|
|
308
385
|
throw new MisuseError("Invalid argument key type");
|
|
309
386
|
}
|
|
387
|
+
// One spelling per parameter, as at every other ingress — ':p'/'$p'/'p' and
|
|
388
|
+
// '1'/':1'/':01' each collapse to one key. The index range check runs AFTER
|
|
389
|
+
// normalization so bind(':0') is rejected exactly as bind(0) is.
|
|
390
|
+
const paramKey = typeof key === 'number' ? key : boundKeyToParamKey(key);
|
|
391
|
+
if (typeof paramKey === 'number' && paramKey < 1) {
|
|
392
|
+
throw new RangeError(`Argument index ${key} out of range (must be >= 1)`);
|
|
393
|
+
}
|
|
394
|
+
this.boundArgs[paramKey] = canonicalizeSqlValue(value);
|
|
395
|
+
this.dropStalePlanOnBind();
|
|
310
396
|
return this;
|
|
311
397
|
}
|
|
312
398
|
/**
|
|
@@ -316,31 +402,13 @@ export class Statement {
|
|
|
316
402
|
this.validateStatement("bind all parameters for");
|
|
317
403
|
if (this.busy)
|
|
318
404
|
throw new MisuseError("Statement busy, reset first");
|
|
319
|
-
|
|
320
|
-
if (Array.isArray(args)) {
|
|
321
|
-
// Convert array to object with 1-based numeric keys to match bind() and constructor
|
|
322
|
-
args.forEach((value, index) => {
|
|
323
|
-
if (!isSqlValue(value)) {
|
|
324
|
-
throw new MisuseError(`bindAll: invalid value at index ${index}: expected SqlValue, got ${describeSqlValueViolation(value)}`);
|
|
325
|
-
}
|
|
326
|
-
this.boundArgs[index + 1] = canonicalizeSqlValue(value);
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
else if (typeof args === 'object' && args !== null) {
|
|
330
|
-
// Validate every entry before assigning any, so a rejected value leaves
|
|
331
|
-
// boundArgs empty rather than partially bound.
|
|
332
|
-
for (const [key, value] of Object.entries(args)) {
|
|
333
|
-
if (!isSqlValue(value)) {
|
|
334
|
-
throw new MisuseError(`bindAll: invalid value for key '${key}': expected SqlValue, got ${describeSqlValueViolation(value)}`);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
for (const [key, value] of Object.entries(args)) {
|
|
338
|
-
this.boundArgs[key] = canonicalizeSqlValue(value);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
405
|
+
if (!Array.isArray(args) && (typeof args !== 'object' || args === null)) {
|
|
342
406
|
throw new MisuseError("Invalid parameters type for bindAll. Use array or object.");
|
|
343
407
|
}
|
|
408
|
+
// Normalize first, assign second: a rejected object (bad value, or one parameter
|
|
409
|
+
// bound under two spellings) leaves the previous bindings untouched.
|
|
410
|
+
this.boundArgs = normalizeBoundParams(args, 'bindAll');
|
|
411
|
+
this.dropStalePlanOnBind();
|
|
344
412
|
return this;
|
|
345
413
|
}
|
|
346
414
|
/** Checks if the current statement, when executed, is expected to produce rows. */
|
|
@@ -673,7 +741,7 @@ export class Statement {
|
|
|
673
741
|
this.validateStatement("clear bindings for");
|
|
674
742
|
if (this.busy)
|
|
675
743
|
throw new MisuseError("Statement busy, reset first");
|
|
676
|
-
this.boundArgs =
|
|
744
|
+
this.boundArgs = emptyBoundParams();
|
|
677
745
|
// Don't set needsCompile - parameter types are preserved
|
|
678
746
|
return this;
|
|
679
747
|
}
|
|
@@ -685,7 +753,7 @@ export class Statement {
|
|
|
685
753
|
return;
|
|
686
754
|
this.finalized = true;
|
|
687
755
|
this.busy = false;
|
|
688
|
-
this.boundArgs =
|
|
756
|
+
this.boundArgs = emptyBoundParams();
|
|
689
757
|
this.plan = null;
|
|
690
758
|
this.emissionContext = null;
|
|
691
759
|
this.scheduler = null;
|
|
@@ -907,11 +975,11 @@ export class Statement {
|
|
|
907
975
|
// silently return no rows. The set is collected structurally at plan time
|
|
908
976
|
// (JSON-vs-JSON comparisons are excluded), so this never over-fires.
|
|
909
977
|
for (const key of this.scalarRequiredParams) {
|
|
910
|
-
//
|
|
911
|
-
//
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
978
|
+
// `scalarRequiredParams` keys are plan-side (already bare / numeric) and
|
|
979
|
+
// boundArgs is normalized to the same keys at ingress, so one lookup finds
|
|
980
|
+
// the binding whatever spelling the caller used. `undefined` still means
|
|
981
|
+
// "not bound" — a parameter bound to `null` reads as `null` and is checked.
|
|
982
|
+
const value = this.boundArgs[key];
|
|
915
983
|
if (value !== undefined && isObjectClassValue(value)) {
|
|
916
984
|
throw new QuereusError(`parameter ${typeof key === 'number' ? `?${key}` : `:${key}`} ` +
|
|
917
985
|
`bound to an array/object value but used in a scalar comparison`, StatusCode.MISMATCH);
|
|
@@ -979,10 +1047,7 @@ export class Statement {
|
|
|
979
1047
|
*/
|
|
980
1048
|
getAnalysisPlan() {
|
|
981
1049
|
const currentAst = this.getAstStatement();
|
|
982
|
-
|
|
983
|
-
this.parameterTypes = getParameterTypes(this.boundArgs);
|
|
984
|
-
}
|
|
985
|
-
const { plan: rawPlan } = this.db._buildPlan([currentAst], this.parameterTypes, this._schemaPathOverride);
|
|
1050
|
+
const { plan: rawPlan } = this.db._buildPlan([currentAst], this.establishParameterTypes(), this._schemaPathOverride);
|
|
986
1051
|
return this.db.optimizer.optimizeForAnalysis(rawPlan, this.db);
|
|
987
1052
|
}
|
|
988
1053
|
/**
|