@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.
Files changed (116) hide show
  1. package/README.md +257 -257
  2. package/dist/src/common/errors.d.ts +10 -0
  3. package/dist/src/common/errors.d.ts.map +1 -1
  4. package/dist/src/common/errors.js +14 -0
  5. package/dist/src/common/errors.js.map +1 -1
  6. package/dist/src/common/types.d.ts +12 -0
  7. package/dist/src/common/types.d.ts.map +1 -1
  8. package/dist/src/common/types.js.map +1 -1
  9. package/dist/src/core/database-events.d.ts.map +1 -1
  10. package/dist/src/core/database-events.js +12 -9
  11. package/dist/src/core/database-events.js.map +1 -1
  12. package/dist/src/core/database-transaction.d.ts.map +1 -1
  13. package/dist/src/core/database-transaction.js +28 -19
  14. package/dist/src/core/database-transaction.js.map +1 -1
  15. package/dist/src/core/database.d.ts +58 -6
  16. package/dist/src/core/database.d.ts.map +1 -1
  17. package/dist/src/core/database.js +185 -54
  18. package/dist/src/core/database.js.map +1 -1
  19. package/dist/src/core/param.d.ts +46 -1
  20. package/dist/src/core/param.d.ts.map +1 -1
  21. package/dist/src/core/param.js +87 -7
  22. package/dist/src/core/param.js.map +1 -1
  23. package/dist/src/core/statement.d.ts +66 -1
  24. package/dist/src/core/statement.d.ts.map +1 -1
  25. package/dist/src/core/statement.js +156 -91
  26. package/dist/src/core/statement.js.map +1 -1
  27. package/dist/src/index.d.ts +3 -3
  28. package/dist/src/index.d.ts.map +1 -1
  29. package/dist/src/index.js +2 -2
  30. package/dist/src/index.js.map +1 -1
  31. package/dist/src/parser/lexer.d.ts.map +1 -1
  32. package/dist/src/parser/lexer.js +9 -3
  33. package/dist/src/parser/lexer.js.map +1 -1
  34. package/dist/src/planner/analysis/change-scope.d.ts.map +1 -1
  35. package/dist/src/planner/analysis/change-scope.js +7 -20
  36. package/dist/src/planner/analysis/change-scope.js.map +1 -1
  37. package/dist/src/planner/analysis/check-extraction.d.ts +62 -0
  38. package/dist/src/planner/analysis/check-extraction.d.ts.map +1 -1
  39. package/dist/src/planner/analysis/check-extraction.js +19 -5
  40. package/dist/src/planner/analysis/check-extraction.js.map +1 -1
  41. package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
  42. package/dist/src/planner/analysis/constraint-extractor.js +7 -0
  43. package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
  44. package/dist/src/planner/analysis/scalar-param-usage.d.ts +6 -3
  45. package/dist/src/planner/analysis/scalar-param-usage.d.ts.map +1 -1
  46. package/dist/src/planner/analysis/scalar-param-usage.js +32 -4
  47. package/dist/src/planner/analysis/scalar-param-usage.js.map +1 -1
  48. package/dist/src/planner/building/coercion.js +1 -1
  49. package/dist/src/planner/building/coercion.js.map +1 -1
  50. package/dist/src/planner/building/schema-authored-context.d.ts +7 -7
  51. package/dist/src/planner/building/schema-authored-context.js +7 -7
  52. package/dist/src/planner/nodes/scalar.d.ts +19 -1
  53. package/dist/src/planner/nodes/scalar.d.ts.map +1 -1
  54. package/dist/src/planner/nodes/scalar.js +13 -2
  55. package/dist/src/planner/nodes/scalar.js.map +1 -1
  56. package/dist/src/planner/nodes/set-operation-node.js +4 -0
  57. package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
  58. package/dist/src/runtime/emit/add-constraint.js +44 -1
  59. package/dist/src/runtime/emit/add-constraint.js.map +1 -1
  60. package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
  61. package/dist/src/runtime/emit/alter-table.js +20 -32
  62. package/dist/src/runtime/emit/alter-table.js.map +1 -1
  63. package/dist/src/runtime/emit/ddl-event-scope.d.ts +20 -13
  64. package/dist/src/runtime/emit/ddl-event-scope.d.ts.map +1 -1
  65. package/dist/src/runtime/emit/ddl-event-scope.js +20 -13
  66. package/dist/src/runtime/emit/ddl-event-scope.js.map +1 -1
  67. package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
  68. package/dist/src/runtime/emit/materialized-view-helpers.js +9 -4
  69. package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
  70. package/dist/src/runtime/emit/parameter.d.ts.map +1 -1
  71. package/dist/src/runtime/emit/parameter.js +13 -8
  72. package/dist/src/runtime/emit/parameter.js.map +1 -1
  73. package/dist/src/runtime/emit/schema-declarative.d.ts +17 -6
  74. package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
  75. package/dist/src/runtime/emit/schema-declarative.js +169 -31
  76. package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
  77. package/dist/src/schema/catalog-rendering.d.ts +26 -0
  78. package/dist/src/schema/catalog-rendering.d.ts.map +1 -1
  79. package/dist/src/schema/catalog-rendering.js +41 -4
  80. package/dist/src/schema/catalog-rendering.js.map +1 -1
  81. package/dist/src/schema/catalog.d.ts +23 -0
  82. package/dist/src/schema/catalog.d.ts.map +1 -1
  83. package/dist/src/schema/catalog.js +6 -4
  84. package/dist/src/schema/catalog.js.map +1 -1
  85. package/dist/src/schema/constraint-builder.d.ts +67 -11
  86. package/dist/src/schema/constraint-builder.d.ts.map +1 -1
  87. package/dist/src/schema/constraint-builder.js +115 -12
  88. package/dist/src/schema/constraint-builder.js.map +1 -1
  89. package/dist/src/schema/ddl-generator.d.ts +21 -0
  90. package/dist/src/schema/ddl-generator.d.ts.map +1 -1
  91. package/dist/src/schema/ddl-generator.js +7 -1
  92. package/dist/src/schema/ddl-generator.js.map +1 -1
  93. package/dist/src/schema/rename/self-qualifier-strip.d.ts +27 -0
  94. package/dist/src/schema/rename/self-qualifier-strip.d.ts.map +1 -1
  95. package/dist/src/schema/rename/self-qualifier-strip.js +73 -0
  96. package/dist/src/schema/rename/self-qualifier-strip.js.map +1 -1
  97. package/dist/src/schema/rename-rewriter.d.ts +5 -5
  98. package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
  99. package/dist/src/schema/rename-rewriter.js +5 -5
  100. package/dist/src/schema/rename-rewriter.js.map +1 -1
  101. package/dist/src/schema/schema-differ.d.ts +41 -1
  102. package/dist/src/schema/schema-differ.d.ts.map +1 -1
  103. package/dist/src/schema/schema-differ.js +529 -36
  104. package/dist/src/schema/schema-differ.js.map +1 -1
  105. package/dist/src/types/temporal-ops.d.ts.map +1 -1
  106. package/dist/src/types/temporal-ops.js +16 -10
  107. package/dist/src/types/temporal-ops.js.map +1 -1
  108. package/dist/src/vtab/capabilities.d.ts +5 -2
  109. package/dist/src/vtab/capabilities.d.ts.map +1 -1
  110. package/dist/src/vtab/memory/layer/manager.d.ts +15 -11
  111. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  112. package/dist/src/vtab/memory/layer/manager.js +15 -11
  113. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  114. package/dist/src/vtab/module.d.ts +16 -0
  115. package/dist/src/vtab/module.d.ts.map +1 -1
  116. package/package.json +3 -3
