@quereus/quereus 4.8.0 → 4.10.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 (280) hide show
  1. package/dist/src/core/database.d.ts +6 -0
  2. package/dist/src/core/database.d.ts.map +1 -1
  3. package/dist/src/core/database.js +45 -5
  4. package/dist/src/core/database.js.map +1 -1
  5. package/dist/src/core/statement.d.ts.map +1 -1
  6. package/dist/src/core/statement.js +74 -15
  7. package/dist/src/core/statement.js.map +1 -1
  8. package/dist/src/func/builtins/aggregate.d.ts.map +1 -1
  9. package/dist/src/func/builtins/aggregate.js +88 -35
  10. package/dist/src/func/builtins/aggregate.js.map +1 -1
  11. package/dist/src/func/builtins/conversion.d.ts +5 -0
  12. package/dist/src/func/builtins/conversion.d.ts.map +1 -1
  13. package/dist/src/func/builtins/conversion.js +16 -2
  14. package/dist/src/func/builtins/conversion.js.map +1 -1
  15. package/dist/src/func/builtins/explain.d.ts.map +1 -1
  16. package/dist/src/func/builtins/explain.js +9 -2
  17. package/dist/src/func/builtins/explain.js.map +1 -1
  18. package/dist/src/func/builtins/index.d.ts.map +1 -1
  19. package/dist/src/func/builtins/index.js +5 -2
  20. package/dist/src/func/builtins/index.js.map +1 -1
  21. package/dist/src/func/builtins/scalar.d.ts.map +1 -1
  22. package/dist/src/func/builtins/scalar.js +4 -2
  23. package/dist/src/func/builtins/scalar.js.map +1 -1
  24. package/dist/src/func/builtins/string.d.ts +2 -0
  25. package/dist/src/func/builtins/string.d.ts.map +1 -1
  26. package/dist/src/func/builtins/string.js +60 -4
  27. package/dist/src/func/builtins/string.js.map +1 -1
  28. package/dist/src/func/registration.d.ts +6 -0
  29. package/dist/src/func/registration.d.ts.map +1 -1
  30. package/dist/src/func/registration.js +1 -0
  31. package/dist/src/func/registration.js.map +1 -1
  32. package/dist/src/index.d.ts +7 -3
  33. package/dist/src/index.d.ts.map +1 -1
  34. package/dist/src/index.js +21 -2
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/planner/building/constraint-builder.js +1 -1
  37. package/dist/src/planner/building/constraint-builder.js.map +1 -1
  38. package/dist/src/planner/nodes/scalar.d.ts.map +1 -1
  39. package/dist/src/planner/nodes/scalar.js +13 -1
  40. package/dist/src/planner/nodes/scalar.js.map +1 -1
  41. package/dist/src/planner/rules/retrieve/rule-grow-retrieve.d.ts.map +1 -1
  42. package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js +121 -51
  43. package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js.map +1 -1
  44. package/dist/src/planner/rules/shared/index-style-context.d.ts +14 -4
  45. package/dist/src/planner/rules/shared/index-style-context.d.ts.map +1 -1
  46. package/dist/src/planner/rules/shared/index-style-context.js +14 -4
  47. package/dist/src/planner/rules/shared/index-style-context.js.map +1 -1
  48. package/dist/src/runtime/emission-context.d.ts +14 -1
  49. package/dist/src/runtime/emission-context.d.ts.map +1 -1
  50. package/dist/src/runtime/emission-context.js +17 -5
  51. package/dist/src/runtime/emission-context.js.map +1 -1
  52. package/dist/src/runtime/emit/aggregate-setup.d.ts +44 -8
  53. package/dist/src/runtime/emit/aggregate-setup.d.ts.map +1 -1
  54. package/dist/src/runtime/emit/aggregate-setup.js +79 -15
  55. package/dist/src/runtime/emit/aggregate-setup.js.map +1 -1
  56. package/dist/src/runtime/emit/aggregate.d.ts.map +1 -1
  57. package/dist/src/runtime/emit/aggregate.js +21 -106
  58. package/dist/src/runtime/emit/aggregate.js.map +1 -1
  59. package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
  60. package/dist/src/runtime/emit/alter-table.js +212 -155
  61. package/dist/src/runtime/emit/alter-table.js.map +1 -1
  62. package/dist/src/runtime/emit/asof-scan.js +1 -1
  63. package/dist/src/runtime/emit/asof-scan.js.map +1 -1
  64. package/dist/src/runtime/emit/assertion-drop-guard.d.ts +26 -12
  65. package/dist/src/runtime/emit/assertion-drop-guard.d.ts.map +1 -1
  66. package/dist/src/runtime/emit/assertion-drop-guard.js +45 -26
  67. package/dist/src/runtime/emit/assertion-drop-guard.js.map +1 -1
  68. package/dist/src/runtime/emit/assertion-rename-helpers.d.ts +6 -6
  69. package/dist/src/runtime/emit/assertion-rename-helpers.d.ts.map +1 -1
  70. package/dist/src/runtime/emit/assertion-rename-helpers.js +18 -20
  71. package/dist/src/runtime/emit/assertion-rename-helpers.js.map +1 -1
  72. package/dist/src/runtime/emit/between.d.ts +2 -0
  73. package/dist/src/runtime/emit/between.d.ts.map +1 -1
  74. package/dist/src/runtime/emit/between.js +7 -8
  75. package/dist/src/runtime/emit/between.js.map +1 -1
  76. package/dist/src/runtime/emit/binary.d.ts +27 -5
  77. package/dist/src/runtime/emit/binary.d.ts.map +1 -1
  78. package/dist/src/runtime/emit/binary.js +278 -187
  79. package/dist/src/runtime/emit/binary.js.map +1 -1
  80. package/dist/src/runtime/emit/case.d.ts +34 -0
  81. package/dist/src/runtime/emit/case.d.ts.map +1 -1
  82. package/dist/src/runtime/emit/case.js +18 -11
  83. package/dist/src/runtime/emit/case.js.map +1 -1
  84. package/dist/src/runtime/emit/cast.d.ts +2 -0
  85. package/dist/src/runtime/emit/cast.d.ts.map +1 -1
  86. package/dist/src/runtime/emit/cast.js +7 -5
  87. package/dist/src/runtime/emit/cast.js.map +1 -1
  88. package/dist/src/runtime/emit/column-reference.d.ts +3 -1
  89. package/dist/src/runtime/emit/column-reference.d.ts.map +1 -1
  90. package/dist/src/runtime/emit/column-reference.js +6 -2
  91. package/dist/src/runtime/emit/column-reference.js.map +1 -1
  92. package/dist/src/runtime/emit/column-rename-cascade.d.ts +84 -0
  93. package/dist/src/runtime/emit/column-rename-cascade.d.ts.map +1 -0
  94. package/dist/src/runtime/emit/column-rename-cascade.js +134 -0
  95. package/dist/src/runtime/emit/column-rename-cascade.js.map +1 -0
  96. package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
  97. package/dist/src/runtime/emit/dml-executor.js +21 -0
  98. package/dist/src/runtime/emit/dml-executor.js.map +1 -1
  99. package/dist/src/runtime/emit/drop-column-guards.d.ts +82 -53
  100. package/dist/src/runtime/emit/drop-column-guards.d.ts.map +1 -1
  101. package/dist/src/runtime/emit/drop-column-guards.js +155 -90
  102. package/dist/src/runtime/emit/drop-column-guards.js.map +1 -1
  103. package/dist/src/runtime/emit/drop-table.d.ts.map +1 -1
  104. package/dist/src/runtime/emit/drop-table.js +5 -0
  105. package/dist/src/runtime/emit/drop-table.js.map +1 -1
  106. package/dist/src/runtime/emit/drop-view.d.ts.map +1 -1
  107. package/dist/src/runtime/emit/drop-view.js +5 -0
  108. package/dist/src/runtime/emit/drop-view.js.map +1 -1
  109. package/dist/src/runtime/emit/expression-drop-guard.d.ts +57 -0
  110. package/dist/src/runtime/emit/expression-drop-guard.d.ts.map +1 -0
  111. package/dist/src/runtime/emit/expression-drop-guard.js +82 -0
  112. package/dist/src/runtime/emit/expression-drop-guard.js.map +1 -0
  113. package/dist/src/runtime/emit/filter.d.ts +1 -1
  114. package/dist/src/runtime/emit/filter.js +1 -1
  115. package/dist/src/runtime/emit/hash-aggregate.d.ts.map +1 -1
  116. package/dist/src/runtime/emit/hash-aggregate.js +18 -88
  117. package/dist/src/runtime/emit/hash-aggregate.js.map +1 -1
  118. package/dist/src/runtime/emit/literal.d.ts +14 -1
  119. package/dist/src/runtime/emit/literal.d.ts.map +1 -1
  120. package/dist/src/runtime/emit/literal.js +33 -7
  121. package/dist/src/runtime/emit/literal.js.map +1 -1
  122. package/dist/src/runtime/emit/materialized-view-helpers.d.ts +33 -10
  123. package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
  124. package/dist/src/runtime/emit/materialized-view-helpers.js +158 -49
  125. package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
  126. package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -1
  127. package/dist/src/runtime/emit/materialized-view.js +3 -0
  128. package/dist/src/runtime/emit/materialized-view.js.map +1 -1
  129. package/dist/src/runtime/emit/parameter.d.ts +3 -1
  130. package/dist/src/runtime/emit/parameter.d.ts.map +1 -1
  131. package/dist/src/runtime/emit/parameter.js +7 -2
  132. package/dist/src/runtime/emit/parameter.js.map +1 -1
  133. package/dist/src/runtime/emit/scalar-function.d.ts +29 -1
  134. package/dist/src/runtime/emit/scalar-function.d.ts.map +1 -1
  135. package/dist/src/runtime/emit/scalar-function.js +66 -16
  136. package/dist/src/runtime/emit/scalar-function.js.map +1 -1
  137. package/dist/src/runtime/emit/scalar-op.d.ts +63 -0
  138. package/dist/src/runtime/emit/scalar-op.d.ts.map +1 -0
  139. package/dist/src/runtime/emit/scalar-op.js +37 -0
  140. package/dist/src/runtime/emit/scalar-op.js.map +1 -0
  141. package/dist/src/runtime/emit/scan.d.ts.map +1 -1
  142. package/dist/src/runtime/emit/scan.js +19 -1
  143. package/dist/src/runtime/emit/scan.js.map +1 -1
  144. package/dist/src/runtime/emit/temporal-arithmetic.d.ts +18 -8
  145. package/dist/src/runtime/emit/temporal-arithmetic.d.ts.map +1 -1
  146. package/dist/src/runtime/emit/temporal-arithmetic.js +29 -311
  147. package/dist/src/runtime/emit/temporal-arithmetic.js.map +1 -1
  148. package/dist/src/runtime/emit/unary.d.ts +2 -0
  149. package/dist/src/runtime/emit/unary.d.ts.map +1 -1
  150. package/dist/src/runtime/emit/unary.js +125 -53
  151. package/dist/src/runtime/emit/unary.js.map +1 -1
  152. package/dist/src/runtime/emitters.d.ts +22 -0
  153. package/dist/src/runtime/emitters.d.ts.map +1 -1
  154. package/dist/src/runtime/emitters.js +29 -0
  155. package/dist/src/runtime/emitters.js.map +1 -1
  156. package/dist/src/runtime/scalar-fusion.d.ts +47 -0
  157. package/dist/src/runtime/scalar-fusion.d.ts.map +1 -0
  158. package/dist/src/runtime/scalar-fusion.js +275 -0
  159. package/dist/src/runtime/scalar-fusion.js.map +1 -0
  160. package/dist/src/runtime/strict-flags.d.ts +9 -0
  161. package/dist/src/runtime/strict-flags.d.ts.map +1 -1
  162. package/dist/src/runtime/strict-flags.js +9 -0
  163. package/dist/src/runtime/strict-flags.js.map +1 -1
  164. package/dist/src/runtime/strict-fork.js +2 -2
  165. package/dist/src/runtime/strict-fork.js.map +1 -1
  166. package/dist/src/runtime/strict-representation.d.ts +78 -0
  167. package/dist/src/runtime/strict-representation.d.ts.map +1 -0
  168. package/dist/src/runtime/strict-representation.js +159 -0
  169. package/dist/src/runtime/strict-representation.js.map +1 -0
  170. package/dist/src/schema/assertion.d.ts +58 -0
  171. package/dist/src/schema/assertion.d.ts.map +1 -1
  172. package/dist/src/schema/assertion.js +78 -0
  173. package/dist/src/schema/assertion.js.map +1 -1
  174. package/dist/src/schema/catalog-persistability.d.ts +11 -3
  175. package/dist/src/schema/catalog-persistability.d.ts.map +1 -1
  176. package/dist/src/schema/catalog-persistability.js +48 -29
  177. package/dist/src/schema/catalog-persistability.js.map +1 -1
  178. package/dist/src/schema/catalog.d.ts +22 -0
  179. package/dist/src/schema/catalog.d.ts.map +1 -1
  180. package/dist/src/schema/catalog.js +4 -1
  181. package/dist/src/schema/catalog.js.map +1 -1
  182. package/dist/src/schema/column-republication.d.ts +126 -0
  183. package/dist/src/schema/column-republication.d.ts.map +1 -0
  184. package/dist/src/schema/column-republication.js +121 -0
  185. package/dist/src/schema/column-republication.js.map +1 -0
  186. package/dist/src/schema/column-source-resolver.d.ts +29 -6
  187. package/dist/src/schema/column-source-resolver.d.ts.map +1 -1
  188. package/dist/src/schema/column-source-resolver.js +78 -9
  189. package/dist/src/schema/column-source-resolver.js.map +1 -1
  190. package/dist/src/schema/expression-dependents.d.ts +160 -0
  191. package/dist/src/schema/expression-dependents.d.ts.map +1 -0
  192. package/dist/src/schema/expression-dependents.js +228 -0
  193. package/dist/src/schema/expression-dependents.js.map +1 -0
  194. package/dist/src/schema/function.d.ts +15 -0
  195. package/dist/src/schema/function.d.ts.map +1 -1
  196. package/dist/src/schema/function.js.map +1 -1
  197. package/dist/src/schema/object-dependency-closure.d.ts +127 -0
  198. package/dist/src/schema/object-dependency-closure.d.ts.map +1 -0
  199. package/dist/src/schema/object-dependency-closure.js +117 -0
  200. package/dist/src/schema/object-dependency-closure.js.map +1 -0
  201. package/dist/src/schema/object-ref-resolver.d.ts +83 -0
  202. package/dist/src/schema/object-ref-resolver.d.ts.map +1 -0
  203. package/dist/src/schema/object-ref-resolver.js +135 -0
  204. package/dist/src/schema/object-ref-resolver.js.map +1 -0
  205. package/dist/src/schema/rename/column-rename.d.ts +228 -0
  206. package/dist/src/schema/rename/column-rename.d.ts.map +1 -0
  207. package/dist/src/schema/rename/column-rename.js +1225 -0
  208. package/dist/src/schema/rename/column-rename.js.map +1 -0
  209. package/dist/src/schema/rename/self-qualifier-strip.d.ts +29 -0
  210. package/dist/src/schema/rename/self-qualifier-strip.d.ts.map +1 -0
  211. package/dist/src/schema/rename/self-qualifier-strip.js +267 -0
  212. package/dist/src/schema/rename/self-qualifier-strip.js.map +1 -0
  213. package/dist/src/schema/rename/shared.d.ts +108 -0
  214. package/dist/src/schema/rename/shared.d.ts.map +1 -0
  215. package/dist/src/schema/rename/shared.js +49 -0
  216. package/dist/src/schema/rename/shared.js.map +1 -0
  217. package/dist/src/schema/rename/table-rename.d.ts +263 -0
  218. package/dist/src/schema/rename/table-rename.d.ts.map +1 -0
  219. package/dist/src/schema/rename/table-rename.js +860 -0
  220. package/dist/src/schema/rename/table-rename.js.map +1 -0
  221. package/dist/src/schema/rename-rewriter.d.ts +18 -278
  222. package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
  223. package/dist/src/schema/rename-rewriter.js +17 -1506
  224. package/dist/src/schema/rename-rewriter.js.map +1 -1
  225. package/dist/src/schema/schema-differ.d.ts.map +1 -1
  226. package/dist/src/schema/schema-differ.js +228 -21
  227. package/dist/src/schema/schema-differ.js.map +1 -1
  228. package/dist/src/types/builtin-types.d.ts.map +1 -1
  229. package/dist/src/types/builtin-types.js +29 -36
  230. package/dist/src/types/builtin-types.js.map +1 -1
  231. package/dist/src/types/cast-semantics.d.ts +7 -5
  232. package/dist/src/types/cast-semantics.d.ts.map +1 -1
  233. package/dist/src/types/cast-semantics.js +20 -9
  234. package/dist/src/types/cast-semantics.js.map +1 -1
  235. package/dist/src/types/index.d.ts +1 -1
  236. package/dist/src/types/index.d.ts.map +1 -1
  237. package/dist/src/types/index.js +1 -1
  238. package/dist/src/types/index.js.map +1 -1
  239. package/dist/src/types/registry.d.ts.map +1 -1
  240. package/dist/src/types/registry.js +2 -1
  241. package/dist/src/types/registry.js.map +1 -1
  242. package/dist/src/types/temporal-ops.d.ts +119 -0
  243. package/dist/src/types/temporal-ops.d.ts.map +1 -0
  244. package/dist/src/types/temporal-ops.js +348 -0
  245. package/dist/src/types/temporal-ops.js.map +1 -0
  246. package/dist/src/types/temporal-types.d.ts +11 -0
  247. package/dist/src/types/temporal-types.d.ts.map +1 -1
  248. package/dist/src/types/temporal-types.js +87 -0
  249. package/dist/src/types/temporal-types.js.map +1 -1
  250. package/dist/src/util/affinity.d.ts +2 -1
  251. package/dist/src/util/affinity.d.ts.map +1 -1
  252. package/dist/src/util/affinity.js +16 -5
  253. package/dist/src/util/affinity.js.map +1 -1
  254. package/dist/src/util/ast-spine-clone.d.ts +2 -2
  255. package/dist/src/util/ast-spine-clone.js +2 -2
  256. package/dist/src/util/coercion.d.ts +22 -7
  257. package/dist/src/util/coercion.d.ts.map +1 -1
  258. package/dist/src/util/coercion.js +28 -15
  259. package/dist/src/util/coercion.js.map +1 -1
  260. package/dist/src/util/comparison.d.ts +25 -0
  261. package/dist/src/util/comparison.d.ts.map +1 -1
  262. package/dist/src/util/comparison.js +25 -0
  263. package/dist/src/util/comparison.js.map +1 -1
  264. package/dist/src/util/key-tuple-codec.d.ts +12 -3
  265. package/dist/src/util/key-tuple-codec.d.ts.map +1 -1
  266. package/dist/src/util/key-tuple-codec.js.map +1 -1
  267. package/dist/src/util/numeric-canonical.d.ts +26 -0
  268. package/dist/src/util/numeric-canonical.d.ts.map +1 -0
  269. package/dist/src/util/numeric-canonical.js +66 -0
  270. package/dist/src/util/numeric-canonical.js.map +1 -0
  271. package/dist/src/util/value-text.d.ts +48 -0
  272. package/dist/src/util/value-text.d.ts.map +1 -0
  273. package/dist/src/util/value-text.js +35 -0
  274. package/dist/src/util/value-text.js.map +1 -0
  275. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  276. package/dist/src/vtab/memory/layer/manager.js +14 -5
  277. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  278. package/dist/src/vtab/module.d.ts +25 -0
  279. package/dist/src/vtab/module.d.ts.map +1 -1
  280. package/package.json +4 -3