@@ -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,EAAE,KAAK,aAAa,EAAiB,MAAM,oBAAoB,CAAC;AAGvE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,GAAG,GAAG,CAAC,MAAM,GAAG,MAAM,EAAE,UAAU,CAAC,GAAG,SAAS,CAuBjH;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAK/D"}
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"}
@@ -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
- // For named params like ':name', ParameterScope expects 'name' as key for hints.
32
- // A positional param bound after prepare (bind/bindAll) lands here too, keyed
33
- // by its stringified index (`boundArgs[index + 1]`) — normalize it back to a
34
- // number so it lines up with the array branch above and with ParameterScope's
35
- // own key, rather than silently missing the hint lookup.
36
- const name = key.startsWith(':') ? key.substring(1) : key;
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":"AAEA,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAExE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAiC;IAClE,IAAI,OAAqD,CAAC;IAC1D,IAAI,MAAM,EAAE,CAAC;QACZ,OAAO,GAAG,IAAI,GAAG,EAA+B,CAAC;QACjD,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,MAAM,CAAC,OAAO,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,EAAE;gBACpC,uFAAuF;gBACvF,2EAA2E;gBAC3E,OAAQ,CAAC,GAAG,CAAC,KAAK,GAAG,CAAC,EAAE,sBAAsB,CAAC,UAAU,CAAC,CAAC,CAAC;YAC7D,CAAC,CAAC,CAAC;QACJ,CAAC;aAAM,CAAC;YACP,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;gBAC/C,iFAAiF;gBACjF,8EAA8E;gBAC9E,6EAA6E;gBAC7E,8EAA8E;gBAC9E,yDAAyD;gBACzD,MAAM,IAAI,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;gBAC1D,OAAQ,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,sBAAsB,CAAC,KAAK,CAAC,CAAC,CAAC;YACtE,CAAC,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IACD,OAAO,OAAO,CAAC;AAChB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC7C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IAC3B,6EAA6E;IAC7E,OAAO,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACnD,CAAC;AAED;;;;;GAKG;AACH,SAAS,sBAAsB,CAAC,KAAe;IAC9C,MAAM,WAAW,GAAG,yBAAyB,CAAC,KAAK,CAAC,CAAC;IAErD,OAAO;QACN,SAAS,EAAE,QAAQ;QACnB,WAAW;QACX,QAAQ,EAAE,KAAK,KAAK,IAAI;QACxB,UAAU,EAAE,IAAI;KAChB,CAAC;AACH,CAAC"}
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
- /** Parameter types established at prepare time (either explicit or inferred from initial values) */
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":"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"}
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
- /** Parameter types established at prepare time (either explicit or inferred from initial values) */
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 - infer types and bind them
122
- this.parameterTypes = getParameterTypes(paramsOrTypes);
123
- // Also bind the initial values. Values canonicalize as they enter boundArgs
124
- // (a safe-range bigint narrows to number, R1 — util/numeric-canonical.ts):
125
- // per-bind, not per-row, and shared by all three ingress sites (here,
126
- // bind, bindAll). Type inference above saw the raw values, but it maps a
127
- // safe-range bigint and its number form to INTEGER alike, so no drift.
128
- if (Array.isArray(paramsOrTypes)) {
129
- paramsOrTypes.forEach((value, index) => {
130
- this.boundArgs[index + 1] = canonicalizeSqlValue(value);
131
- });
132
- }
133
- else {
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.plan = null;
160
- this.emissionContext = null;
161
- this.scheduler = null;
162
- this.needsCompile = true;
163
- this.columnDefCache.clear();
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
- // On first compilation, establish the parameter types
189
- // Use explicit types if provided, otherwise infer from bound args
190
- // NOTE: this freezes on the FIRST compile, and empty bound args yield an empty
191
- // (but established) map — so compiling before any bind (getColumnDefs()/isQuery()
192
- // on a freshly prepared statement) permanently leaves every parameter at the
193
- // default TEXT type, while binding first types them from the values. The
194
- // execution paths bind before compiling, so this only shows through pre-bind
195
- // introspection. Deliberate: re-inferring per bind would recompile the plan on
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], this.parameterTypes, this._schemaPathOverride);
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.needsCompile = true;
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 === 'number') {
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
- this.boundArgs = {};
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
- // Presence check, not `??`: a parameter legitimately bound to `null` must
911
- // use that binding, not fall through to the `:`-prefixed alternate key.
912
- const value = typeof key === 'string'
913
- ? (Object.hasOwn(this.boundArgs, key) ? this.boundArgs[key] : this.boundArgs[`:${key}`])
914
- : this.boundArgs[key];
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
- if (this.parameterTypes === undefined) {
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
  /**