@@ -6,10 +6,25 @@ import { hasRelationalDescendant } from "../../planner/analysis/scalar-subquerie
6
6
  import { emitPlanNode, emitCallFromPlan } from "../emitters.js";
7
7
  import { compareSqlValuesFast, createTypedComparator, hasSemanticOrdering, isTruthy } from "../../util/comparison.js";
8
8
  import { coerceToNumberForArithmetic } from "../../util/coercion.js";
9
+ import { canonicalizeInteger } from "../../util/numeric-canonical.js";
10
+ import { valueToText } from "../../util/value-text.js";
9
11
  import { simpleLike, compileLikeMatcher } from "../../util/patterns.js";
10
12
  import { tryTemporalArithmetic, tryTemporalComparison } from "./temporal-arithmetic.js";
13
+ import { runTemporalCase, temporalOpCaseForTypes, unsupportedTemporalOp } from "../../types/temporal-ops.js";
11
14
  import { effectiveComparisonCollation } from "../../planner/analysis/comparison-collation.js";
12
- export function emitBinaryOp(plan, ctx) {
15
+ import { emitScalarOp } from "./scalar-op.js";
16
+ /**
17
+ * Operator dispatch for a binary node — the single entry point both consumers share, so
18
+ * neither has to restate which operator routes to which body. `undefined` means the node
19
+ * has no {@link ScalarOpSpec}: today that is exactly the AND/OR short-circuit form, whose
20
+ * right operand is a deferred sub-program rather than a value (see
21
+ * {@link buildLogicalOpSpec}). A fusion consumer treats `undefined` as "do not fuse";
22
+ * {@link emitBinaryOp} falls back to {@link emitShortCircuitLogicalOp}.
23
+ *
24
+ * An unsupported operator throws here rather than returning `undefined` — that is an
25
+ * unimplemented operator, not a node that declines to fuse.
26
+ */
27
+ export function buildBinaryOpSpec(plan, ctx) {
13
28
  // Normalize operator to uppercase for case-insensitive matching of keywords
14
29
  const operator = plan.expression.operator.toUpperCase();
15
30
  switch (operator) {
@@ -18,7 +33,7 @@ export function emitBinaryOp(plan, ctx) {
18
33
  case '*':
19
34
  case '/':
20
35
  case '%':
21
- return emitNumericOp(plan, ctx);
36
+ return buildNumericOpSpec(plan);
22
37
  case '=':
23
38
  case '==':
24
39
  case '!=':
@@ -27,20 +42,24 @@ export function emitBinaryOp(plan, ctx) {
27
42
  case '<=':
28
43
  case '>':
29
44
  case '>=':
30
- return emitComparisonOp(plan, ctx);
45
+ return buildComparisonOpSpec(plan, ctx);
31
46
  case '||':
32
- return emitConcatOp(plan, ctx);
47
+ return buildConcatOpSpec(plan);
33
48
  case 'AND':
34
49
  case 'OR':
35
50
  case 'XOR':
36
- return emitLogicalOp(plan, ctx);
51
+ return buildLogicalOpSpec(plan);
37
52
  case 'LIKE':
38
- return emitLikeOp(plan, ctx);
39
- // TODO: emitBitwise
53
+ return buildLikeOpSpec(plan);
54
+ // TODO: bitwise operators
40
55
  default:
41
56
  quereusError(`Unsupported binary operator: ${plan.expression.operator}`, StatusCode.UNSUPPORTED, undefined, plan.expression);
42
57
  }
43
58
  }
59
+ export function emitBinaryOp(plan, ctx) {
60
+ const spec = buildBinaryOpSpec(plan, ctx);
61
+ return spec ? emitScalarOp(spec, ctx) : emitShortCircuitLogicalOp(plan, ctx);
62
+ }
44
63
  /** Handle arithmetic when at least one operand is bigint.
45
64
  * Both bigint → use bigint arithmetic directly.
46
65
  * Mixed (one bigint, one non-bigint):
@@ -50,7 +69,11 @@ export function emitBinaryOp(plan, ctx) {
50
69
  function mixedBigIntArithmetic(v1, v2, inner, innerBigInt) {
51
70
  if (typeof v1 === 'bigint' && typeof v2 === 'bigint') {
52
71
  try {
53
- return innerBigInt(v1, v2);
72
+ // Narrow a result that lands inside the safe-integer range back to number
73
+ // (R1, util/numeric-canonical.ts) — on the success path only; the catch
74
+ // arms (division by zero, RangeError) still return null. Narrowing itself
75
+ // cannot throw.
76
+ return canonicalizeInteger(innerBigInt(v1, v2));
54
77
  }
55
78
  catch {
56
79
  return null;
@@ -64,7 +87,8 @@ function mixedBigIntArithmetic(v1, v2, inner, innerBigInt) {
64
87
  const num = typeof v1n === 'bigint' ? v2n : v1n;
65
88
  if (Number.isInteger(num)) {
66
89
  try {
67
- return innerBigInt(typeof v1n === 'bigint' ? v1n : BigInt(v1n), typeof v2n === 'bigint' ? v2n : BigInt(v2n));
90
+ // Same success-path narrowing as the both-bigint arm above.
91
+ return canonicalizeInteger(innerBigInt(typeof v1n === 'bigint' ? v1n : BigInt(v1n), typeof v2n === 'bigint' ? v2n : BigInt(v2n)));
68
92
  }
69
93
  catch {
70
94
  // Fall through to float path (e.g., division by zero)
@@ -78,7 +102,43 @@ function mixedBigIntArithmetic(v1, v2, inner, innerBigInt) {
78
102
  return null;
79
103
  return result;
80
104
  }
81
- export function emitNumericOp(plan, ctx) {
105
+ /**
106
+ * The general arithmetic body: sniff the values for a temporal shape first, then fall back
107
+ * to coercing numeric arithmetic.
108
+ *
109
+ * Two branches of {@link buildNumericOpSpec} share it — the temporal arm whose declared
110
+ * types settle nothing, and the non-temporal generic path — because value sniffing is the
111
+ * defined semantics for both. They differ only in the note they carry; keeping one body
112
+ * means they cannot come to differ in behavior.
113
+ */
114
+ function buildCoercingArithmeticRun(operator, inner, innerBigInt) {
115
+ return function runCoercingArithmetic(_ctx, v1, v2) {
116
+ const temporalResult = tryTemporalArithmetic(operator, v1, v2);
117
+ if (temporalResult !== undefined) {
118
+ return temporalResult;
119
+ }
120
+ if (v1 !== null && v2 !== null) {
121
+ if (typeof v1 === 'bigint' || typeof v2 === 'bigint') {
122
+ return mixedBigIntArithmetic(v1, v2, inner, innerBigInt);
123
+ }
124
+ else {
125
+ const n1 = coerceToNumberForArithmetic(v1);
126
+ const n2 = coerceToNumberForArithmetic(v2);
127
+ try {
128
+ const result = inner(n1, n2);
129
+ if (!Number.isFinite(result))
130
+ return null;
131
+ return result;
132
+ }
133
+ catch {
134
+ return null;
135
+ }
136
+ }
137
+ }
138
+ return null;
139
+ };
140
+ }
141
+ export function buildNumericOpSpec(plan) {
82
142
  let inner;
83
143
  let innerBigInt;
84
144
  switch (plan.expression.operator) {
@@ -111,51 +171,79 @@ export function emitNumericOp(plan, ctx) {
111
171
  let run;
112
172
  let note;
113
173
  if (leftLogical.isTemporal || rightLogical.isTemporal) {
114
- // Temporal path: must check temporal arithmetic first
115
- run = function runTemporalArithmetic(ctx, v1, v2) {
116
- const temporalResult = tryTemporalArithmetic(plan.expression.operator, v1, v2);
117
- if (temporalResult !== undefined) {
118
- return temporalResult;
119
- }
120
- if (v1 !== null && v2 !== null) {
121
- if (typeof v1 === 'bigint' || typeof v2 === 'bigint') {
122
- return mixedBigIntArithmetic(v1, v2, inner, innerBigInt);
123
- }
124
- else {
125
- const n1 = coerceToNumberForArithmetic(v1);
126
- const n2 = coerceToNumberForArithmetic(v2);
127
- try {
128
- const result = inner(n1, n2);
129
- if (!Number.isFinite(result))
130
- return null;
131
- return result;
132
- }
133
- catch {
134
- return null;
135
- }
136
- }
137
- }
138
- return null;
139
- };
140
- note = `${plan.expression.operator}(temporal)`;
174
+ // Temporal path. Both operands' declared logical types are already in hand — that
175
+ // is *why* this branch was selected — so the (operator, left kind, right kind)
176
+ // lookup into the operation table (types/temporal-ops.ts) happens once here rather
177
+ // than once per row. Same lookup `BinaryOpNode.generateType` used to announce the
178
+ // result type, through the same function, so the two cannot select different cases.
179
+ // Three arms, chosen at emit:
180
+ const { kinds, entry } = temporalOpCaseForTypes(plan.expression.operator, leftLogical, rightLogical);
181
+ if (kinds && entry) {
182
+ // (1) Specialized: the case is fixed, so the per-row path is one call into the
183
+ // shared envelope — no value sniffing at all.
184
+ //
185
+ // This arm TRUSTS the declared types. A DATE-declared operand actually holding
186
+ // `'garbage'` returns null here (runTemporalCase's catch), where the sniffing
187
+ // path below would have raised `Unsupported temporal operation`. That is the
188
+ // intended trade: write-side coercion enforces declared logical types on every
189
+ // normal path, so only a misbehaving virtual table can produce such a value.
190
+ run = function runTemporalCased(_ctx, v1, v2) {
191
+ return runTemporalCase(entry, v1, v2);
192
+ };
193
+ note = `${plan.expression.operator}(temporal-${kinds[0]}-${kinds[1]})`;
194
+ }
195
+ else if (kinds) {
196
+ // (2) Statically unsupported: both kinds are known and the table has no case
197
+ // for them (DATE + DATE, DATE * NUMBER, TIME − DATE, anything with `%`), so no
198
+ // combination of runtime values can succeed.
199
+ //
200
+ // The throw deliberately stays at RUNTIME rather than moving to emit/plan time.
201
+ // Today the error is raised only when a row is actually evaluated, so
202
+ // `select case when 0 then date_a + date_b else 1 end`, the same expression
203
+ // under `where 1 = 0`, and the same expression over an empty table all succeed.
204
+ // Hoisting the throw would turn each into a hard failure for no gain — a
205
+ // constant-throw closure costs nothing per row on the paths that never run it.
206
+ //
207
+ // The null check is load-bearing: `date_a + date_b` with a NULL operand returns
208
+ // null today rather than erroring, and must keep doing so.
209
+ run = function runTemporalUnsupported(_ctx, v1, v2) {
210
+ if (v1 === null || v2 === null)
211
+ return null;
212
+ unsupportedTemporalOp();
213
+ };
214
+ note = `${plan.expression.operator}(temporal-unsupported)`;
215
+ }
216
+ else {
217
+ // (3) Fallback: at least one operand is TEXT / ANY / NULL / TIMESTAMP / a
218
+ // plugin-registered temporal type, so the declared types cannot settle the
219
+ // case. Runtime sniffing IS the defined semantics there (a TEXT column holding
220
+ // a duration string is a supported shape), so this arm is the generic body
221
+ // below under a different note — same function, so they cannot drift.
222
+ run = buildCoercingArithmeticRun(plan.expression.operator, inner, innerBigInt);
223
+ note = `${plan.expression.operator}(temporal)`;
224
+ }
141
225
  }
142
226
  else if (leftLogical.isNumeric && rightLogical.isNumeric) {
143
227
  // Numeric-only path: skip temporal check and coercion entirely
228
+ //
229
+ // NOTE: accepted tradeoff — the two null checks plus two `typeof === 'bigint'`
230
+ // checks below are NOT collapsed into one `typeof === 'number'` pair with the
231
+ // null/bigint handling behind a fallback. Measured at 1.19 ns vs 1.22 ns per
232
+ // operation (isolated microbench, see `compareSqlValuesFast` in
233
+ // util/comparison.ts) — no difference at all, so the guard would be pure noise.
144
234
  run = function runNumericOnly(ctx, v1, v2) {
145
235
  if (v1 !== null && v2 !== null) {
146
236
  if (typeof v1 === 'bigint' || typeof v2 === 'bigint') {
147
237
  return mixedBigIntArithmetic(v1, v2, inner, innerBigInt);
148
238
  }
149
239
  else {
150
- try {
151
- const result = inner(v1, v2);
152
- if (!Number.isFinite(result))
153
- return null;
154
- return result;
155
- }
156
- catch {
240
+ // No try/catch: `inner` over plain numbers cannot throw (division by
241
+ // zero yields Infinity/NaN, caught by the finite check below); only
242
+ // the bigint arm above can throw (handled inside mixedBigIntArithmetic).
243
+ const result = inner(v1, v2);
244
+ if (!Number.isFinite(result))
157
245
  return null;
158
- }
246
+ return result;
159
247
  }
160
248
  }
161
249
  return null;
@@ -164,42 +252,16 @@ export function emitNumericOp(plan, ctx) {
164
252
  }
165
253
  else {
166
254
  // Generic path: temporal check + coercion (for TEXT or mixed types)
167
- run = function runGenericArithmetic(ctx, v1, v2) {
168
- const temporalResult = tryTemporalArithmetic(plan.expression.operator, v1, v2);
169
- if (temporalResult !== undefined) {
170
- return temporalResult;
171
- }
172
- if (v1 !== null && v2 !== null) {
173
- if (typeof v1 === 'bigint' || typeof v2 === 'bigint') {
174
- return mixedBigIntArithmetic(v1, v2, inner, innerBigInt);
175
- }
176
- else {
177
- const n1 = coerceToNumberForArithmetic(v1);
178
- const n2 = coerceToNumberForArithmetic(v2);
179
- try {
180
- const result = inner(n1, n2);
181
- if (!Number.isFinite(result))
182
- return null;
183
- return result;
184
- }
185
- catch {
186
- return null;
187
- }
188
- }
189
- }
190
- return null;
191
- };
255
+ run = buildCoercingArithmeticRun(plan.expression.operator, inner, innerBigInt);
192
256
  note = `${plan.expression.operator}(numeric)`;
193
257
  }
194
- const leftExpr = emitPlanNode(plan.left, ctx);
195
- const rightExpr = emitPlanNode(plan.right, ctx);
196
258
  return {
197
- params: [leftExpr, rightExpr],
198
- run: asRun(run),
259
+ operands: [plan.left, plan.right],
260
+ run,
199
261
  note
200
262
  };
201
263
  }
202
- export function emitComparisonOp(plan, ctx) {
264
+ export function buildComparisonOpSpec(plan, ctx) {
203
265
  const leftType = plan.left.getType();
204
266
  const rightType = plan.right.getType();
205
267
  // One shared, symmetric resolution for plan-time facts and runtime behavior
@@ -241,6 +303,11 @@ export function emitComparisonOp(plan, ctx) {
241
303
  else if (!needsTemporalCheck && bothSameCategory) {
242
304
  // Fast same-category comparison: no temporal check, no coercion needed
243
305
  // Use compareSqlValuesFast which handles runtime type mismatches gracefully
306
+ //
307
+ // NOTE: accepted tradeoff — this does NOT further specialize on the known category
308
+ // (both-TEXT → guarded `typeof` pair → collation call, both-numeric → guarded
309
+ // inline three-way). Measured and declined; the numbers and the revisit condition
310
+ // are on `compareSqlValuesFast` in util/comparison.ts.
244
311
  const cmpToResult = buildCmpToResult(operator, plan);
245
312
  run = function runSameCategoryCompare(ctx, v1, v2) {
246
313
  if (v1 === null || v2 === null)
@@ -254,11 +321,9 @@ export function emitComparisonOp(plan, ctx) {
254
321
  run = buildGenericComparisonRun(operator, plan, collationFunc);
255
322
  noteTag = 'compare';
256
323
  }
257
- const leftExpr = emitPlanNode(plan.left, ctx);
258
- const rightExpr = emitPlanNode(plan.right, ctx);
259
324
  return {
260
- params: [leftExpr, rightExpr],
261
- run: asRun(run),
325
+ operands: [plan.left, plan.right],
326
+ run,
262
327
  note: `${plan.expression.operator}(${noteTag}${collationName !== 'BINARY' ? ` ${collationName}` : ''})`
263
328
  };
264
329
  }
@@ -297,121 +362,146 @@ function buildGenericComparisonRun(operator, plan, collationFunc) {
297
362
  return cmpToResult(compareSqlValuesFast(v1, v2, collationFunc));
298
363
  };
299
364
  }
300
- export function emitConcatOp(plan, ctx) {
365
+ export function buildConcatOpSpec(plan) {
301
366
  function run(ctx, v1, v2) {
302
367
  // SQL concatenation: NULL || anything -> NULL
303
368
  if (v1 === null || v2 === null)
304
369
  return null;
305
- // Convert both operands to strings
306
- const s1 = String(v1);
307
- const s2 = String(v2);
308
- return s1 + s2;
370
+ // Convert both operands through the shared value-to-text rule, so `b || ''`
371
+ // spells a blob or a JSON document exactly as `cast(b as text)` does.
372
+ return valueToText(v1) + valueToText(v2);
309
373
  }
310
- const leftExpr = emitPlanNode(plan.left, ctx);
311
- const rightExpr = emitPlanNode(plan.right, ctx);
312
374
  return {
313
- params: [leftExpr, rightExpr],
314
- run: asRun(run),
375
+ operands: [plan.left, plan.right],
376
+ run,
315
377
  note: '||(concat)'
316
378
  };
317
379
  }
318
- export function emitLogicalOp(plan, ctx) {
319
- // Normalize operator to uppercase for case-insensitive matching
320
- const operator = plan.expression.operator.toUpperCase();
321
- // SQL three-valued-logic combine — single source of truth shared by the eager
322
- // `run` and the deferred `runShortCircuit` path, so the two cannot diverge (the
323
- // parity tests in test/and-or-short-circuit.spec.ts guard exactly this). Coerce
324
- // non-NULL operands to a boolean using SQL truthiness (isTruthy) rather than JS
325
- // truthiness so that values like blobs and non-numeric strings agree with how
326
- // FilterNode/CASE/NOT treat them — otherwise `<blob> AND true` and a bare
327
- // `<blob>` predicate diverge.
328
- function combineLogical(v1, v2) {
380
+ /** false dominates; else NULL if any operand is NULL; else true. */
381
+ const combineAnd = (b1, b2) => {
382
+ if (b1 === false || b2 === false)
383
+ return false;
384
+ if (b1 === null || b2 === null)
385
+ return null;
386
+ return true;
387
+ };
388
+ /** true dominates; else NULL if any operand is NULL; else false. */
389
+ const combineOr = (b1, b2) => {
390
+ if (b1 === true || b2 === true)
391
+ return true;
392
+ if (b1 === null || b2 === null)
393
+ return null;
394
+ return false;
395
+ };
396
+ /** NULL with anything -> NULL; else logical inequality. */
397
+ const combineXor = (b1, b2) => {
398
+ if (b1 === null || b2 === null)
399
+ return null;
400
+ return b1 !== b2;
401
+ };
402
+ function selectLogicalCombine(operator, plan) {
403
+ switch (operator) {
404
+ case 'AND': return combineAnd;
405
+ case 'OR': return combineOr;
406
+ case 'XOR': return combineXor;
407
+ default:
408
+ quereusError(`Unsupported logical operator: ${plan.expression.operator}`, StatusCode.UNSUPPORTED, undefined, plan.expression);
409
+ }
410
+ }
411
+ /**
412
+ * The 3VL combine over raw operand values, resolved once at emit time (not per row) —
413
+ * single source of truth shared by the eager spec body and the deferred short-circuit
414
+ * path below, so the two cannot diverge (the parity tests in
415
+ * test/and-or-short-circuit.spec.ts guard exactly this).
416
+ *
417
+ * Coerces non-NULL operands to a boolean using SQL truthiness (isTruthy) rather than
418
+ * JS truthiness so that values like blobs and non-numeric strings agree with how
419
+ * FilterNode/CASE/NOT treat them — otherwise `<blob> AND true` and a bare `<blob>`
420
+ * predicate diverge.
421
+ */
422
+ function buildCombineLogical(operator, plan) {
423
+ const combine = selectLogicalCombine(operator, plan);
424
+ return function combineLogical(v1, v2) {
329
425
  const b1 = v1 === null ? null : isTruthy(v1);
330
426
  const b2 = v2 === null ? null : isTruthy(v2);
331
- switch (operator) {
332
- case 'AND': {
333
- // false dominates; else NULL if any operand is NULL; else true.
334
- if (b1 === false || b2 === false)
335
- return false;
336
- if (b1 === null || b2 === null)
337
- return null;
338
- return true;
339
- }
340
- case 'OR': {
341
- // true dominates; else NULL if any operand is NULL; else false.
342
- if (b1 === true || b2 === true)
343
- return true;
344
- if (b1 === null || b2 === null)
345
- return null;
346
- return false;
347
- }
348
- case 'XOR': {
349
- // NULL with anything -> NULL; else logical inequality.
350
- if (b1 === null || b2 === null)
351
- return null;
352
- return b1 !== b2;
353
- }
354
- default:
355
- quereusError(`Unsupported logical operator: ${plan.expression.operator}`, StatusCode.UNSUPPORTED, undefined, plan.expression);
356
- }
357
- }
358
- // Eager two-param combine. Used for XOR (both operands always required) and for
359
- // AND/OR whose right operand is cheap (no subquery) — the zero-overhead path.
427
+ return combine(b1, b2);
428
+ };
429
+ }
430
+ /**
431
+ * Short-circuit deferral gate: AND/OR whose right operand contains a subquery
432
+ * (a scalar/IN/EXISTS subquery — a relational descendant). Such an operand is emitted
433
+ * as an on-demand callback and evaluated lazily, only when the left operand does not
434
+ * already decide the result (`false AND x` → false; `true OR x` → true). This stops an
435
+ * expensive/side-effecting subquery from running on every row for nothing. Left stays
436
+ * eager — it is always needed and always evaluated first, so operand order is unchanged.
437
+ *
438
+ * NOTE: only a *subquery* right operand defers. A non-subquery expensive scalar operand
439
+ * (deeply nested arithmetic, or a volatile/slow UDF called directly rather than inside a
440
+ * subquery) is still evaluated eagerly. Such operands are rare and the dominant expensive
441
+ * case in SQL is the subquery; if a non-subquery volatile/expensive scalar operand ever
442
+ * shows up hot, extend this gate with a cost or volatility check.
443
+ */
444
+ function usesShortCircuit(plan, operator) {
445
+ return (operator === 'AND' || operator === 'OR') && hasRelationalDescendant(plan.right);
446
+ }
447
+ /**
448
+ * Eager two-operand combine. Used for XOR (both operands always required) and for
449
+ * AND/OR whose right operand is cheap (no subquery) — the zero-overhead path.
450
+ *
451
+ * `undefined` for the short-circuit form, whose right operand is a `SubProgram` param
452
+ * rather than a value and whose body returns `MaybePromise` — not a {@link ScalarOpSpec}
453
+ * body. Consumers fall back: the emitter below builds that Instruction itself, and a
454
+ * fusion consumer must decline to fuse.
455
+ */
456
+ export function buildLogicalOpSpec(plan) {
457
+ // Normalize operator to uppercase for case-insensitive matching
458
+ const operator = plan.expression.operator.toUpperCase();
459
+ if (usesShortCircuit(plan, operator))
460
+ return undefined;
461
+ const combineLogical = buildCombineLogical(operator, plan);
360
462
  function run(_ctx, v1, v2) {
361
463
  return combineLogical(v1, v2);
362
464
  }
465
+ return {
466
+ operands: [plan.left, plan.right],
467
+ run,
468
+ note: `${plan.expression.operator}(logical)`
469
+ };
470
+ }
471
+ function emitShortCircuitLogicalOp(plan, ctx) {
472
+ const operator = plan.expression.operator.toUpperCase();
473
+ const combineLogical = buildCombineLogical(operator, plan);
363
474
  const leftExpr = emitPlanNode(plan.left, ctx);
364
- // Short-circuit deferral: for AND/OR whose right operand contains a subquery
365
- // (a scalar/IN/EXISTS subquery — a relational descendant), emit the right
366
- // operand as an on-demand callback and evaluate it lazily, only when the left
367
- // operand does not already decide the result (`false AND x` → false;
368
- // `true OR x` → true). This stops an expensive/side-effecting subquery from
369
- // running on every row for nothing. Left stays eager — it is always needed
370
- // and always evaluated first, so operand order is unchanged.
371
- //
372
- // NOTE: only a *subquery* right operand defers. A non-subquery expensive
373
- // scalar operand (deeply nested arithmetic, or a volatile/slow UDF called
374
- // directly rather than inside a subquery) is still evaluated eagerly. Such
375
- // operands are rare and the dominant expensive case in SQL is the subquery;
376
- // if a non-subquery volatile/expensive scalar operand ever shows up hot,
377
- // extend this gate with a cost or volatility check.
378
- if ((operator === 'AND' || operator === 'OR') && hasRelationalDescendant(plan.right)) {
379
- const rightCall = emitCallFromPlan(plan.right, ctx);
380
- function runShortCircuit(ctx, v1, rightFn) {
381
- // Left decides — the right operand is never fetched (`false AND x` → false;
382
- // `true OR x` → true). Same SQL truthiness as the eager path.
383
- const b1 = v1 === null ? null : isTruthy(v1);
384
- if (operator === 'AND' && b1 === false)
385
- return false;
386
- if (operator === 'OR' && b1 === true)
387
- return true;
388
- // Otherwise fetch the deferred right and combine with the shared 3VL
389
- // (combineLogical) — byte-identical to the eager path. Stay synchronous
390
- // when the right sub-program resolves synchronously; only take the
391
- // microtask hop on a genuinely async subquery (see docs/runtime.md
392
- // "Avoid a per-row microtask hop on the synchronous fast path"). A
393
- // left-decides row returned above, so it never pays an async tick.
394
- const raw = rightFn(ctx);
395
- return raw instanceof Promise
396
- ? raw.then(v2 => combineLogical(v1, v2))
397
- : combineLogical(v1, raw);
398
- }
399
- return {
400
- params: [leftExpr, rightCall],
401
- run: asRun(runShortCircuit),
402
- note: `${plan.expression.operator}(logical short-circuit)`
403
- };
475
+ const rightCall = emitCallFromPlan(plan.right, ctx);
476
+ // The left-operand value that decides the result on its own, resolved once at
477
+ // emit time: `false AND x` → false, `true OR x` → true. Also the result itself.
478
+ const decidingValue = operator === 'AND' ? false : true;
479
+ function runShortCircuit(ctx, v1, rightFn) {
480
+ // Left decides — the right operand is never fetched (`false AND x` → false;
481
+ // `true OR x` → true). Same SQL truthiness as the eager path.
482
+ const b1 = v1 === null ? null : isTruthy(v1);
483
+ if (b1 === decidingValue)
484
+ return decidingValue;
485
+ // Otherwise fetch the deferred right and combine with the shared 3VL
486
+ // (combineLogical) — byte-identical to the eager path. Stay synchronous
487
+ // when the right sub-program resolves synchronously; only take the
488
+ // microtask hop on a genuinely async subquery (see docs/runtime.md
489
+ // "Avoid a per-row microtask hop on the synchronous fast path"). A
490
+ // left-decides row returned above, so it never pays an async tick.
491
+ const raw = rightFn(ctx);
492
+ return raw instanceof Promise
493
+ ? raw.then(v2 => combineLogical(v1, v2))
494
+ : combineLogical(v1, raw);
404
495
  }
405
- const rightExpr = emitPlanNode(plan.right, ctx);
406
496
  return {
407
- params: [leftExpr, rightExpr],
408
- run: asRun(run),
409
- note: `${plan.expression.operator}(logical)`
497
+ params: [leftExpr, rightCall],
498
+ run: asRun(runShortCircuit),
499
+ note: `${plan.expression.operator}(logical short-circuit)`
410
500
  };
411
501
  }
412
502
  /**
413
503
  * If `node` is a literal-constant, non-NULL pattern, return the exact string the
414
- * per-row path would derive from it (`String(value)`), otherwise undefined.
504
+ * per-row path would derive from it ({@link valueToText}), otherwise undefined.
415
505
  * A NULL literal, a not-yet-resolved Promise value, or any non-literal node
416
506
  * falls through to the dynamic (memoized) per-row path so semantics are
417
507
  * unchanged. Cast/collate-wrapped literals are intentionally NOT unwrapped —
@@ -423,14 +513,14 @@ function constLikePattern(node) {
423
513
  const value = node.expression.value;
424
514
  if (value === null || value === undefined || value instanceof Promise)
425
515
  return undefined;
426
- return String(value);
516
+ return valueToText(value);
427
517
  }
428
- export function emitLikeOp(plan, ctx) {
429
- const leftExpr = emitPlanNode(plan.left, ctx);
518
+ export function buildLikeOpSpec(plan) {
430
519
  // Fast path: the pattern operand is a literal constant. Compile the matcher
431
520
  // once here at emit time and capture it in the closure, so no per-row compile
432
- // or cache lookup happens at all. The literal operand is not emitted as a
433
- // param since its value is already baked into `matcher`.
521
+ // or cache lookup happens at all. The literal operand is NOT declared as an
522
+ // operand since its value is already baked into `matcher` — this spec's arity
523
+ // is one, while the plan node still has two children.
434
524
  const constPattern = constLikePattern(plan.right);
435
525
  if (constPattern !== undefined) {
436
526
  const matcher = compileLikeMatcher(constPattern);
@@ -438,11 +528,11 @@ export function emitLikeOp(plan, ctx) {
438
528
  // text LIKE <const>: NULL text → NULL, else run the pre-compiled matcher.
439
529
  if (text === null)
440
530
  return null;
441
- return matcher(String(text));
531
+ return matcher(valueToText(text));
442
532
  }
443
533
  return {
444
- params: [leftExpr],
445
- run: asRun(runConstPattern),
534
+ operands: [plan.left],
535
+ run: runConstPattern,
446
536
  note: 'LIKE(like-const)'
447
537
  };
448
538
  }
@@ -452,15 +542,16 @@ export function emitLikeOp(plan, ctx) {
452
542
  if (text === null || pattern === null) {
453
543
  return null;
454
544
  }
455
- // Convert both operands to strings and perform LIKE matching (memoized compile).
456
- const textStr = String(text);
457
- const patternStr = String(pattern);
545
+ // Convert both operands through the shared value-to-text rule and perform LIKE
546
+ // matching (memoized compile). Must agree with `constLikePattern` above, or the
547
+ // constant-pattern fast path starts answering differently from this one.
548
+ const textStr = valueToText(text);
549
+ const patternStr = valueToText(pattern);
458
550
  return simpleLike(patternStr, textStr);
459
551
  }
460
- const rightExpr = emitPlanNode(plan.right, ctx);
461
552
  return {
462
- params: [leftExpr, rightExpr],
463
- run: asRun(run),
553
+ operands: [plan.left, plan.right],
554
+ run,
464
555
  note: 'LIKE(like)'
465
556
  };
466
557
  }