@quereus/quereus 4.3.1 → 4.3.2

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 (721) hide show
  1. package/README.md +25 -7
  2. package/dist/src/common/types.d.ts +9 -59
  3. package/dist/src/common/types.d.ts.map +1 -1
  4. package/dist/src/common/types.js.map +1 -1
  5. package/dist/src/core/database-assertions.d.ts +2 -2
  6. package/dist/src/core/database-assertions.d.ts.map +1 -1
  7. package/dist/src/core/database-assertions.js +4 -3
  8. package/dist/src/core/database-assertions.js.map +1 -1
  9. package/dist/src/core/database-external-changes.d.ts +1 -1
  10. package/dist/src/core/database-external-changes.js +1 -1
  11. package/dist/src/core/database-internal.d.ts.map +1 -1
  12. package/dist/src/core/database-materialized-views-analysis.d.ts +199 -0
  13. package/dist/src/core/database-materialized-views-analysis.d.ts.map +1 -0
  14. package/dist/src/core/database-materialized-views-analysis.js +502 -0
  15. package/dist/src/core/database-materialized-views-analysis.js.map +1 -0
  16. package/dist/src/core/database-materialized-views-apply.d.ts +280 -0
  17. package/dist/src/core/database-materialized-views-apply.d.ts.map +1 -0
  18. package/dist/src/core/database-materialized-views-apply.js +624 -0
  19. package/dist/src/core/database-materialized-views-apply.js.map +1 -0
  20. package/dist/src/core/database-materialized-views-plan-builders.d.ts +203 -0
  21. package/dist/src/core/database-materialized-views-plan-builders.d.ts.map +1 -0
  22. package/dist/src/core/database-materialized-views-plan-builders.js +1084 -0
  23. package/dist/src/core/database-materialized-views-plan-builders.js.map +1 -0
  24. package/dist/src/core/database-materialized-views-plans.d.ts +417 -0
  25. package/dist/src/core/database-materialized-views-plans.d.ts.map +1 -0
  26. package/dist/src/core/database-materialized-views-plans.js +13 -0
  27. package/dist/src/core/database-materialized-views-plans.js.map +1 -0
  28. package/dist/src/core/database-materialized-views.d.ts +11 -819
  29. package/dist/src/core/database-materialized-views.d.ts.map +1 -1
  30. package/dist/src/core/database-materialized-views.js +60 -2069
  31. package/dist/src/core/database-materialized-views.js.map +1 -1
  32. package/dist/src/core/database-transaction.d.ts +10 -1
  33. package/dist/src/core/database-transaction.d.ts.map +1 -1
  34. package/dist/src/core/database-transaction.js +16 -4
  35. package/dist/src/core/database-transaction.js.map +1 -1
  36. package/dist/src/core/database-watchers.d.ts.map +1 -1
  37. package/dist/src/core/database-watchers.js +2 -1
  38. package/dist/src/core/database-watchers.js.map +1 -1
  39. package/dist/src/core/database.d.ts +99 -13
  40. package/dist/src/core/database.d.ts.map +1 -1
  41. package/dist/src/core/database.js +250 -33
  42. package/dist/src/core/database.js.map +1 -1
  43. package/dist/src/core/derived-row-validator.d.ts +1 -1
  44. package/dist/src/core/derived-row-validator.d.ts.map +1 -1
  45. package/dist/src/core/derived-row-validator.js +5 -3
  46. package/dist/src/core/derived-row-validator.js.map +1 -1
  47. package/dist/src/core/statement.d.ts +8 -0
  48. package/dist/src/core/statement.d.ts.map +1 -1
  49. package/dist/src/core/statement.js +98 -18
  50. package/dist/src/core/statement.js.map +1 -1
  51. package/dist/src/emit/ast-stringify.d.ts.map +1 -1
  52. package/dist/src/emit/ast-stringify.js +24 -13
  53. package/dist/src/emit/ast-stringify.js.map +1 -1
  54. package/dist/src/func/builtins/explain.d.ts.map +1 -1
  55. package/dist/src/func/builtins/explain.js +2 -1
  56. package/dist/src/func/builtins/explain.js.map +1 -1
  57. package/dist/src/func/builtins/json.d.ts.map +1 -1
  58. package/dist/src/func/builtins/json.js +2 -1
  59. package/dist/src/func/builtins/json.js.map +1 -1
  60. package/dist/src/func/builtins/mutation.d.ts.map +1 -1
  61. package/dist/src/func/builtins/mutation.js +3 -2
  62. package/dist/src/func/builtins/mutation.js.map +1 -1
  63. package/dist/src/index.d.ts +16 -6
  64. package/dist/src/index.d.ts.map +1 -1
  65. package/dist/src/index.js +33 -7
  66. package/dist/src/index.js.map +1 -1
  67. package/dist/src/parser/ast.d.ts +2 -2
  68. package/dist/src/parser/lexer.d.ts +11 -1
  69. package/dist/src/parser/lexer.d.ts.map +1 -1
  70. package/dist/src/parser/lexer.js +40 -34
  71. package/dist/src/parser/lexer.js.map +1 -1
  72. package/dist/src/parser/parser.d.ts.map +1 -1
  73. package/dist/src/parser/parser.js +16 -9
  74. package/dist/src/parser/parser.js.map +1 -1
  75. package/dist/src/planner/analysis/authored-inverse.js +1 -1
  76. package/dist/src/planner/analysis/comparison-collation.d.ts +52 -0
  77. package/dist/src/planner/analysis/comparison-collation.d.ts.map +1 -1
  78. package/dist/src/planner/analysis/comparison-collation.js +84 -7
  79. package/dist/src/planner/analysis/comparison-collation.js.map +1 -1
  80. package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
  81. package/dist/src/planner/analysis/constraint-extractor.js +64 -17
  82. package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
  83. package/dist/src/planner/analysis/coverage-prover.d.ts +1 -1
  84. package/dist/src/planner/analysis/coverage-prover.js +1 -1
  85. package/dist/src/planner/analysis/sat-checker.d.ts +8 -1
  86. package/dist/src/planner/analysis/sat-checker.d.ts.map +1 -1
  87. package/dist/src/planner/analysis/sat-checker.js +117 -22
  88. package/dist/src/planner/analysis/sat-checker.js.map +1 -1
  89. package/dist/src/planner/analysis/scalar-invertibility.d.ts +13 -4
  90. package/dist/src/planner/analysis/scalar-invertibility.d.ts.map +1 -1
  91. package/dist/src/planner/analysis/scalar-invertibility.js +12 -3
  92. package/dist/src/planner/analysis/scalar-invertibility.js.map +1 -1
  93. package/dist/src/planner/analysis/update-lineage.d.ts +1 -1
  94. package/dist/src/planner/analysis/update-lineage.js +1 -1
  95. package/dist/src/planner/analysis/update-lineage.js.map +1 -1
  96. package/dist/src/planner/analysis/view-complement.d.ts +2 -2
  97. package/dist/src/planner/building/delete.js +2 -2
  98. package/dist/src/planner/building/delete.js.map +1 -1
  99. package/dist/src/planner/building/dml-target.d.ts +2 -2
  100. package/dist/src/planner/building/dml-target.js +2 -2
  101. package/dist/src/planner/building/insert.d.ts.map +1 -1
  102. package/dist/src/planner/building/insert.js +53 -2
  103. package/dist/src/planner/building/insert.js.map +1 -1
  104. package/dist/src/planner/building/schema-resolution.d.ts +3 -0
  105. package/dist/src/planner/building/schema-resolution.d.ts.map +1 -1
  106. package/dist/src/planner/building/schema-resolution.js +4 -5
  107. package/dist/src/planner/building/schema-resolution.js.map +1 -1
  108. package/dist/src/planner/building/select.js +3 -3
  109. package/dist/src/planner/building/select.js.map +1 -1
  110. package/dist/src/planner/building/update.js +2 -2
  111. package/dist/src/planner/building/update.js.map +1 -1
  112. package/dist/src/planner/building/view-mutation-builder.js +7 -7
  113. package/dist/src/planner/building/view-mutation-builder.js.map +1 -1
  114. package/dist/src/planner/cache/materialization-advisory.d.ts +1 -2
  115. package/dist/src/planner/cache/materialization-advisory.d.ts.map +1 -1
  116. package/dist/src/planner/cache/materialization-advisory.js +21 -14
  117. package/dist/src/planner/cache/materialization-advisory.js.map +1 -1
  118. package/dist/src/planner/cache/reference-graph.d.ts +14 -1
  119. package/dist/src/planner/cache/reference-graph.d.ts.map +1 -1
  120. package/dist/src/planner/cache/reference-graph.js +26 -26
  121. package/dist/src/planner/cache/reference-graph.js.map +1 -1
  122. package/dist/src/planner/cost/index.d.ts +7 -3
  123. package/dist/src/planner/cost/index.d.ts.map +1 -1
  124. package/dist/src/planner/cost/index.js.map +1 -1
  125. package/dist/src/planner/debug.d.ts +3 -2
  126. package/dist/src/planner/debug.d.ts.map +1 -1
  127. package/dist/src/planner/debug.js +9 -3
  128. package/dist/src/planner/debug.js.map +1 -1
  129. package/dist/src/planner/framework/characteristics.d.ts +46 -19
  130. package/dist/src/planner/framework/characteristics.d.ts.map +1 -1
  131. package/dist/src/planner/framework/characteristics.js +50 -134
  132. package/dist/src/planner/framework/characteristics.js.map +1 -1
  133. package/dist/src/planner/framework/context.d.ts +8 -44
  134. package/dist/src/planner/framework/context.d.ts.map +1 -1
  135. package/dist/src/planner/framework/context.js +0 -91
  136. package/dist/src/planner/framework/context.js.map +1 -1
  137. package/dist/src/planner/framework/pass.d.ts +7 -0
  138. package/dist/src/planner/framework/pass.d.ts.map +1 -1
  139. package/dist/src/planner/framework/pass.js +84 -0
  140. package/dist/src/planner/framework/pass.js.map +1 -1
  141. package/dist/src/planner/framework/registry.d.ts +13 -42
  142. package/dist/src/planner/framework/registry.d.ts.map +1 -1
  143. package/dist/src/planner/framework/registry.js +14 -182
  144. package/dist/src/planner/framework/registry.js.map +1 -1
  145. package/dist/src/planner/mutation/backward-body.d.ts +2 -2
  146. package/dist/src/planner/mutation/cte-flatten.js +1 -1
  147. package/dist/src/planner/mutation/decomposition.d.ts +2 -2
  148. package/dist/src/planner/mutation/decomposition.js +1 -1
  149. package/dist/src/planner/mutation/lens-enforcement.d.ts.map +1 -1
  150. package/dist/src/planner/mutation/lens-enforcement.js +2 -1
  151. package/dist/src/planner/mutation/lens-enforcement.js.map +1 -1
  152. package/dist/src/planner/mutation/multi-source.d.ts +8 -8
  153. package/dist/src/planner/mutation/multi-source.js +11 -11
  154. package/dist/src/planner/mutation/multi-source.js.map +1 -1
  155. package/dist/src/planner/mutation/propagate.d.ts +1 -1
  156. package/dist/src/planner/mutation/propagate.js +1 -1
  157. package/dist/src/planner/mutation/scope-transform.d.ts +2 -2
  158. package/dist/src/planner/mutation/scope-transform.js +3 -3
  159. package/dist/src/planner/mutation/scope-transform.js.map +1 -1
  160. package/dist/src/planner/mutation/set-op.d.ts +1 -1
  161. package/dist/src/planner/mutation/set-op.d.ts.map +1 -1
  162. package/dist/src/planner/mutation/set-op.js +3 -2
  163. package/dist/src/planner/mutation/set-op.js.map +1 -1
  164. package/dist/src/planner/mutation/single-source.d.ts +3 -3
  165. package/dist/src/planner/mutation/single-source.js +9 -9
  166. package/dist/src/planner/mutation/single-source.js.map +1 -1
  167. package/dist/src/planner/nodes/aggregate-function.d.ts +3 -1
  168. package/dist/src/planner/nodes/aggregate-function.d.ts.map +1 -1
  169. package/dist/src/planner/nodes/aggregate-function.js +1 -0
  170. package/dist/src/planner/nodes/aggregate-function.js.map +1 -1
  171. package/dist/src/planner/nodes/aggregate-node.d.ts +1 -0
  172. package/dist/src/planner/nodes/aggregate-node.d.ts.map +1 -1
  173. package/dist/src/planner/nodes/aggregate-node.js +8 -1
  174. package/dist/src/planner/nodes/aggregate-node.js.map +1 -1
  175. package/dist/src/planner/nodes/alias-node.d.ts.map +1 -1
  176. package/dist/src/planner/nodes/alias-node.js +3 -1
  177. package/dist/src/planner/nodes/alias-node.js.map +1 -1
  178. package/dist/src/planner/nodes/asof-scan-node.d.ts.map +1 -1
  179. package/dist/src/planner/nodes/asof-scan-node.js +3 -2
  180. package/dist/src/planner/nodes/asof-scan-node.js.map +1 -1
  181. package/dist/src/planner/nodes/asserted-keys-node.d.ts +1 -1
  182. package/dist/src/planner/nodes/asserted-keys-node.d.ts.map +1 -1
  183. package/dist/src/planner/nodes/asserted-keys-node.js +5 -2
  184. package/dist/src/planner/nodes/asserted-keys-node.js.map +1 -1
  185. package/dist/src/planner/nodes/async-gather-node.d.ts.map +1 -1
  186. package/dist/src/planner/nodes/async-gather-node.js +3 -1
  187. package/dist/src/planner/nodes/async-gather-node.js.map +1 -1
  188. package/dist/src/planner/nodes/block.js +3 -3
  189. package/dist/src/planner/nodes/block.js.map +1 -1
  190. package/dist/src/planner/nodes/bloom-join-node.d.ts +2 -0
  191. package/dist/src/planner/nodes/bloom-join-node.d.ts.map +1 -1
  192. package/dist/src/planner/nodes/bloom-join-node.js +5 -1
  193. package/dist/src/planner/nodes/bloom-join-node.js.map +1 -1
  194. package/dist/src/planner/nodes/cache-node.d.ts +1 -0
  195. package/dist/src/planner/nodes/cache-node.d.ts.map +1 -1
  196. package/dist/src/planner/nodes/cache-node.js +1 -0
  197. package/dist/src/planner/nodes/cache-node.js.map +1 -1
  198. package/dist/src/planner/nodes/cte-node.d.ts +1 -0
  199. package/dist/src/planner/nodes/cte-node.d.ts.map +1 -1
  200. package/dist/src/planner/nodes/cte-node.js +4 -1
  201. package/dist/src/planner/nodes/cte-node.js.map +1 -1
  202. package/dist/src/planner/nodes/distinct-node.js +3 -3
  203. package/dist/src/planner/nodes/distinct-node.js.map +1 -1
  204. package/dist/src/planner/nodes/dml-executor-node.d.ts +21 -0
  205. package/dist/src/planner/nodes/dml-executor-node.d.ts.map +1 -1
  206. package/dist/src/planner/nodes/dml-executor-node.js.map +1 -1
  207. package/dist/src/planner/nodes/envelope-scan-node.d.ts +1 -1
  208. package/dist/src/planner/nodes/envelope-scan-node.js +1 -1
  209. package/dist/src/planner/nodes/fanout-lookup-join-node.d.ts.map +1 -1
  210. package/dist/src/planner/nodes/fanout-lookup-join-node.js +4 -2
  211. package/dist/src/planner/nodes/fanout-lookup-join-node.js.map +1 -1
  212. package/dist/src/planner/nodes/filter.d.ts +10 -2
  213. package/dist/src/planner/nodes/filter.d.ts.map +1 -1
  214. package/dist/src/planner/nodes/filter.js +81 -14
  215. package/dist/src/planner/nodes/filter.js.map +1 -1
  216. package/dist/src/planner/nodes/hash-aggregate.d.ts +12 -1
  217. package/dist/src/planner/nodes/hash-aggregate.d.ts.map +1 -1
  218. package/dist/src/planner/nodes/hash-aggregate.js +28 -1
  219. package/dist/src/planner/nodes/hash-aggregate.js.map +1 -1
  220. package/dist/src/planner/nodes/internal-recursive-cte-ref-node.d.ts +3 -1
  221. package/dist/src/planner/nodes/internal-recursive-cte-ref-node.d.ts.map +1 -1
  222. package/dist/src/planner/nodes/internal-recursive-cte-ref-node.js +1 -0
  223. package/dist/src/planner/nodes/internal-recursive-cte-ref-node.js.map +1 -1
  224. package/dist/src/planner/nodes/join-node.d.ts +3 -1
  225. package/dist/src/planner/nodes/join-node.d.ts.map +1 -1
  226. package/dist/src/planner/nodes/join-node.js +5 -5
  227. package/dist/src/planner/nodes/join-node.js.map +1 -1
  228. package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts.map +1 -1
  229. package/dist/src/planner/nodes/lens-auxiliary-access-node.js +4 -1
  230. package/dist/src/planner/nodes/lens-auxiliary-access-node.js.map +1 -1
  231. package/dist/src/planner/nodes/limit-offset.d.ts +1 -0
  232. package/dist/src/planner/nodes/limit-offset.d.ts.map +1 -1
  233. package/dist/src/planner/nodes/limit-offset.js +4 -3
  234. package/dist/src/planner/nodes/limit-offset.js.map +1 -1
  235. package/dist/src/planner/nodes/merge-join-node.d.ts +2 -0
  236. package/dist/src/planner/nodes/merge-join-node.d.ts.map +1 -1
  237. package/dist/src/planner/nodes/merge-join-node.js +6 -1
  238. package/dist/src/planner/nodes/merge-join-node.js.map +1 -1
  239. package/dist/src/planner/nodes/ordinal-slice-node.d.ts.map +1 -1
  240. package/dist/src/planner/nodes/ordinal-slice-node.js +3 -1
  241. package/dist/src/planner/nodes/ordinal-slice-node.js.map +1 -1
  242. package/dist/src/planner/nodes/plan-node.d.ts +68 -5
  243. package/dist/src/planner/nodes/plan-node.d.ts.map +1 -1
  244. package/dist/src/planner/nodes/plan-node.js +137 -24
  245. package/dist/src/planner/nodes/plan-node.js.map +1 -1
  246. package/dist/src/planner/nodes/project-node.d.ts +3 -2
  247. package/dist/src/planner/nodes/project-node.d.ts.map +1 -1
  248. package/dist/src/planner/nodes/project-node.js +1 -0
  249. package/dist/src/planner/nodes/project-node.js.map +1 -1
  250. package/dist/src/planner/nodes/recursive-cte-node.d.ts.map +1 -1
  251. package/dist/src/planner/nodes/recursive-cte-node.js +8 -2
  252. package/dist/src/planner/nodes/recursive-cte-node.js.map +1 -1
  253. package/dist/src/planner/nodes/reference.d.ts +5 -1
  254. package/dist/src/planner/nodes/reference.d.ts.map +1 -1
  255. package/dist/src/planner/nodes/reference.js +3 -0
  256. package/dist/src/planner/nodes/reference.js.map +1 -1
  257. package/dist/src/planner/nodes/remote-query-node.d.ts.map +1 -1
  258. package/dist/src/planner/nodes/remote-query-node.js +3 -1
  259. package/dist/src/planner/nodes/remote-query-node.js.map +1 -1
  260. package/dist/src/planner/nodes/retrieve-node.d.ts.map +1 -1
  261. package/dist/src/planner/nodes/retrieve-node.js +3 -1
  262. package/dist/src/planner/nodes/retrieve-node.js.map +1 -1
  263. package/dist/src/planner/nodes/set-operation-node.d.ts.map +1 -1
  264. package/dist/src/planner/nodes/set-operation-node.js +3 -1
  265. package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
  266. package/dist/src/planner/nodes/sink-node.d.ts.map +1 -1
  267. package/dist/src/planner/nodes/sink-node.js +3 -1
  268. package/dist/src/planner/nodes/sink-node.js.map +1 -1
  269. package/dist/src/planner/nodes/sort.d.ts +2 -1
  270. package/dist/src/planner/nodes/sort.d.ts.map +1 -1
  271. package/dist/src/planner/nodes/sort.js +9 -5
  272. package/dist/src/planner/nodes/sort.js.map +1 -1
  273. package/dist/src/planner/nodes/stream-aggregate.d.ts +12 -1
  274. package/dist/src/planner/nodes/stream-aggregate.d.ts.map +1 -1
  275. package/dist/src/planner/nodes/stream-aggregate.js +32 -3
  276. package/dist/src/planner/nodes/stream-aggregate.js.map +1 -1
  277. package/dist/src/planner/nodes/table-access-nodes.d.ts +2 -1
  278. package/dist/src/planner/nodes/table-access-nodes.d.ts.map +1 -1
  279. package/dist/src/planner/nodes/table-access-nodes.js +2 -0
  280. package/dist/src/planner/nodes/table-access-nodes.js.map +1 -1
  281. package/dist/src/planner/nodes/view-mutation-node.d.ts +2 -2
  282. package/dist/src/planner/nodes/view-mutation-node.d.ts.map +1 -1
  283. package/dist/src/planner/nodes/view-mutation-node.js +5 -1
  284. package/dist/src/planner/nodes/view-mutation-node.js.map +1 -1
  285. package/dist/src/planner/nodes/window-function.d.ts +3 -1
  286. package/dist/src/planner/nodes/window-function.d.ts.map +1 -1
  287. package/dist/src/planner/nodes/window-function.js +1 -0
  288. package/dist/src/planner/nodes/window-function.js.map +1 -1
  289. package/dist/src/planner/optimizer.d.ts +52 -8
  290. package/dist/src/planner/optimizer.d.ts.map +1 -1
  291. package/dist/src/planner/optimizer.js +885 -894
  292. package/dist/src/planner/optimizer.js.map +1 -1
  293. package/dist/src/planner/rules/access/rule-select-access-path.d.ts.map +1 -1
  294. package/dist/src/planner/rules/access/rule-select-access-path.js +235 -202
  295. package/dist/src/planner/rules/access/rule-select-access-path.js.map +1 -1
  296. package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts +2 -2
  297. package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js +2 -2
  298. package/dist/src/planner/rules/cache/rule-scalar-cse.d.ts +1 -1
  299. package/dist/src/planner/rules/cache/rule-scalar-cse.js +1 -1
  300. package/dist/src/planner/rules/join/rule-fanout-batched-outer.d.ts +1 -1
  301. package/dist/src/planner/rules/join/rule-fanout-batched-outer.js +1 -1
  302. package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.d.ts +1 -1
  303. package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js +1 -1
  304. package/dist/src/planner/rules/join/rule-join-elimination.d.ts +1 -1
  305. package/dist/src/planner/rules/join/rule-join-elimination.js +1 -1
  306. package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js +5 -6
  307. package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js.map +1 -1
  308. package/dist/src/planner/rules/predicate/rule-empty-relation-folding.d.ts +1 -1
  309. package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js +1 -1
  310. package/dist/src/planner/rules/predicate/rule-filter-contradiction.d.ts +2 -2
  311. package/dist/src/planner/rules/predicate/rule-filter-contradiction.d.ts.map +1 -1
  312. package/dist/src/planner/rules/predicate/rule-filter-contradiction.js +4 -3
  313. package/dist/src/planner/rules/predicate/rule-filter-contradiction.js.map +1 -1
  314. package/dist/src/planner/rules/predicate/rule-filter-selectivity.d.ts +19 -0
  315. package/dist/src/planner/rules/predicate/rule-filter-selectivity.d.ts.map +1 -0
  316. package/dist/src/planner/rules/predicate/rule-filter-selectivity.js +48 -0
  317. package/dist/src/planner/rules/predicate/rule-filter-selectivity.js.map +1 -0
  318. package/dist/src/planner/rules/predicate/rule-sargable-range-rewrite.js +12 -1
  319. package/dist/src/planner/rules/predicate/rule-sargable-range-rewrite.js.map +1 -1
  320. package/dist/src/planner/rules/retrieve/rule-grow-retrieve.d.ts.map +1 -1
  321. package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js +22 -20
  322. package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js.map +1 -1
  323. package/dist/src/planner/rules/shared/index-style-context.d.ts +39 -0
  324. package/dist/src/planner/rules/shared/index-style-context.d.ts.map +1 -0
  325. package/dist/src/planner/rules/shared/index-style-context.js +26 -0
  326. package/dist/src/planner/rules/shared/index-style-context.js.map +1 -0
  327. package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.d.ts +1 -1
  328. package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.js +1 -1
  329. package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.d.ts +1 -1
  330. package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js +1 -1
  331. package/dist/src/planner/stats/analyze.d.ts.map +1 -1
  332. package/dist/src/planner/stats/analyze.js +2 -20
  333. package/dist/src/planner/stats/analyze.js.map +1 -1
  334. package/dist/src/planner/stats/histogram.d.ts.map +1 -1
  335. package/dist/src/planner/stats/histogram.js +4 -0
  336. package/dist/src/planner/stats/histogram.js.map +1 -1
  337. package/dist/src/planner/util/fd-utils.d.ts +2 -3
  338. package/dist/src/planner/util/fd-utils.d.ts.map +1 -1
  339. package/dist/src/planner/util/fd-utils.js +2 -3
  340. package/dist/src/planner/util/fd-utils.js.map +1 -1
  341. package/dist/src/planner/validation/plan-validator.d.ts +16 -0
  342. package/dist/src/planner/validation/plan-validator.d.ts.map +1 -1
  343. package/dist/src/planner/validation/plan-validator.js +36 -0
  344. package/dist/src/planner/validation/plan-validator.js.map +1 -1
  345. package/dist/src/runtime/async-util.d.ts +26 -0
  346. package/dist/src/runtime/async-util.d.ts.map +1 -1
  347. package/dist/src/runtime/async-util.js +139 -55
  348. package/dist/src/runtime/async-util.js.map +1 -1
  349. package/dist/src/runtime/context-helpers.d.ts +32 -4
  350. package/dist/src/runtime/context-helpers.d.ts.map +1 -1
  351. package/dist/src/runtime/context-helpers.js +26 -9
  352. package/dist/src/runtime/context-helpers.js.map +1 -1
  353. package/dist/src/runtime/deferred-constraint-queue.d.ts.map +1 -1
  354. package/dist/src/runtime/deferred-constraint-queue.js +2 -1
  355. package/dist/src/runtime/deferred-constraint-queue.js.map +1 -1
  356. package/dist/src/runtime/emission-context.d.ts +27 -3
  357. package/dist/src/runtime/emission-context.d.ts.map +1 -1
  358. package/dist/src/runtime/emission-context.js +38 -7
  359. package/dist/src/runtime/emission-context.js.map +1 -1
  360. package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
  361. package/dist/src/runtime/emit/add-constraint.js +7 -1
  362. package/dist/src/runtime/emit/add-constraint.js.map +1 -1
  363. package/dist/src/runtime/emit/aggregate.d.ts.map +1 -1
  364. package/dist/src/runtime/emit/aggregate.js +17 -13
  365. package/dist/src/runtime/emit/aggregate.js.map +1 -1
  366. package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
  367. package/dist/src/runtime/emit/alter-table.js +98 -9
  368. package/dist/src/runtime/emit/alter-table.js.map +1 -1
  369. package/dist/src/runtime/emit/analyze.d.ts.map +1 -1
  370. package/dist/src/runtime/emit/analyze.js +2 -1
  371. package/dist/src/runtime/emit/analyze.js.map +1 -1
  372. package/dist/src/runtime/emit/asof-scan.d.ts.map +1 -1
  373. package/dist/src/runtime/emit/asof-scan.js +13 -10
  374. package/dist/src/runtime/emit/asof-scan.js.map +1 -1
  375. package/dist/src/runtime/emit/async-gather.d.ts.map +1 -1
  376. package/dist/src/runtime/emit/async-gather.js +4 -3
  377. package/dist/src/runtime/emit/async-gather.js.map +1 -1
  378. package/dist/src/runtime/emit/between.d.ts.map +1 -1
  379. package/dist/src/runtime/emit/between.js +2 -1
  380. package/dist/src/runtime/emit/between.js.map +1 -1
  381. package/dist/src/runtime/emit/binary.d.ts.map +1 -1
  382. package/dist/src/runtime/emit/binary.js +122 -14
  383. package/dist/src/runtime/emit/binary.js.map +1 -1
  384. package/dist/src/runtime/emit/block.d.ts.map +1 -1
  385. package/dist/src/runtime/emit/block.js +2 -1
  386. package/dist/src/runtime/emit/block.js.map +1 -1
  387. package/dist/src/runtime/emit/bloom-join.d.ts.map +1 -1
  388. package/dist/src/runtime/emit/bloom-join.js +20 -8
  389. package/dist/src/runtime/emit/bloom-join.js.map +1 -1
  390. package/dist/src/runtime/emit/cache.d.ts.map +1 -1
  391. package/dist/src/runtime/emit/cache.js +2 -1
  392. package/dist/src/runtime/emit/cache.js.map +1 -1
  393. package/dist/src/runtime/emit/case.d.ts.map +1 -1
  394. package/dist/src/runtime/emit/case.js +66 -45
  395. package/dist/src/runtime/emit/case.js.map +1 -1
  396. package/dist/src/runtime/emit/cast.d.ts.map +1 -1
  397. package/dist/src/runtime/emit/cast.js +2 -1
  398. package/dist/src/runtime/emit/cast.js.map +1 -1
  399. package/dist/src/runtime/emit/constraint-check.d.ts.map +1 -1
  400. package/dist/src/runtime/emit/constraint-check.js +13 -7
  401. package/dist/src/runtime/emit/constraint-check.js.map +1 -1
  402. package/dist/src/runtime/emit/create-assertion.d.ts.map +1 -1
  403. package/dist/src/runtime/emit/create-assertion.js +2 -1
  404. package/dist/src/runtime/emit/create-assertion.js.map +1 -1
  405. package/dist/src/runtime/emit/create-index.d.ts.map +1 -1
  406. package/dist/src/runtime/emit/create-index.js +12 -1
  407. package/dist/src/runtime/emit/create-index.js.map +1 -1
  408. package/dist/src/runtime/emit/create-table.d.ts.map +1 -1
  409. package/dist/src/runtime/emit/create-table.js +12 -1
  410. package/dist/src/runtime/emit/create-table.js.map +1 -1
  411. package/dist/src/runtime/emit/cte-reference.d.ts.map +1 -1
  412. package/dist/src/runtime/emit/cte-reference.js +2 -1
  413. package/dist/src/runtime/emit/cte-reference.js.map +1 -1
  414. package/dist/src/runtime/emit/cte.d.ts.map +1 -1
  415. package/dist/src/runtime/emit/cte.js +2 -1
  416. package/dist/src/runtime/emit/cte.js.map +1 -1
  417. package/dist/src/runtime/emit/ddl-transaction-policy.d.ts +50 -0
  418. package/dist/src/runtime/emit/ddl-transaction-policy.d.ts.map +1 -0
  419. package/dist/src/runtime/emit/ddl-transaction-policy.js +69 -0
  420. package/dist/src/runtime/emit/ddl-transaction-policy.js.map +1 -0
  421. package/dist/src/runtime/emit/delete.d.ts.map +1 -1
  422. package/dist/src/runtime/emit/delete.js +2 -1
  423. package/dist/src/runtime/emit/delete.js.map +1 -1
  424. package/dist/src/runtime/emit/distinct.d.ts.map +1 -1
  425. package/dist/src/runtime/emit/distinct.js +2 -1
  426. package/dist/src/runtime/emit/distinct.js.map +1 -1
  427. package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
  428. package/dist/src/runtime/emit/dml-executor.js +70 -31
  429. package/dist/src/runtime/emit/dml-executor.js.map +1 -1
  430. package/dist/src/runtime/emit/drop-assertion.d.ts.map +1 -1
  431. package/dist/src/runtime/emit/drop-assertion.js +2 -1
  432. package/dist/src/runtime/emit/drop-assertion.js.map +1 -1
  433. package/dist/src/runtime/emit/drop-index.d.ts.map +1 -1
  434. package/dist/src/runtime/emit/drop-index.js +17 -1
  435. package/dist/src/runtime/emit/drop-index.js.map +1 -1
  436. package/dist/src/runtime/emit/drop-table.d.ts.map +1 -1
  437. package/dist/src/runtime/emit/drop-table.js +11 -1
  438. package/dist/src/runtime/emit/drop-table.js.map +1 -1
  439. package/dist/src/runtime/emit/eager-prefetch.d.ts.map +1 -1
  440. package/dist/src/runtime/emit/eager-prefetch.js +2 -1
  441. package/dist/src/runtime/emit/eager-prefetch.js.map +1 -1
  442. package/dist/src/runtime/emit/envelope-scan.d.ts.map +1 -1
  443. package/dist/src/runtime/emit/envelope-scan.js +2 -1
  444. package/dist/src/runtime/emit/envelope-scan.js.map +1 -1
  445. package/dist/src/runtime/emit/fanout-lookup-join.d.ts.map +1 -1
  446. package/dist/src/runtime/emit/fanout-lookup-join.js +3 -2
  447. package/dist/src/runtime/emit/fanout-lookup-join.js.map +1 -1
  448. package/dist/src/runtime/emit/filter.d.ts.map +1 -1
  449. package/dist/src/runtime/emit/filter.js +8 -3
  450. package/dist/src/runtime/emit/filter.js.map +1 -1
  451. package/dist/src/runtime/emit/hash-aggregate.d.ts.map +1 -1
  452. package/dist/src/runtime/emit/hash-aggregate.js +16 -11
  453. package/dist/src/runtime/emit/hash-aggregate.js.map +1 -1
  454. package/dist/src/runtime/emit/insert.d.ts.map +1 -1
  455. package/dist/src/runtime/emit/insert.js +2 -1
  456. package/dist/src/runtime/emit/insert.js.map +1 -1
  457. package/dist/src/runtime/emit/join.d.ts.map +1 -1
  458. package/dist/src/runtime/emit/join.js +20 -6
  459. package/dist/src/runtime/emit/join.js.map +1 -1
  460. package/dist/src/runtime/emit/limit-offset.d.ts.map +1 -1
  461. package/dist/src/runtime/emit/limit-offset.js +2 -1
  462. package/dist/src/runtime/emit/limit-offset.js.map +1 -1
  463. package/dist/src/runtime/emit/literal.d.ts.map +1 -1
  464. package/dist/src/runtime/emit/literal.js +2 -1
  465. package/dist/src/runtime/emit/literal.js.map +1 -1
  466. package/dist/src/runtime/emit/materialized-view-helpers.d.ts +22 -0
  467. package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
  468. package/dist/src/runtime/emit/materialized-view-helpers.js +123 -12
  469. package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
  470. package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -1
  471. package/dist/src/runtime/emit/materialized-view.js +6 -0
  472. package/dist/src/runtime/emit/materialized-view.js.map +1 -1
  473. package/dist/src/runtime/emit/merge-join.d.ts.map +1 -1
  474. package/dist/src/runtime/emit/merge-join.js +14 -4
  475. package/dist/src/runtime/emit/merge-join.js.map +1 -1
  476. package/dist/src/runtime/emit/ordinal-slice.d.ts.map +1 -1
  477. package/dist/src/runtime/emit/ordinal-slice.js +2 -1
  478. package/dist/src/runtime/emit/ordinal-slice.js.map +1 -1
  479. package/dist/src/runtime/emit/pragma.d.ts.map +1 -1
  480. package/dist/src/runtime/emit/pragma.js +2 -1
  481. package/dist/src/runtime/emit/pragma.js.map +1 -1
  482. package/dist/src/runtime/emit/project.d.ts.map +1 -1
  483. package/dist/src/runtime/emit/project.js +7 -2
  484. package/dist/src/runtime/emit/project.js.map +1 -1
  485. package/dist/src/runtime/emit/recursive-cte.d.ts.map +1 -1
  486. package/dist/src/runtime/emit/recursive-cte.js +2 -1
  487. package/dist/src/runtime/emit/recursive-cte.js.map +1 -1
  488. package/dist/src/runtime/emit/returning.d.ts.map +1 -1
  489. package/dist/src/runtime/emit/returning.js +7 -2
  490. package/dist/src/runtime/emit/returning.js.map +1 -1
  491. package/dist/src/runtime/emit/scalar-function.d.ts.map +1 -1
  492. package/dist/src/runtime/emit/scalar-function.js +2 -1
  493. package/dist/src/runtime/emit/scalar-function.js.map +1 -1
  494. package/dist/src/runtime/emit/scan.d.ts.map +1 -1
  495. package/dist/src/runtime/emit/scan.js +55 -17
  496. package/dist/src/runtime/emit/scan.js.map +1 -1
  497. package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
  498. package/dist/src/runtime/emit/schema-declarative.js +6 -5
  499. package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
  500. package/dist/src/runtime/emit/sequencing.d.ts.map +1 -1
  501. package/dist/src/runtime/emit/sequencing.js +2 -1
  502. package/dist/src/runtime/emit/sequencing.js.map +1 -1
  503. package/dist/src/runtime/emit/set-operation.d.ts.map +1 -1
  504. package/dist/src/runtime/emit/set-operation.js +6 -5
  505. package/dist/src/runtime/emit/set-operation.js.map +1 -1
  506. package/dist/src/runtime/emit/sink.d.ts.map +1 -1
  507. package/dist/src/runtime/emit/sink.js +2 -1
  508. package/dist/src/runtime/emit/sink.js.map +1 -1
  509. package/dist/src/runtime/emit/sort.d.ts.map +1 -1
  510. package/dist/src/runtime/emit/sort.js +2 -1
  511. package/dist/src/runtime/emit/sort.js.map +1 -1
  512. package/dist/src/runtime/emit/subquery.d.ts.map +1 -1
  513. package/dist/src/runtime/emit/subquery.js +42 -23
  514. package/dist/src/runtime/emit/subquery.js.map +1 -1
  515. package/dist/src/runtime/emit/table-valued-function.d.ts.map +1 -1
  516. package/dist/src/runtime/emit/table-valued-function.js +2 -1
  517. package/dist/src/runtime/emit/table-valued-function.js.map +1 -1
  518. package/dist/src/runtime/emit/temporal-arithmetic.d.ts.map +1 -1
  519. package/dist/src/runtime/emit/temporal-arithmetic.js +2 -1
  520. package/dist/src/runtime/emit/temporal-arithmetic.js.map +1 -1
  521. package/dist/src/runtime/emit/transaction.d.ts.map +1 -1
  522. package/dist/src/runtime/emit/transaction.js +2 -1
  523. package/dist/src/runtime/emit/transaction.js.map +1 -1
  524. package/dist/src/runtime/emit/unary.d.ts.map +1 -1
  525. package/dist/src/runtime/emit/unary.js +2 -1
  526. package/dist/src/runtime/emit/unary.js.map +1 -1
  527. package/dist/src/runtime/emit/update.d.ts.map +1 -1
  528. package/dist/src/runtime/emit/update.js +2 -1
  529. package/dist/src/runtime/emit/update.js.map +1 -1
  530. package/dist/src/runtime/emit/values.d.ts.map +1 -1
  531. package/dist/src/runtime/emit/values.js +2 -1
  532. package/dist/src/runtime/emit/values.js.map +1 -1
  533. package/dist/src/runtime/emit/view-mutation.d.ts +1 -1
  534. package/dist/src/runtime/emit/view-mutation.d.ts.map +1 -1
  535. package/dist/src/runtime/emit/view-mutation.js +6 -3
  536. package/dist/src/runtime/emit/view-mutation.js.map +1 -1
  537. package/dist/src/runtime/emit/window.d.ts.map +1 -1
  538. package/dist/src/runtime/emit/window.js +26 -9
  539. package/dist/src/runtime/emit/window.js.map +1 -1
  540. package/dist/src/runtime/emitters.d.ts +11 -3
  541. package/dist/src/runtime/emitters.d.ts.map +1 -1
  542. package/dist/src/runtime/emitters.js +13 -22
  543. package/dist/src/runtime/emitters.js.map +1 -1
  544. package/dist/src/runtime/foreign-key-actions.d.ts.map +1 -1
  545. package/dist/src/runtime/foreign-key-actions.js +39 -13
  546. package/dist/src/runtime/foreign-key-actions.js.map +1 -1
  547. package/dist/src/runtime/parallel-driver.d.ts +18 -4
  548. package/dist/src/runtime/parallel-driver.d.ts.map +1 -1
  549. package/dist/src/runtime/parallel-driver.js +72 -18
  550. package/dist/src/runtime/parallel-driver.js.map +1 -1
  551. package/dist/src/runtime/scheduler.d.ts +56 -7
  552. package/dist/src/runtime/scheduler.d.ts.map +1 -1
  553. package/dist/src/runtime/scheduler.js +223 -200
  554. package/dist/src/runtime/scheduler.js.map +1 -1
  555. package/dist/src/runtime/strict-flags.d.ts +22 -0
  556. package/dist/src/runtime/strict-flags.d.ts.map +1 -0
  557. package/dist/src/runtime/strict-flags.js +26 -0
  558. package/dist/src/runtime/strict-flags.js.map +1 -0
  559. package/dist/src/runtime/strict-fork.d.ts +4 -4
  560. package/dist/src/runtime/strict-fork.d.ts.map +1 -1
  561. package/dist/src/runtime/strict-fork.js +175 -31
  562. package/dist/src/runtime/strict-fork.js.map +1 -1
  563. package/dist/src/runtime/types.d.ts +66 -4
  564. package/dist/src/runtime/types.d.ts.map +1 -1
  565. package/dist/src/runtime/types.js +37 -0
  566. package/dist/src/runtime/types.js.map +1 -1
  567. package/dist/src/runtime/utils.d.ts.map +1 -1
  568. package/dist/src/runtime/utils.js +8 -17
  569. package/dist/src/runtime/utils.js.map +1 -1
  570. package/dist/src/schema/column.d.ts +8 -0
  571. package/dist/src/schema/column.d.ts.map +1 -1
  572. package/dist/src/schema/column.js.map +1 -1
  573. package/dist/src/schema/ddl-generator.js +5 -0
  574. package/dist/src/schema/ddl-generator.js.map +1 -1
  575. package/dist/src/schema/lens-compiler.js +2 -2
  576. package/dist/src/schema/lens-compiler.js.map +1 -1
  577. package/dist/src/schema/lens-prover.d.ts.map +1 -1
  578. package/dist/src/schema/lens-prover.js +2 -3
  579. package/dist/src/schema/lens-prover.js.map +1 -1
  580. package/dist/src/schema/lens.d.ts +1 -1
  581. package/dist/src/schema/manager.d.ts.map +1 -1
  582. package/dist/src/schema/manager.js +7 -2
  583. package/dist/src/schema/manager.js.map +1 -1
  584. package/dist/src/schema/rename-rewriter.d.ts +61 -0
  585. package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
  586. package/dist/src/schema/rename-rewriter.js +77 -0
  587. package/dist/src/schema/rename-rewriter.js.map +1 -1
  588. package/dist/src/schema/table.d.ts +36 -8
  589. package/dist/src/schema/table.d.ts.map +1 -1
  590. package/dist/src/schema/table.js +61 -11
  591. package/dist/src/schema/table.js.map +1 -1
  592. package/dist/src/schema/unique-enforcement.d.ts +28 -5
  593. package/dist/src/schema/unique-enforcement.d.ts.map +1 -1
  594. package/dist/src/schema/unique-enforcement.js +37 -5
  595. package/dist/src/schema/unique-enforcement.js.map +1 -1
  596. package/dist/src/schema/view.d.ts +1 -1
  597. package/dist/src/schema/view.js +1 -1
  598. package/dist/src/types/json-type.d.ts.map +1 -1
  599. package/dist/src/types/json-type.js +18 -8
  600. package/dist/src/types/json-type.js.map +1 -1
  601. package/dist/src/types/logical-type.d.ts +26 -0
  602. package/dist/src/types/logical-type.d.ts.map +1 -1
  603. package/dist/src/types/logical-type.js.map +1 -1
  604. package/dist/src/util/comparison.d.ts +83 -59
  605. package/dist/src/util/comparison.d.ts.map +1 -1
  606. package/dist/src/util/comparison.js +191 -131
  607. package/dist/src/util/comparison.js.map +1 -1
  608. package/dist/src/util/json-canonical.d.ts +34 -0
  609. package/dist/src/util/json-canonical.d.ts.map +1 -0
  610. package/dist/src/util/json-canonical.js +32 -0
  611. package/dist/src/util/json-canonical.js.map +1 -0
  612. package/dist/src/util/key-serializer.d.ts +10 -3
  613. package/dist/src/util/key-serializer.d.ts.map +1 -1
  614. package/dist/src/util/key-serializer.js +43 -17
  615. package/dist/src/util/key-serializer.js.map +1 -1
  616. package/dist/src/util/key-tuple-codec.d.ts +37 -0
  617. package/dist/src/util/key-tuple-codec.d.ts.map +1 -0
  618. package/dist/src/util/key-tuple-codec.js +65 -0
  619. package/dist/src/util/key-tuple-codec.js.map +1 -0
  620. package/dist/src/util/latches.d.ts +29 -3
  621. package/dist/src/util/latches.d.ts.map +1 -1
  622. package/dist/src/util/latches.js +60 -6
  623. package/dist/src/util/latches.js.map +1 -1
  624. package/dist/src/util/patterns.d.ts +22 -8
  625. package/dist/src/util/patterns.d.ts.map +1 -1
  626. package/dist/src/util/patterns.js +87 -16
  627. package/dist/src/util/patterns.js.map +1 -1
  628. package/dist/src/util/qualified-name.d.ts +10 -0
  629. package/dist/src/util/qualified-name.d.ts.map +1 -0
  630. package/dist/src/util/qualified-name.js +15 -0
  631. package/dist/src/util/qualified-name.js.map +1 -0
  632. package/dist/src/vtab/backing-host.d.ts +8 -1
  633. package/dist/src/vtab/backing-host.d.ts.map +1 -1
  634. package/dist/src/vtab/backing-host.js +7 -0
  635. package/dist/src/vtab/backing-host.js.map +1 -1
  636. package/dist/src/vtab/best-access-plan.d.ts +18 -0
  637. package/dist/src/vtab/best-access-plan.d.ts.map +1 -1
  638. package/dist/src/vtab/best-access-plan.js +15 -0
  639. package/dist/src/vtab/best-access-plan.js.map +1 -1
  640. package/dist/src/vtab/capabilities.d.ts +44 -0
  641. package/dist/src/vtab/capabilities.d.ts.map +1 -1
  642. package/dist/src/vtab/filter-info.d.ts +42 -0
  643. package/dist/src/vtab/filter-info.d.ts.map +1 -1
  644. package/dist/src/vtab/filter-info.js +137 -1
  645. package/dist/src/vtab/filter-info.js.map +1 -1
  646. package/dist/src/vtab/idx-str.d.ts +62 -0
  647. package/dist/src/vtab/idx-str.d.ts.map +1 -0
  648. package/dist/src/vtab/idx-str.js +130 -0
  649. package/dist/src/vtab/idx-str.js.map +1 -0
  650. package/dist/src/vtab/index-descriptor.d.ts +122 -0
  651. package/dist/src/vtab/index-descriptor.d.ts.map +1 -0
  652. package/dist/src/vtab/index-descriptor.js +104 -0
  653. package/dist/src/vtab/index-descriptor.js.map +1 -0
  654. package/dist/src/vtab/mapping-advertisement.d.ts +1 -1
  655. package/dist/src/vtab/memory/index.d.ts +12 -4
  656. package/dist/src/vtab/memory/index.d.ts.map +1 -1
  657. package/dist/src/vtab/memory/index.js +17 -9
  658. package/dist/src/vtab/memory/index.js.map +1 -1
  659. package/dist/src/vtab/memory/layer/base.d.ts +85 -31
  660. package/dist/src/vtab/memory/layer/base.d.ts.map +1 -1
  661. package/dist/src/vtab/memory/layer/base.js +127 -86
  662. package/dist/src/vtab/memory/layer/base.js.map +1 -1
  663. package/dist/src/vtab/memory/layer/connection.d.ts +27 -0
  664. package/dist/src/vtab/memory/layer/connection.d.ts.map +1 -1
  665. package/dist/src/vtab/memory/layer/connection.js +36 -1
  666. package/dist/src/vtab/memory/layer/connection.js.map +1 -1
  667. package/dist/src/vtab/memory/layer/interface.d.ts +24 -4
  668. package/dist/src/vtab/memory/layer/interface.d.ts.map +1 -1
  669. package/dist/src/vtab/memory/layer/manager.d.ts +212 -16
  670. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  671. package/dist/src/vtab/memory/layer/manager.js +754 -170
  672. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  673. package/dist/src/vtab/memory/layer/plan-filter.d.ts +26 -1
  674. package/dist/src/vtab/memory/layer/plan-filter.d.ts.map +1 -1
  675. package/dist/src/vtab/memory/layer/plan-filter.js +29 -11
  676. package/dist/src/vtab/memory/layer/plan-filter.js.map +1 -1
  677. package/dist/src/vtab/memory/layer/safe-iterate.d.ts +2 -2
  678. package/dist/src/vtab/memory/layer/safe-iterate.d.ts.map +1 -1
  679. package/dist/src/vtab/memory/layer/safe-iterate.js +2 -2
  680. package/dist/src/vtab/memory/layer/safe-iterate.js.map +1 -1
  681. package/dist/src/vtab/memory/layer/scan-layer.d.ts +5 -1
  682. package/dist/src/vtab/memory/layer/scan-layer.d.ts.map +1 -1
  683. package/dist/src/vtab/memory/layer/scan-layer.js +38 -22
  684. package/dist/src/vtab/memory/layer/scan-layer.js.map +1 -1
  685. package/dist/src/vtab/memory/layer/scan-plan.d.ts.map +1 -1
  686. package/dist/src/vtab/memory/layer/scan-plan.js +13 -21
  687. package/dist/src/vtab/memory/layer/scan-plan.js.map +1 -1
  688. package/dist/src/vtab/memory/layer/transaction.d.ts +169 -7
  689. package/dist/src/vtab/memory/layer/transaction.d.ts.map +1 -1
  690. package/dist/src/vtab/memory/layer/transaction.js +334 -9
  691. package/dist/src/vtab/memory/layer/transaction.js.map +1 -1
  692. package/dist/src/vtab/memory/module.d.ts +11 -4
  693. package/dist/src/vtab/memory/module.d.ts.map +1 -1
  694. package/dist/src/vtab/memory/module.js +35 -16
  695. package/dist/src/vtab/memory/module.js.map +1 -1
  696. package/dist/src/vtab/memory/table.d.ts +30 -4
  697. package/dist/src/vtab/memory/table.d.ts.map +1 -1
  698. package/dist/src/vtab/memory/table.js +63 -15
  699. package/dist/src/vtab/memory/table.js.map +1 -1
  700. package/dist/src/vtab/memory/utils/predicate.d.ts +9 -1
  701. package/dist/src/vtab/memory/utils/predicate.d.ts.map +1 -1
  702. package/dist/src/vtab/memory/utils/predicate.js +35 -14
  703. package/dist/src/vtab/memory/utils/predicate.js.map +1 -1
  704. package/dist/src/vtab/memory/utils/primary-key-encode.d.ts.map +1 -1
  705. package/dist/src/vtab/memory/utils/primary-key-encode.js +12 -6
  706. package/dist/src/vtab/memory/utils/primary-key-encode.js.map +1 -1
  707. package/dist/src/vtab/memory/utils/primary-key.d.ts +6 -1
  708. package/dist/src/vtab/memory/utils/primary-key.d.ts.map +1 -1
  709. package/dist/src/vtab/memory/utils/primary-key.js +12 -8
  710. package/dist/src/vtab/memory/utils/primary-key.js.map +1 -1
  711. package/dist/src/vtab/module.d.ts +38 -14
  712. package/dist/src/vtab/module.d.ts.map +1 -1
  713. package/dist/src/vtab/table.d.ts +41 -2
  714. package/dist/src/vtab/table.d.ts.map +1 -1
  715. package/dist/src/vtab/table.js +19 -0
  716. package/dist/src/vtab/table.js.map +1 -1
  717. package/package.json +5 -4
  718. package/dist/src/planner/rules/cache/rule-materialization-advisory.d.ts +0 -18
  719. package/dist/src/planner/rules/cache/rule-materialization-advisory.d.ts.map +0 -1
  720. package/dist/src/planner/rules/cache/rule-materialization-advisory.js +0 -65
  721. package/dist/src/planner/rules/cache/rule-materialization-advisory.js.map +0 -1
@@ -41,37 +41,16 @@
41
41
  import { createLogger } from '../common/logger.js';
42
42
  import { QuereusError } from '../common/errors.js';
43
43
  import { StatusCode } from '../common/types.js';
44
- import { isRelationalNode, isScalarNode } from '../planner/nodes/plan-node.js';
45
- import { ColumnReferenceNode, TableReferenceNode } from '../planner/nodes/reference.js';
46
- import { FilterNode } from '../planner/nodes/filter.js';
47
- import { checkDeterministic } from '../planner/validation/determinism-validator.js';
48
- import { emitPlanNode } from '../runtime/emitters.js';
49
- import { EmissionContext } from '../runtime/emission-context.js';
50
- import { Scheduler } from '../runtime/scheduler.js';
51
- import { RowContextMap } from '../runtime/context-helpers.js';
52
- import { createStrictRowContextMap, wrapTableContextsStrict } from '../runtime/strict-fork.js';
53
- import { isAsyncIterable } from '../runtime/utils.js';
54
- import { AggregateNode } from '../planner/nodes/aggregate-node.js';
55
- import { TableFunctionCallNode } from '../planner/nodes/table-function-call.js';
56
- import { PlanNodeType } from '../planner/nodes/plan-node-type.js';
57
44
  import { buildSourceUnionScope } from '../planner/analysis/change-scope.js';
58
- import { injectKeyFilter } from '../planner/analysis/key-filter.js';
59
- import { keysOf } from '../planner/util/fd-utils.js';
60
- import { deriveCoarsenedBackingKey, resolveValuePreservingSourceCol } from '../planner/analysis/coarsened-key.js';
61
- import { proveOneToOneJoin } from '../planner/analysis/coverage-prover.js';
62
- import { CapabilityDetectors } from '../planner/framework/characteristics.js';
63
- import { selectMaintenanceStrategy, isFullRebuildPathological, seqScanCost, filterCost, projectCost, } from '../planner/cost/index.js';
64
- import { resolveBackingHost, tryResolveBackingHost, isBodyIrrelevantTableChange, tryRecompileMaterializedViewLive } from '../runtime/emit/materialized-view-helpers.js';
65
- import { assertTransitiveRestrictsForParentMutation, executeForeignKeyActionsAndLens } from '../runtime/foreign-key-actions.js';
66
- import { buildDerivedRowValidator, makePoisonedDerivedRowValidator, validateDerivedRowImage } from './derived-row-validator.js';
67
- import { buildPrimaryKeyFromValues } from '../vtab/memory/utils/primary-key.js';
68
- import { compilePredicate } from '../vtab/memory/utils/predicate.js';
69
- import { compareSqlValues, rowsValueIdentical, normalizeCollationName } from '../util/comparison.js';
70
- import { coveringMvHonorsIndexCollation, uniqueEnforcementCollations } from '../schema/unique-enforcement.js';
45
+ import { isBodyIrrelevantTableChange, tryRecompileMaterializedViewLive } from '../runtime/emit/materialized-view-helpers.js';
46
+ import { buildDerivedRowValidator, makePoisonedDerivedRowValidator } from './derived-row-validator.js';
47
+ import { compareSqlValuesFast, resolveCollationFunctions } from '../util/comparison.js';
48
+ import { coveringMvHonorsIndexCollation } from '../schema/unique-enforcement.js';
49
+ import { splitBaseKey } from '../util/qualified-name.js';
50
+ import { mvKey, planSourceBases, isBinaryCollation, } from './database-materialized-views-analysis.js';
51
+ import { buildMaintenancePlan } from './database-materialized-views-plan-builders.js';
52
+ import { applyInverseProjection, applyForwardResidual, applyJoinResidual, applyPrefixDelete, runScheduler, validateDerivedChanges, assertNoNullInNotNullSeededPkRowTime, enforceParentSideReferentialActions, backingHost, getBackingConnection, } from './database-materialized-views-apply.js';
71
53
  const log = createLogger('core:materialized-views');
72
- /** Fallback source row estimate when the StatsProvider has no count (mirrors the
73
- * optimizer's naive default). Only feeds the create-time maintenance cost gate. */
74
- const DEFAULT_SOURCE_ROWS = 1000;
75
54
  export class MaterializedViewManager {
76
55
  ctx;
77
56
  unsubscribeSchemaChanges = null;
@@ -290,7 +269,7 @@ export class MaterializedViewManager {
290
269
  // backing table. v1 is the conservative union of a `full` watch per source.
291
270
  mv.derivation.sourceScope = buildSourceUnionScope(mv.derivation.sourceTables);
292
271
  this.releaseRowTime(key);
293
- const plan = this.buildMaintenancePlan(mv); // throws on ineligible shape
272
+ const plan = buildMaintenancePlan(this.ctx, mv); // throws on ineligible shape
294
273
  // Compile the declared-CHECK/FK derived-row validator (undefined when the
295
274
  // table declares nothing — the zero-overhead gate). Built here, inside the
296
275
  // registration the create/attach paths roll back on throw, so a constraint
@@ -459,6 +438,7 @@ export class MaterializedViewManager {
459
438
  const coarsened = mv.derivation.coarsenedKey;
460
439
  if (!coarsened || coarsened.weakened.length === 0)
461
440
  return undefined;
441
+ const resolver = this.ctx.getCollationResolver();
462
442
  const watch = [];
463
443
  for (const w of coarsened.weakened) {
464
444
  const index = mv.columnIndexMap.get(w.column.toLowerCase());
@@ -471,6 +451,7 @@ export class MaterializedViewManager {
471
451
  watch.push({
472
452
  index,
473
453
  sourceCollation: w.sourceCollation,
454
+ sourceCollationFn: resolver(w.sourceCollation),
474
455
  outputCollation: w.outputCollation,
475
456
  column: w.column,
476
457
  });
@@ -517,7 +498,7 @@ export class MaterializedViewManager {
517
498
  continue;
518
499
  const weakenedColumns = [];
519
500
  for (const w of watch) {
520
- if (compareSqlValues(change.oldRow[w.index], change.newRow[w.index], w.sourceCollation) !== 0) {
501
+ if (compareSqlValuesFast(change.oldRow[w.index], change.newRow[w.index], w.sourceCollationFn) !== 0) {
521
502
  weakenedColumns.push(w.column);
522
503
  }
523
504
  }
@@ -604,20 +585,24 @@ export class MaterializedViewManager {
604
585
  // independently of the cascade below; it neither consumes nor reorders the
605
586
  // backing changes routed onward.
606
587
  this.detectAndReportCoarseningCollisions(plan, backingChanges);
588
+ // Reject a NULL stored into a NOT-NULL ordering-seeded physical-PK backing
589
+ // column whose source loosened — BEFORE the cascade, so the corruption never
590
+ // reaches a consumer. No-op (`undefined`) unless this MV carries the skew.
591
+ assertNoNullInNotNullSeededPkRowTime(plan, backingChanges);
607
592
  // Declared CHECK / child-side FK over the rows this delta wrote — BEFORE
608
593
  // cascading, so a consumer never consumes an invalid producer row. Every
609
594
  // row already in the backing was validated when it entered (the bulk
610
595
  // validation at create/attach seeds the induction), so only the delta is
611
596
  // validated. No-op (`undefined`) for a constraint-less table.
612
597
  if (plan.derivedRowValidator) {
613
- await this.validateDerivedChanges(plan, plan.derivedRowValidator, backingChanges, cache);
598
+ await validateDerivedChanges(this.ctx, plan, plan.derivedRowValidator, backingChanges, cache);
614
599
  }
615
600
  // Parent-side referential enforcement: this maintenance delete/key-update of an
616
601
  // `M` row may orphan rows in an ordinary table `C` whose FK references `M`. Fire
617
602
  // the shared engine over the backing delta — RESTRICT-walk then declared actions —
618
603
  // after `M`'s own image is validated, before the MV-over-MV cascade. Runs whether
619
604
  // or not `M` has MV consumers (placed before the leaf fast-path).
620
- await this.enforceParentSideReferentialActions(plan, backingChanges);
605
+ await enforceParentSideReferentialActions(this.ctx, plan, backingChanges);
621
606
  const backingBase = `${plan.backingSchema}.${plan.backingTableName}`.toLowerCase();
622
607
  if (!this.rowTimeBySource.has(backingBase))
623
608
  continue; // leaf — no dependents
@@ -674,17 +659,21 @@ export class MaterializedViewManager {
674
659
  // floor's collation-keyed `replace-all` realizes the same LWW merge as the
675
660
  // bounded-delta arms (observe-only; gated on `coarseningWatch`).
676
661
  this.detectAndReportCoarseningCollisions(plan, backingChanges);
662
+ // NOT-NULL ordering-seeded PK guard over the rebuild diff — the
663
+ // full-rebuild-floor analogue of the per-row guard in {@link maintainRowTime}.
664
+ // No-op (`undefined`) unless this MV carries the skew.
665
+ assertNoNullInNotNullSeededPkRowTime(plan, backingChanges);
677
666
  // Validate the rebuild diff's written images at the flush boundary —
678
667
  // the full-rebuild analogue of the per-row validation in
679
668
  // {@link maintainRowTime} (deferred-rebuild semantics preserved: a bulk
680
669
  // source write fails once at end-of-statement, not per source row).
681
670
  if (plan.derivedRowValidator) {
682
- await this.validateDerivedChanges(plan, plan.derivedRowValidator, backingChanges, cache);
671
+ await validateDerivedChanges(this.ctx, plan, plan.derivedRowValidator, backingChanges, cache);
683
672
  }
684
673
  // Parent-side referential enforcement for the rebuild diff's deletes/key-updates,
685
674
  // fired inside the statement-atomicity savepoint (the flush runs before its
686
675
  // release) so a RESTRICT failure or cascade error unwinds the whole statement.
687
- await this.enforceParentSideReferentialActions(plan, backingChanges);
676
+ await enforceParentSideReferentialActions(this.ctx, plan, backingChanges);
688
677
  const backingBase = `${plan.backingSchema}.${plan.backingTableName}`.toLowerCase();
689
678
  if (!this.rowTimeBySource.has(backingBase))
690
679
  continue; // leaf — no dependents
@@ -724,6 +713,7 @@ export class MaterializedViewManager {
724
713
  + `'${backingBase}' — a row-time dependency cycle should be structurally impossible`, StatusCode.INTERNAL);
725
714
  }
726
715
  }
716
+ /* ──────────────────── maintenance dispatch ──────────────────── */
727
717
  /**
728
718
  * Dispatch a maintenance plan on its `kind`, compute the per-row backing delta,
729
719
  * apply it, and return the **effective** {@link BackingRowChange}(s) the backing
@@ -733,17 +723,22 @@ export class MaterializedViewManager {
733
723
  * `'full-rebuild'` (the floor — re-evaluate the whole body and replace the backing). The
734
724
  * floor ignores the specific `change` (it rebuilds wholesale); the others derive a
735
725
  * bounded per-row delta from it.
726
+ *
727
+ * The dispatch stays on the manager (the per-arm appliers live in
728
+ * database-materialized-views-apply.ts as free functions over the manager context); it
729
+ * is the seam the row-time equivalence suite instruments to observe the effective
730
+ * changes a maintenance apply realizes.
736
731
  */
737
732
  async applyMaintenancePlan(plan, change, changedBase, cache) {
738
733
  switch (plan.kind) {
739
734
  case 'inverse-projection':
740
- return this.applyInverseProjection(plan, change, cache);
735
+ return applyInverseProjection(this.ctx, plan, change, cache);
741
736
  case 'residual-recompute':
742
- return this.applyForwardResidual(plan, change, cache);
737
+ return applyForwardResidual(this.ctx, plan, change, cache);
743
738
  case 'prefix-delete':
744
- return this.applyPrefixDelete(plan, change, cache);
739
+ return applyPrefixDelete(this.ctx, plan, change, cache);
745
740
  case 'join-residual':
746
- return this.applyJoinResidual(plan, change, changedBase, cache);
741
+ return applyJoinResidual(this.ctx, plan, change, changedBase, cache);
747
742
  case 'full-rebuild':
748
743
  return this.applyFullRebuild(plan, cache);
749
744
  default: {
@@ -755,1015 +750,12 @@ export class MaterializedViewManager {
755
750
  }
756
751
  }
757
752
  }
758
- /**
759
- * Compute an `'inverse-projection'` plan's per-row backing delta, apply it, and
760
- * return the **effective** {@link BackingRowChange}(s) the backing layer realized.
761
- * An out-of-scope row (or a delete of an absent backing key) yields no change. This
762
- * body is the shipped covering-index maintenance, lifted verbatim from the former
763
- * `applyRowTimeChange`, plus the equal-image short-circuit: an UPDATE whose old and
764
- * new projected images are value-identical (both in scope) projects to NO backing
765
- * delta — the dominant no-op echo (a source update touching only unprojected columns,
766
- * or rewriting a projected column to its existing value) is suppressed before any
767
- * backing-connection work. Accurate by the maintenance invariant (the backing row IS
768
- * the old image's projection), so nothing would have changed; the host's
769
- * value-identical upsert skip (vtab/backing-host.ts) remains the effective-state
770
- * backstop for the paths that do emit ops.
771
- */
772
- async applyInverseProjection(plan, change, cache) {
773
- const inScope = (row) => plan.predicate === undefined || plan.predicate.evaluate(row) === true;
774
- const project = (row) => plan.projectors.map(p => p.kind === 'passthrough' ? row[p.sourceCol] : p.eval(row));
775
- const keyOf = (backingRow) => buildPrimaryKeyFromValues(plan.backingPkDefinition.map(d => backingRow[d.index]), plan.backingPkDefinition);
776
- const ops = [];
777
- if (change.op === 'insert') {
778
- if (inScope(change.newRow))
779
- ops.push({ kind: 'upsert', row: project(change.newRow) });
780
- }
781
- else if (change.op === 'delete') {
782
- if (inScope(change.oldRow))
783
- ops.push({ kind: 'delete-key', key: keyOf(project(change.oldRow)) });
784
- }
785
- else {
786
- // UPDATE: a both-in-scope, same-backing-key change is one upsert (the host
787
- // reports a single `update`); otherwise delete the old image if it was in
788
- // scope and upsert the new image if it is — predicate-scope transitions and
789
- // key-changing updates are genuinely two-sided. The scope check reads the
790
- // SOURCE row (the predicate may reference unprojected columns), so both
791
- // images must be in scope for the equal-image short-circuit.
792
- const oldIn = inScope(change.oldRow);
793
- const newIn = inScope(change.newRow);
794
- if (oldIn && newIn) {
795
- const oldImage = project(change.oldRow);
796
- const newImage = project(change.newRow);
797
- // Byte-faithful identity (rowsValueIdentical): subsumes key equality, and a
798
- // collation-equal / byte-different image is NOT suppressed (it must re-key
799
- // the stored bytes) — the same discipline as the host-level upsert skip.
800
- if (rowsValueIdentical(oldImage, newImage))
801
- return [];
802
- if (this.backingPkEqual(plan.backingPkDefinition, oldImage, newImage)) {
803
- // Same backing key (collation-aware — a collation-equal / byte-different
804
- // key is the SAME btree identity, and the upsert re-keys the stored
805
- // bytes): one upsert replaces the row wholesale, so the host reports
806
- // a single `update` — matching the residual arms' post-suppression
807
- // shape (one cascade dispatch, one change-log entry, no secondary-index
808
- // churn from a delete+insert at an unchanged key).
809
- ops.push({ kind: 'upsert', row: newImage });
810
- }
811
- else {
812
- ops.push({ kind: 'delete-key', key: keyOf(oldImage) });
813
- ops.push({ kind: 'upsert', row: newImage });
814
- }
815
- }
816
- else {
817
- if (oldIn)
818
- ops.push({ kind: 'delete-key', key: keyOf(project(change.oldRow)) });
819
- if (newIn)
820
- ops.push({ kind: 'upsert', row: project(change.newRow) });
821
- }
822
- }
823
- if (ops.length === 0)
824
- return [];
825
- const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
826
- if (!backing) {
827
- throw new QuereusError(`Internal error: backing table '${plan.backingTableName}' for materialized view '${plan.mv.name}' not found`, StatusCode.INTERNAL);
828
- }
829
- const host = this.backingHost(backing);
830
- const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
831
- return host.applyMaintenance(connection, ops);
832
- }
833
- /**
834
- * Validate the row images a maintenance apply WROTE (insert/update
835
- * {@link BackingRowChange}s — a delete writes no image) against the plan's
836
- * compiled {@link DerivedRowConstraintValidator}. Inline checks abort the
837
- * writing statement with the maintained-table-attributed CONSTRAINT error;
838
- * auto-deferred checks (subquery CHECK, every child-side FK) queue to the
839
- * deferred-constraint queue and validate at commit. Deferred entries are
840
- * pinned to the backing connection the maintenance write used (resolved from
841
- * the per-statement cache, or re-resolved deterministically — the same
842
- * connection either way) so commit-time evaluation reads the same pending
843
- * state, mirroring the DML pipeline's active-connection capture.
844
- */
845
- async validateDerivedChanges(plan, validator, changes, cache) {
846
- let connectionId;
847
- if (validator.checks.some(c => c.needsDeferred)) {
848
- const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
849
- if (backing) {
850
- const host = this.backingHost(backing);
851
- const conn = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
852
- connectionId = conn.connectionId;
853
- }
854
- }
855
- for (const change of changes) {
856
- if (change.op === 'delete')
857
- continue;
858
- await validateDerivedRowImage(this.ctx, validator, change.newRow, connectionId);
859
- }
860
- }
861
- /**
862
- * Fire **parent-side** referential enforcement over the backing rows a maintenance
863
- * apply REMOVED or re-keyed (delete / key-update {@link BackingRowChange}s — an insert
864
- * has no parent-side action). When the maintained table `M` is the PARENT (FK target)
865
- * of an FK declared on an ordinary table `C` (`create table C (… references M(col) …)`),
866
- * a maintenance-driven delete/key-update of the referenced `M` row would silently orphan
867
- * `C`'s rows, bypassing the declared RESTRICT / referential action. This is the
868
- * **dual** of {@link validateDerivedChanges} (constraints declared *on* `M`); the FK here
869
- * lives on `C` and references `M`, so it is invisible to `M`'s own plan/validator.
870
- *
871
- * It reuses the SAME shared referential-action engine the DML executor and the
872
- * external-change ingestion seam use — no third copy — applying its two functions over
873
- * each backing change exactly as `database-external-changes.ts` does:
874
- * - {@link assertTransitiveRestrictsForParentMutation} — pre-walk the transitive cascade
875
- * closure and throw a CONSTRAINT error naming `M` on any surviving RESTRICT child;
876
- * - {@link executeForeignKeyActionsAndLens} — run declared CASCADE / SET NULL / SET DEFAULT,
877
- * re-entering the DML executor (the already-holding-the-mutex variant) for each cascaded
878
- * child write, so `C`'s own constraints, watches, nested cascades, and (if `C` is itself
879
- * an MV source) its own maintenance all fire.
880
- *
881
- * Ordering: called AFTER the backing delta has landed in the pending layer (the RESTRICT
882
- * walk runs POST-application — the child rows it keys off still exist because the cascade
883
- * has not run yet) and AFTER `M`'s own image is validated, matching the DML executor's
884
- * per-change order (capture → MV maintenance → FK actions) and the external-changes seam.
885
- * `lensRouted = false`: a maintenance backing write is a physical basis write (maintained
886
- * tables are not lens basis spines). A surviving RESTRICT throws up through
887
- * {@link maintainRowTime} → the DML executor → the statement, rolling back the source write
888
- * attributed to `M`.
889
- *
890
- * Gate: a cheap `foreign_keys`-pragma early-return keeps the pragma-off path free (the
891
- * engine also early-returns, but skipping the `getTable` + loop avoids all per-change work).
892
- * NOT gated on `plan.derivedRowValidator` — that gate is child-side (constraints *on* `M`);
893
- * an inbound FK lives on `C` and leaves `M`'s plan untouched. Beyond the gate it fires
894
- * unconditionally per delete/update change, but the engine no longer pays an `O(catalog)`
895
- * scan: both calls route through `SchemaManager.getReferencingForeignKeys`, the precomputed
896
- * reverse-FK index, so an `M` that nothing references resolves to the shared empty bucket and
897
- * each call early-returns in O(1) — a maintained table with no inbound FK (the common case)
898
- * pays only the pragma check plus one map lookup per delete/key-update change.
899
- */
900
- async enforceParentSideReferentialActions(plan, changes) {
901
- const db = this.ctx;
902
- if (!db.options.getBooleanOption('foreign_keys'))
903
- return; // cheap gate; engine early-returns too
904
- // The backing `TableSchema` — same object validateDerivedChanges resolves; its `.name`
905
- // equals `M`'s, so an FK on `C` (`references M`) matches the engine's referencing scan.
906
- const parent = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
907
- if (!parent)
908
- return; // backing gone ⇒ MV already broken
909
- for (const change of changes) {
910
- if (change.op === 'insert')
911
- continue; // inserts have no parent-side actions
912
- await assertTransitiveRestrictsForParentMutation(db, parent, change.op, change.oldRow, change.newRow);
913
- await executeForeignKeyActionsAndLens(db, parent, change.op, change.oldRow, change.newRow);
914
- }
915
- }
916
- /**
917
- * Resolve the {@link BackingHost} capability surface for a backing table —
918
- * see `vtab/backing-host.ts` for the contract. The host is resolved fresh per
919
- * use (a map lookup on the owning module), so a drop+recreate of the backing
920
- * always yields the new incarnation's host.
921
- */
922
- backingHost(backing) {
923
- // The ctx IS the Database (same construction as buildMaintenancePlan's cast).
924
- return resolveBackingHost(this.ctx, backing);
925
- }
926
- /**
927
- * Obtain (lazily create + register) the backing table's
928
- * {@link VirtualTableConnection} for the current transaction. Reuses the same
929
- * connection a `select` from the MV resolves to (so reads-own-writes holds) —
930
- * matched among the Database's registered connections by
931
- * {@link BackingHost.ownsConnection}, which is pinned to the live backing
932
- * incarnation; a freshly created connection is registered with the Database so
933
- * the coordinated commit/rollback covers its pending state in lockstep with the
934
- * source write.
935
- *
936
- * When an optional per-statement {@link BackingConnectionCache} is supplied, the
937
- * scan over the Database's active connections (the dominant per-row cost on a bulk
938
- * write) is paid once per (statement, backing): a hit returns the cached connection
939
- * directly, and a miss caches whichever connection the scan resolves — or the one it
940
- * lazily creates + registers. Caching the resolved/created connection is sound
941
- * because the scan is deterministic within a statement (nothing interleaves between
942
- * a statement's rows to change which connection a `select` from the MV picks), so the
943
- * cache holds exactly what an uncached re-resolution would return.
944
- */
945
- async getBackingConnection(host, qualifiedName, cache) {
946
- const cacheKey = qualifiedName.toLowerCase();
947
- const cached = cache?.get(cacheKey);
948
- if (cached)
949
- return cached;
950
- for (const c of this.ctx.getConnectionsForTable(qualifiedName)) {
951
- if (host.ownsConnection(c)) {
952
- cache?.set(cacheKey, c);
953
- return c;
954
- }
955
- }
956
- const conn = host.connect();
957
- await this.ctx.registerConnection(conn);
958
- cache?.set(cacheKey, conn);
959
- return conn;
960
- }
961
- /**
962
- * Build the row-time maintenance plan for an MV — **cost-gated, with a floor, never a
963
- * shape allowlist**. The builder tries to match a bounded-delta arm by shape
964
- * ({@link tryBuildBoundedDeltaArm}); a body that matches **none** falls through to the
965
- * always-correct {@link buildFullRebuildPlan} floor (re-evaluate the whole body, replace
966
- * the backing transactionally). **No body is rejected for its shape.** Only four
967
- * create-time rejections remain, all non-shape:
968
- * - a **non-deterministic** body without `pragma nondeterministic_schema` — a hard reject
969
- * in the matched arm (so the arm-specific determinism diagnostic survives) or, for a
970
- * body matching no arm, in the floor's whole-body determinism check;
971
- * - a **bag** (no provable unique key) — the floor's `keysOf` reject (a duplicate-producing
972
- * body usually fails the set contract earlier, at create-fill);
973
- * - a body with **no relational output**;
974
- * - a **full-rebuild-only body over a source past the size threshold**
975
- * ({@link isFullRebuildPathological}, the `materialized_view_rebuild_row_threshold` option).
976
- *
977
- * The single source may itself be another MV's backing table (an MV-over-MV body):
978
- * `building/select.ts` rewrites a reference to `mv1` into a `TableReference` against
979
- * `mv1`'s backing table, so the source base is `mv1`'s backing base and the same checks
980
- * evaluate against the (keyed `memory`) backing schema unchanged. A write to `mv1` then
981
- * drives `mv2` via the cascade in {@link maintainRowTime}.
982
- *
983
- * Eligibility is a *cost choice* among the body's structurally-sound strategies
984
- * ({@link selectMaintenanceStrategy}): the bounded-delta arms are preferred by the argmin
985
- * cost gate, and full-rebuild is selected exactly when no bounded-delta arm is sound (an
986
- * empty sound set resolves to the floor) — so an existing eligible shape is unaffected.
987
- */
988
- buildMaintenancePlan(mv) {
989
- const db = this.ctx;
990
- // Analyze the MV's own body to compile maintenance; suppress the read-side
991
- // rewrite so the body stays over its SOURCE table, not re-pointed at this
992
- // MV's backing (which the maintenance plan is what keeps consistent).
993
- const analyzed = db.schemaManager.withSuppressedMaterializedViewRewrite(() => {
994
- const { plan } = this.ctx._buildPlan([mv.derivation.selectAst]);
995
- return this.ctx.optimizer.optimizeForAnalysis(plan, db);
996
- });
997
- // Replicable-determinism gate — host-conditional, inert by default. A backing host
998
- // whose backing replicates across peers (the sync-store) sets
999
- // `requiresReplicableDerivations` so a platform-dependent UDF or collation cannot
1000
- // diverge peers. Checked here — after `analyzed`, before arm selection — so it applies
1001
- // regardless of which maintenance arm wins, over the SAME analyzed plan the determinism
1002
- // gate walks (nested calls, WHERE / GROUP BY / aggregate-arg / TVF-arg positions all
1003
- // reached). It sits NEXT TO, not in place of, the determinism gate: the two are
1004
- // orthogonal, so this is NOT lifted by `pragma nondeterministic_schema`. A memory/store
1005
- // host leaves the flag undefined ⇒ this block is skipped ⇒ zero behavior change.
1006
- // Idempotent (same body ⇒ same verdict), so it is also desirable on re-register /
1007
- // catalog import: a tampered catalog cannot smuggle a non-replicable body past a
1008
- // demanding host. Two gates of the same shape: a non-replicable FUNCTION (which the
1009
- // body walk's function-bearing nodes carry) and a non-replicable COLLATION (which rides
1010
- // each scalar node's resolved type plus the backing key's declared collations).
1011
- // Resolve the host LENIENTLY: at the create-time gate registration of an
1012
- // `alter table … set maintained` attach, a module that materializes its durable
1013
- // backing late (lamina's `ensureBackingForAttach`, after this gate) has no host
1014
- // yet. The host is used here ONLY for the host-conditional, default-inert
1015
- // `requiresReplicableDerivations` gate — a host that sets it (the synced-store
1016
- // flavor) always exists by plan-build time, so skipping the gate when the host
1017
- // is absent never lets a non-replicable body slip past. The reconcile resolves
1018
- // the host for real, and the maintenance arms re-resolve it per use.
1019
- const host = tryResolveBackingHost(db, mv);
1020
- if (host?.requiresReplicableDerivations) {
1021
- const offendingFn = findNonReplicableFunction(analyzed);
1022
- if (offendingFn)
1023
- throw nonReplicableDerivationError(mv.name, offendingFn);
1024
- const offendingCollation = findNonReplicableCollation(analyzed, mv, db);
1025
- if (offendingCollation)
1026
- throw nonReplicableCollationDerivationError(mv.name, offendingCollation);
1027
- }
1028
- // Try a bounded-delta arm; a shape that fits none falls through to the floor.
1029
- const boundedDelta = this.tryBuildBoundedDeltaArm(mv, analyzed);
1030
- return boundedDelta ?? this.buildFullRebuildPlan(mv, analyzed);
1031
- }
1032
- /**
1033
- * Route the analyzed body to the matching bounded-delta arm, or return `null` when its
1034
- * shape fits **no** bounded-delta arm (the caller then builds the full-rebuild floor).
1035
- * Each arm builder likewise returns `null` on a sub-shape mismatch and falls through
1036
- * here. The arms keep only **determinism** as a hard reject (so their arm-specific
1037
- * determinism diagnostic survives — see the individual builders); every other mismatch
1038
- * is a `null` fall-through. Bag / no-output / size rejects live in the floor.
1039
- */
1040
- tryBuildBoundedDeltaArm(mv, analyzed) {
1041
- // A body that reads no source table has no bounded-delta arm → floor (which rejects
1042
- // a sourceless body). (A self-join / TVF fan-out surfaces ≥2 refs or a TVF node.)
1043
- const tableRefs = [...collectTableRefs(analyzed).values()];
1044
- if (tableRefs.length === 0)
1045
- return null;
1046
- // Shapes no bounded-delta arm models — a window function reads across the partition,
1047
- // set ops / recursive CTEs / DISTINCT / row caps are out of the bounded-delta model.
1048
- // They are NOT rejected: a deterministic, keyed such body is maintained by the floor.
1049
- if (containsNodeType(analyzed, PlanNodeType.Window))
1050
- return null;
1051
- if (containsNodeType(analyzed, PlanNodeType.Distinct))
1052
- return null;
1053
- if (containsNodeType(analyzed, PlanNodeType.SetOperation))
1054
- return null;
1055
- if (containsNodeType(analyzed, PlanNodeType.RecursiveCTE))
1056
- return null;
1057
- if (mv.derivation.selectAst.type === 'select' && (mv.derivation.selectAst.limit !== undefined || mv.derivation.selectAst.offset !== undefined)) {
1058
- return null;
1059
- }
1060
- const tableRef = tableRefs[0];
1061
- const sourceSchema = tableRef.tableSchema;
1062
- const sourceBase = `${sourceSchema.schemaName}.${sourceSchema.name}`.toLowerCase();
1063
- // Single base source `T` joined to ONE lateral table-valued function — a fan-out
1064
- // body (each base row produces N backing rows) → the prefix-delete arm. Routed
1065
- // *before* the generic join branch below, because a lateral fan-out surfaces BOTH a
1066
- // Join and a TableFunctionCall. A multi-base TVF body falls to the floor.
1067
- if (containsNodeType(analyzed, PlanNodeType.TableFunctionCall)) {
1068
- if (tableRefs.length !== 1)
1069
- return null;
1070
- return this.buildLateralTvfPrefixDeletePlan(mv, analyzed, tableRef, sourceBase);
1071
- }
1072
- // Any join → the provably-1:1 join-residual arm. A fanning (non-1:1) join, an outer
1073
- // join, a >2-source join, an aggregate over a join, or a partial WHERE returns `null`
1074
- // from the builder → floor. (The lateral-TVF fan-out above is matched first because
1075
- // it also surfaces a join node.)
1076
- if (containsAnyJoin(analyzed)) {
1077
- return this.buildJoinResidualPlan(mv, analyzed, tableRefs);
1078
- }
1079
- // A non-join multi-source body (e.g. a WHERE-subquery over a second table) has no
1080
- // bounded-delta arm → floor.
1081
- if (tableRefs.length > 1)
1082
- return null;
1083
- // Single-source aggregate (`group by` over bare columns) → residual-recompute arm.
1084
- // Each changed source row belongs to exactly one group; maintenance recomputes that
1085
- // group's backing row from live state. A scalar aggregate (no GROUP BY) falls to the
1086
- // floor.
1087
- const aggregate = findAggregate(analyzed);
1088
- if (aggregate) {
1089
- return this.buildAggregateResidualPlan(mv, analyzed, tableRef, sourceBase, aggregate);
1090
- }
1091
- // The covering-index shape → inverse-projection arm (the default single-source arm).
1092
- return this.buildInverseProjectionPlan(mv, analyzed, tableRef, sourceBase);
1093
- }
1094
- /**
1095
- * Build an `'inverse-projection'` plan for the covering-index shape: a single
1096
- * row-preserving source `T` with a primary key, a linear
1097
- * `TableReference → optional Filter → Project → optional Sort` body, a projection that
1098
- * resolves every source PK column (and every backing-key column) to a **passthrough**
1099
- * source column — non-key columns may instead be a **deterministic scalar expression**
1100
- * over the source row — and a partial WHERE evaluable on a single source row. Returns
1101
- * `null` on any **shape** mismatch (the caller falls through to the full-rebuild floor);
1102
- * a **non-deterministic** computed column is the one hard reject (its arm-specific
1103
- * determinism diagnostic must survive rather than fall through to the floor's generic one).
1104
- */
1105
- buildInverseProjectionPlan(mv, analyzed, tableRef, sourceBase) {
1106
- const db = this.ctx;
1107
- const sourceSchema = tableRef.tableSchema;
1108
- const sourcePkCols = sourceSchema.primaryKeyDefinition.map(d => d.index);
1109
- if (sourcePkCols.length === 0)
1110
- return null; // source has no PK → floor
1111
- const backing = this.ctx._findTable(mv.name, mv.schemaName);
1112
- if (!backing) {
1113
- throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
1114
- }
1115
- // Projection classification: each backing output column is either a passthrough
1116
- // source column (a pure permutation entry) or a deterministic scalar expression
1117
- // over the single source row. A passthrough makes maintenance a column copy; an
1118
- // expression column evaluates `project(sourceRow)` via the runtime (still a pure
1119
- // per-row function — O(log n), no body re-execution). PK / backing-key columns
1120
- // must stay passthrough (the backing key and the inverse-projection conflict map
1121
- // depend on it); non-key columns may be computed.
1122
- //
1123
- // "Passthrough" is value-preserving lineage (`resolveValuePreservingSourceCol`):
1124
- // a bare column reference, OR one wrapped in `collate` / a no-op `cast` — those
1125
- // wrappers copy the source VALUE verbatim, so the column-copy maintenance is
1126
- // exact. This is what lets the collation-weakening migration shape (`select b
1127
- // collate nocase as b from t`) register here with its coarsened backing key:
1128
- // the per-row upsert is keyed under the backing PK's (output) collation, so a
1129
- // colliding source row last-write-wins into the shared backing row, and a
1130
- // delete of one colliding sibling removes the shared row (the documented
1131
- // anomaly — docs/materialized-views.md § Coarsened backing keys).
1132
- const sourceAttrToCol = new Map();
1133
- const sourceDescriptor = [];
1134
- tableRef.getAttributes().forEach((a, i) => {
1135
- sourceAttrToCol.set(a.id, i);
1136
- sourceDescriptor[a.id] = i;
1137
- });
1138
- const producingByAttrId = collectProducingExprs(analyzed);
1139
- const rootAttrs = relationalAttributes(analyzed);
1140
- if (!rootAttrs)
1141
- return null; // no relational output → floor (which hard-rejects it)
1142
- const projectors = [];
1143
- for (let outCol = 0; outCol < rootAttrs.length; outCol++) {
1144
- const attr = rootAttrs[outCol];
1145
- const sourceCol = attr ? resolveValuePreservingSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
1146
- if (sourceCol !== undefined) {
1147
- projectors.push({ kind: 'passthrough', sourceCol });
1148
- continue;
1149
- }
1150
- // Computed column: a deterministic scalar over the source row. A
1151
- // non-deterministic producer is a HARD reject (the arm-specific determinism
1152
- // diagnostic must survive — so `random()` fails on *determinism*, not by silently
1153
- // falling to a still-rejected floor); a deterministic-but-unsupported *shape*
1154
- // (no resolvable producer, a subquery / cross-row reference, an async form)
1155
- // returns `null` → the floor.
1156
- const colName = attr?.name ?? `#${outCol}`;
1157
- const producing = attr ? producingByAttrId.get(attr.id) : undefined;
1158
- if (!producing)
1159
- return null;
1160
- const det = checkDeterministic(producing);
1161
- if (!det.valid) {
1162
- throw cannotMaterialize(mv.name, `it projects a non-deterministic expression column '${colName}' (${det.expression}); `
1163
- + `a row-time backing value must be reproducible from the source row`);
1164
- }
1165
- if (!isSingleRowEvaluable(producing, sourceDescriptor))
1166
- return null;
1167
- let evalFn;
1168
- try {
1169
- evalFn = compileSourceRowEvaluator(db, producing, sourceDescriptor);
1170
- }
1171
- catch {
1172
- return null; // not row-time maintainable as a single-row scalar → floor
1173
- }
1174
- projectors.push({ kind: 'expr', eval: evalFn });
1175
- }
1176
- // Every source PK column must be projected as a passthrough column so the backing
1177
- // key is a deterministic identity of the source row that `lookupCoveringConflicts`
1178
- // can invert. A PK column produced only via an expression (or not at all) breaks
1179
- // that recovery.
1180
- const passthroughSourceCols = new Set(projectors.flatMap(p => p.kind === 'passthrough' ? [p.sourceCol] : []));
1181
- for (const pk of sourcePkCols) {
1182
- if (!passthroughSourceCols.has(pk))
1183
- return null; // PK not passthrough-projected → floor
1184
- }
1185
- const backingPkDefinition = backing.primaryKeyDefinition.map(d => ({ index: d.index, desc: d.desc, collation: d.collation }));
1186
- // A computed column may never land in the backing primary key: the btree keys on
1187
- // it and `lookupCoveringConflicts` recovers the source PK from it, both of which
1188
- // require a passthrough source-column identity.
1189
- for (const d of backingPkDefinition) {
1190
- if (projectors[d.index]?.kind !== 'passthrough')
1191
- return null; // computed backing-key col → floor
1192
- }
1193
- // Partial WHERE must be evaluable on a single source row (no subqueries /
1194
- // cross-row references). `compilePredicate` throws on unsupported forms; an
1195
- // unsupported WHERE shape falls to the floor.
1196
- let predicate;
1197
- const bodyWhere = mv.derivation.selectAst.type === 'select' ? mv.derivation.selectAst.where : undefined;
1198
- if (bodyWhere) {
1199
- try {
1200
- predicate = compilePredicate(bodyWhere, sourceSchema.columns);
1201
- }
1202
- catch {
1203
- return null; // WHERE not evaluable on a single source row → floor
1204
- }
1205
- }
1206
- // ── Cost gate (incremental-maintenance-cost-gate) ──
1207
- // The covering-index shape's only structurally-sound maintenance strategy is
1208
- // 'inverse-projection' (O(1) per changed row); 'full-rebuild' is the floor for bodies
1209
- // this arm did NOT match (reached via the `null` fall-through above), so it is not a
1210
- // competitor here. Eligibility is thus a cost choice among the sound strategies (argmin
1211
- // maintenanceCost); for this shape it resolves to inverse-projection while recording the
1212
- // choice + the cost inputs the runtime reuses.
1213
- const soundStrategies = ['inverse-projection'];
1214
- const sourceStats = this.estimateMaintenanceStats(sourceSchema, projectors.length, predicate !== undefined);
1215
- // Create-time change-cardinality estimate: ~1% of the source per statement (typical OLTP).
1216
- const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
1217
- const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
1218
- // Defensive: this arm's sound set is exactly ['inverse-projection']. A different choice
1219
- // would mean the set grew without the corresponding apply-arm — fail loud rather than
1220
- // register an unexecutable plan.
1221
- if (chosenStrategy !== 'inverse-projection') {
1222
- throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
1223
- }
1224
- return {
1225
- kind: 'inverse-projection',
1226
- mv,
1227
- sourceBase,
1228
- backingSchema: mv.schemaName,
1229
- backingTableName: mv.name,
1230
- chosenStrategy,
1231
- sourceStats,
1232
- backingPkDefinition,
1233
- projectors,
1234
- predicate,
1235
- };
1236
- }
1237
- /**
1238
- * Build a `'residual-recompute'` plan for a single-source aggregate body
1239
- * (`select g1,…, agg(…) from T [where P] group by g1,…` over **bare** group columns),
1240
- * or return `null` on a shape mismatch (the caller falls through to the full-rebuild
1241
- * floor). Each changed source row belongs to exactly one group `(g1,…)`; maintaining the
1242
- * MV means recomputing that group's backing row from live state — delete the old slice,
1243
- * run the group-keyed residual, upsert the recomputed slice (zero rows when the group
1244
- * emptied). See {@link ResidualRecomputePlan} and `docs/incremental-maintenance.md`
1245
- * § residual-recompute.
1246
- *
1247
- * A **non-deterministic** group/aggregate expression is the one hard reject (the
1248
- * arm-specific determinism diagnostic must survive); every other mismatch — a scalar
1249
- * aggregate, a computed group key, a backing key that is not the group key — returns
1250
- * `null` → the floor.
1251
- *
1252
- * NOTE: the group binding is derived **directly** from the aggregate node's bare GROUP
1253
- * BY columns, not via `extractBindings`. `analyzeRowSpecific`'s `'group'` classification
1254
- * additionally requires the group key to cover a *source* unique key (so it reports
1255
- * `'global'` for the common `group by <non-key>` body), which is the wrong test here —
1256
- * the backing is keyed by the group key regardless of whether it is a source key.
1257
- */
1258
- buildAggregateResidualPlan(mv, analyzed, tableRef, sourceBase, aggregate) {
1259
- // A scalar aggregate (no GROUP BY) is one global row keyed by the empty key — no
1260
- // bounded-delta group binding, so it falls to the floor.
1261
- if (aggregate.groupBy.length === 0)
1262
- return null;
1263
- // Map T's output attributes to source column indices. T is a bare
1264
- // `TableReferenceNode`, so output-column index == source-column index.
1265
- const sourceAttrToCol = new Map();
1266
- tableRef.getAttributes().forEach((a, i) => sourceAttrToCol.set(a.id, i));
1267
- const producingByAttrId = collectProducingExprs(analyzed);
1268
- // Transitive provenance: chase output-attr → producing ColumnReference chains
1269
- // (Project-over-Aggregate adds a hop the single-hop `resolveSourceCol` cannot
1270
- // follow) until landing on a T source column, or `undefined`.
1271
- const resolveToSourceCol = (attrId) => resolveTransitiveSourceCol(attrId, sourceAttrToCol, producingByAttrId);
1272
- // Each GROUP BY expression must be a bare source column (a computed group key has
1273
- // no source-column index to bind / key the backing on) → otherwise the floor.
1274
- const groupColumns = [];
1275
- for (const expr of aggregate.groupBy) {
1276
- if (!(expr instanceof ColumnReferenceNode))
1277
- return null;
1278
- const sourceCol = sourceAttrToCol.get(expr.attributeId);
1279
- if (sourceCol === undefined)
1280
- return null;
1281
- groupColumns.push(sourceCol);
1282
- }
1283
- // Determinism: a residual must reproduce exactly what `select <body>` returns, so a
1284
- // volatile group/aggregate expression (random()/now()/volatile UDF) is a HARD reject.
1285
- for (const expr of aggregate.groupBy) {
1286
- const det = checkDeterministic(expr);
1287
- if (!det.valid)
1288
- throw cannotMaterialize(mv.name, `it groups by a non-deterministic expression (${det.expression})`);
1289
- }
1290
- for (const agg of aggregate.aggregates) {
1291
- const det = checkDeterministic(agg.expression);
1292
- if (!det.valid)
1293
- throw cannotMaterialize(mv.name, `it aggregates a non-deterministic expression (${det.expression})`);
1294
- }
1295
- // Backing table + its physical PK. The aggregate's group-key FD
1296
- // (`propagateAggregateFds`) makes the group key the backing key (via `keysOf`).
1297
- const backing = this.ctx._findTable(mv.name, mv.schemaName);
1298
- if (!backing) {
1299
- throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
1300
- }
1301
- const backingPkDefinition = backing.primaryKeyDefinition.map(d => ({ index: d.index, desc: d.desc, collation: d.collation }));
1302
- // Map each backing-PK column back to the source group column it projects, so a
1303
- // changed row's old backing-slice delete key can be built. Every backing-PK column
1304
- // MUST resolve to a GROUP BY source column — else the backing key is not the group
1305
- // key and point-keyed delete+upsert would be unsound → fall to the floor.
1306
- const rootAttrs = relationalAttributes(analyzed);
1307
- if (!rootAttrs)
1308
- return null;
1309
- const groupColumnSet = new Set(groupColumns);
1310
- const backingPkSourceCols = [];
1311
- for (const d of backingPkDefinition) {
1312
- const attr = rootAttrs[d.index];
1313
- const sourceCol = attr ? resolveToSourceCol(attr.id) : undefined;
1314
- if (sourceCol === undefined || !groupColumnSet.has(sourceCol))
1315
- return null;
1316
- backingPkSourceCols.push(sourceCol);
1317
- }
1318
- // Compile + cache the group-keyed residual once (the body with `g1 = :gk0 AND …`
1319
- // injected on T). Re-run per affected group key against the live transaction.
1320
- const relKey = `${sourceBase}#${tableRef.id ?? 'unknown'}`;
1321
- const residualScheduler = this.compileResidual(analyzed, relKey, groupColumns, 'gk');
1322
- if (!residualScheduler)
1323
- return null; // could not parameterize the residual → floor
1324
- // ── Cost gate ──
1325
- // The residual is the structurally-sound incremental arm for an aggregate body;
1326
- // 'full-rebuild' is the always-correct floor for shapes where the residual is NOT
1327
- // sound, so (as with inverse-projection) it is not a competitor here. We still
1328
- // record the chosen strategy + cost inputs for parity with the substrate.
1329
- const soundStrategies = ['residual-recompute'];
1330
- const hasPredicate = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
1331
- const sourceStats = this.estimateMaintenanceStats(tableRef.tableSchema, backing.columns.length, hasPredicate);
1332
- const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
1333
- const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
1334
- if (chosenStrategy !== 'residual-recompute') {
1335
- throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
1336
- }
1337
- return {
1338
- kind: 'residual-recompute',
1339
- mv,
1340
- sourceBase,
1341
- backingSchema: mv.schemaName,
1342
- backingTableName: mv.name,
1343
- chosenStrategy,
1344
- sourceStats,
1345
- binding: { kind: 'group', groupColumns: [...groupColumns] },
1346
- degradeToRebuild: false,
1347
- residualScheduler,
1348
- bindParamPrefix: 'gk',
1349
- bindColumns: groupColumns,
1350
- backingPkDefinition,
1351
- backingPkSourceCols,
1352
- };
1353
- }
1354
- /**
1355
- * Build a `'join-residual'` plan for a provably-1:1 row-preserving **inner/cross join**
1356
- * body (`select … from T join P on T.fk = P.id`), or return `null` on a shape mismatch
1357
- * (the caller falls through to the full-rebuild floor). The driving table `T` is the one
1358
- * whose PK the optimizer surfaced as the backing key (the 1:1 join collapses the composite
1359
- * product key to `T`'s PK); the other base ref is the lookup `P`. See {@link JoinResidualPlan}
1360
- * and `docs/incremental-maintenance.md` § join-residual.
1361
- *
1362
- * Soundness gates (a mismatch on any returns `null` → floor): exactly two base tables; no
1363
- * aggregate over the join; the backing PK is exactly `T`'s PK projected as passthrough
1364
- * columns (so each changed `T` row maps to one backing row and the reverse residual's rows
1365
- * carry the backing key); the join is provably 1:1 on `T` ({@link proveOneToOneJoin} — no
1366
- * row loss via NOT-NULL FK→PK RI, no fan-out via the join-frame `isUnique(T.pk)`); and the
1367
- * join is **inner/cross** (an outer join would make the lookup-side reverse residual unsound
1368
- * — filtering `P` drops the null-extended rows). A **non-deterministic** projection is the
1369
- * one hard reject (its arm-specific determinism diagnostic must survive).
1370
- *
1371
- * **A body WHERE is now accepted** (it is no longer a blanket reject): the predicate is
1372
- * classified by which base table(s) its columns reference (reusing the per-base-ref
1373
- * attribute→source-column maps below). A `T`-only predicate needs nothing extra — the
1374
- * forward residual already carries it and the membership set `{ T : T.fk = P.pk }` cannot
1375
- * move on a `P` write, so the lookup side stays upsert-only. A predicate referencing `P` (or
1376
- * both sides) switches the lookup side to a **delete-capable** reverse residual by building
1377
- * `lookupMembershipResidualScheduler` (the body with the WHERE stripped, keyed on `P`). See
1378
- * {@link JoinResidualPlan}'s "WHERE handling" note and {@link applyLookupResidual}.
1379
- */
1380
- buildJoinResidualPlan(mv, analyzed, tableRefs) {
1381
- // A >2-source join or an aggregate over the join has no join-residual binding → floor.
1382
- // A body WHERE is no longer rejected here — it is classified (T-only vs P-referencing)
1383
- // below, after `T`/`P` are identified, and routed to the matching lookup-side strategy.
1384
- if (tableRefs.length !== 2)
1385
- return null;
1386
- if (findAggregate(analyzed))
1387
- return null;
1388
- const backing = this.ctx._findTable(mv.name, mv.schemaName);
1389
- if (!backing) {
1390
- throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
1391
- }
1392
- const backingPkDefinition = backing.primaryKeyDefinition.map(d => ({ index: d.index, desc: d.desc, collation: d.collation }));
1393
- const rootAttrs = relationalAttributes(analyzed);
1394
- if (!rootAttrs)
1395
- return null;
1396
- const producingByAttrId = collectProducingExprs(analyzed);
1397
- // Per-base-ref attribute → source-column maps. `T` and `P` are bare
1398
- // TableReferenceNodes (output-col index == source-col index).
1399
- const refInfos = tableRefs.map(ref => {
1400
- const attrToCol = new Map();
1401
- ref.getAttributes().forEach((a, i) => attrToCol.set(a.id, i));
1402
- return { ref, attrToCol };
1403
- });
1404
- // Identify the driving table `T` as the one every backing-PK column resolves to, and
1405
- // map each backing-PK column to its `T` source column (the delete key the forward arm
1406
- // builds). A backing-PK column resolving to neither ref — or columns spanning both —
1407
- // means the backing is not keyed on a single source's PK (the join is not provably
1408
- // 1:1, or `keysOf` fell back to all-columns): reject.
1409
- let tIndex;
1410
- const backingPkSourceCols = [];
1411
- for (const d of backingPkDefinition) {
1412
- const attr = rootAttrs[d.index];
1413
- let resolvedRef;
1414
- let resolvedCol;
1415
- for (let i = 0; i < refInfos.length; i++) {
1416
- const sc = attr ? resolveTransitiveSourceCol(attr.id, refInfos[i].attrToCol, producingByAttrId) : undefined;
1417
- if (sc !== undefined) {
1418
- resolvedRef = i;
1419
- resolvedCol = sc;
1420
- break;
1421
- }
1422
- }
1423
- // A backing-PK column resolving to neither ref, or columns spanning both, means the
1424
- // backing is not keyed on a single source's PK (not provably 1:1) → fall to the floor.
1425
- if (resolvedRef === undefined)
1426
- return null;
1427
- if (tIndex === undefined)
1428
- tIndex = resolvedRef;
1429
- else if (tIndex !== resolvedRef)
1430
- return null;
1431
- backingPkSourceCols.push(resolvedCol);
1432
- }
1433
- if (tIndex === undefined)
1434
- return null;
1435
- const tRef = refInfos[tIndex].ref;
1436
- const pRef = refInfos[tIndex === 0 ? 1 : 0].ref;
1437
- const tSchema = tRef.tableSchema;
1438
- const pSchema = pRef.tableSchema;
1439
- const sourceBase = `${tSchema.schemaName}.${tSchema.name}`.toLowerCase();
1440
- const lookupBase = `${pSchema.schemaName}.${pSchema.name}`.toLowerCase();
1441
- if (sourceBase === lookupBase)
1442
- return null; // self-join → floor
1443
- // The backing key must be EXACTLY the driving source's PK (each `T` row → one backing
1444
- // row). `keysOf` surfaced `T.pk` for the 1:1 join; verify it resolved to a real PK key
1445
- // (not the all-columns fallback) by set-equality with `T`'s declared PK.
1446
- const tPkCols = tSchema.primaryKeyDefinition.map(d => d.index);
1447
- if (tPkCols.length === 0)
1448
- return null;
1449
- const backingPkSet = new Set(backingPkSourceCols);
1450
- if (backingPkSet.size !== tPkCols.length || !tPkCols.every(c => backingPkSet.has(c)))
1451
- return null;
1452
- // Prove the join is 1:1 on `T` (no row loss + no fan-out), reusing the coverage
1453
- // prover's shared join predicates over the analyzed body. A fanning / non-1:1 join
1454
- // falls to the floor.
1455
- const root = rootRelationalNode(analyzed);
1456
- if (!root)
1457
- return null;
1458
- const proof = proveOneToOneJoin(root, tSchema);
1459
- if (!proof.ok)
1460
- return null;
1461
- // Restrict to inner/cross: the lookup-side reverse residual filters `P`, which would
1462
- // drop an outer join's null-extended rows (unsound). An outer 1:1 join falls to the floor.
1463
- const topJoin = proof.topJoin;
1464
- const joinType = topJoin && CapabilityDetectors.isJoin(topJoin) ? topJoin.getJoinType() : undefined;
1465
- if (joinType !== 'inner' && joinType !== 'cross')
1466
- return null;
1467
- // Determinism: the residual must reproduce exactly what `select <body>` returns, so a
1468
- // volatile projection (random()/now()/volatile UDF) is a HARD reject.
1469
- for (const attr of rootAttrs) {
1470
- const producing = attr ? producingByAttrId.get(attr.id) : undefined;
1471
- if (!producing)
1472
- continue; // a bare passthrough column has no producing expr to check
1473
- const det = checkDeterministic(producing);
1474
- if (!det.valid) {
1475
- throw cannotMaterialize(mv.name, `it projects a non-deterministic expression (${det.expression}); a row-time backing value must be reproducible from the source rows`);
1476
- }
1477
- }
1478
- // Forward (`T`) residual: the body with `T.pk = :pk0 AND …` injected on `T`. Recomputes
1479
- // the one joined row for a changed `T` row (delegated to `applyForwardResidual`).
1480
- const tRelKey = `${sourceBase}#${tRef.id ?? 'unknown'}`;
1481
- const forwardResidual = this.compileResidual(analyzed, tRelKey, tPkCols, 'pk');
1482
- if (!forwardResidual)
1483
- return null;
1484
- // Reverse (`P`) **in-scope** residual: the body — WHERE retained — with `P.pk = :pk0 AND …`
1485
- // injected on `P`. Drives lookup-side maintenance — finds every currently in-scope joined
1486
- // row referencing a changed `P` row.
1487
- const pPkCols = pSchema.primaryKeyDefinition.map(d => d.index);
1488
- if (pPkCols.length === 0)
1489
- return null;
1490
- const pRelKey = `${lookupBase}#${pRef.id ?? 'unknown'}`;
1491
- const reverseResidual = this.compileResidual(analyzed, pRelKey, pPkCols, 'pk');
1492
- if (!reverseResidual)
1493
- return null;
1494
- // Classify the body WHERE by which base table(s) its columns reference: a predicate that
1495
- // references `P` (or both sides) can flip a row's WHERE membership on a `P` write, so the
1496
- // lookup side must become delete-capable; a `T`-only predicate cannot move membership, so
1497
- // the upsert-only reverse residual above stays sound. The membership residual is the body
1498
- // with the WHERE **stripped** and the key filter on `P` — it returns every currently
1499
- // referencing `T` row (regardless of scope) so its backing key can be deleted before the
1500
- // in-scope survivors are re-upserted. Absent for a no-WHERE / `T`-only-WHERE body.
1501
- const hasWhere = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
1502
- // A volatile WHERE would make every residual (which embeds it) irreproducible → fall to
1503
- // the floor's pragma-gated whole-body determinism reject, not an unsound bounded-delta arm.
1504
- if (hasWhere && bodyWhereIsNonDeterministic(analyzed))
1505
- return null;
1506
- const whereReferencesLookup = hasWhere
1507
- && bodyWhereReferencesLookup(analyzed, refInfos[tIndex].attrToCol, producingByAttrId);
1508
- let lookupMembershipResidual;
1509
- if (whereReferencesLookup) {
1510
- const membership = this.compileLookupMembershipResidual(mv, lookupBase, pPkCols);
1511
- if (!membership)
1512
- return null; // could not strip + re-key the membership residual → floor
1513
- lookupMembershipResidual = membership;
1514
- }
1515
- // ── Cost gate (parity with the other residual arms) ──
1516
- const soundStrategies = ['residual-recompute'];
1517
- const sourceStats = this.estimateMaintenanceStats(tSchema, backing.columns.length, hasWhere);
1518
- const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
1519
- const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
1520
- if (chosenStrategy !== 'residual-recompute') {
1521
- throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
1522
- }
1523
- return {
1524
- kind: 'join-residual',
1525
- mv,
1526
- sourceBase,
1527
- backingSchema: mv.schemaName,
1528
- backingTableName: mv.name,
1529
- chosenStrategy,
1530
- sourceStats,
1531
- binding: { kind: 'row', keyColumns: [...tPkCols] },
1532
- degradeToRebuild: false,
1533
- residualScheduler: forwardResidual,
1534
- bindParamPrefix: 'pk',
1535
- bindColumns: tPkCols,
1536
- backingPkDefinition,
1537
- backingPkSourceCols,
1538
- lookupBase,
1539
- lookupResidualScheduler: reverseResidual,
1540
- lookupMembershipResidualScheduler: lookupMembershipResidual,
1541
- lookupBindColumns: pPkCols,
1542
- lookupBindParamPrefix: 'pk',
1543
- };
1544
- }
1545
- /**
1546
- * Compile the **lookup membership** residual for the join-residual arm's delete-capable
1547
- * lookup side: the MV body with its top-level WHERE **stripped** (membership only) and a
1548
- * key-equality filter injected on the lookup `P`, keyed `pk0…`. The WHERE is stripped at the
1549
- * AST level (a shallow clone dropping `where`) and the body re-built + re-analyzed, so only
1550
- * the WHERE is removed — the join, its `ON` condition, and any projection sub-expressions are
1551
- * preserved. Re-analysis assigns fresh node ids, so `P`'s reference is re-located by base name
1552
- * to compute the injection target. Returns `null` if the lookup ref or the key-filter
1553
- * injection could not be resolved (the caller then falls to the full-rebuild floor).
1554
- *
1555
- * Run per affected `P` key, this residual returns **every** `T` row currently joined to `P`
1556
- * via the join's `ON` condition — irrespective of the WHERE — so {@link applyLookupResidual}
1557
- * can delete each one's `T.pk` backing key before the in-scope residual re-upserts the
1558
- * survivors (the membership set the WHERE-bearing reverse residual would otherwise never
1559
- * shrink).
1560
- */
1561
- compileLookupMembershipResidual(mv, lookupBase, pPkCols) {
1562
- const db = this.ctx;
1563
- const strippedAst = { ...mv.derivation.selectAst, where: undefined };
1564
- const stripped = db.schemaManager.withSuppressedMaterializedViewRewrite(() => {
1565
- const { plan } = this.ctx._buildPlan([strippedAst]);
1566
- return this.ctx.optimizer.optimizeForAnalysis(plan, db);
1567
- });
1568
- // Re-locate `P` in the WHERE-stripped plan by base name (fresh node ids) to build the
1569
- // injection target key the way `compileResidual`'s callers do.
1570
- let pRelKey;
1571
- for (const [relKey, ref] of collectTableRefs(stripped)) {
1572
- if (`${ref.tableSchema.schemaName}.${ref.tableSchema.name}`.toLowerCase() === lookupBase) {
1573
- pRelKey = relKey;
1574
- break;
1575
- }
1576
- }
1577
- if (pRelKey === undefined)
1578
- return null;
1579
- return this.compileResidual(stripped, pRelKey, pPkCols, 'pk');
1580
- }
1581
- /**
1582
- * Build a `'full-rebuild'` plan — the always-correct floor — for an MV whose body matches
1583
- * no bounded-delta arm, or throw with a non-shape diagnostic. This is the fall-through
1584
- * builder {@link tryBuildBoundedDeltaArm} routes to on a `null` (no bounded-delta arm fits).
1585
- * See {@link FullRebuildPlan} and `docs/materialized-views.md` § Full-rebuild floor /
1586
- * § Primary key inference.
1587
- *
1588
- * Create-time rejections (none shape-based — the floor accepts general bodies):
1589
- * - **bag** body with no provable unique key (`keysOf` over the optimized body root is
1590
- * empty) — there is no row identity to materialize on. `keysOf` already gates its
1591
- * all-columns fallback on `isSet`, so a non-empty result is a real key (a true column
1592
- * key OR the all-columns key of a provable set) and an empty result is exactly a bag.
1593
- * (A duplicate-producing body usually fails the set contract earlier, at create-fill.)
1594
- * - **non-deterministic** body (any `random()`/`now()`/volatile UDF anywhere in the plan)
1595
- * without `pragma nondeterministic_schema` — no maintenance could keep it equal to its
1596
- * plain view (mirrors the per-arm determinism rejects and the DDL determinism gate);
1597
- * - body with **no relational output** (degenerate);
1598
- * - **size**: full-rebuild is the only sound strategy *and* the **largest** participating
1599
- * source exceeds the `materialized_view_rebuild_row_threshold` option
1600
- * ({@link isFullRebuildPathological}) — every DML write would re-scan that source.
1601
- * `0` disables the size reject (accept any size).
1602
- */
1603
- buildFullRebuildPlan(mv, analyzed) {
1604
- const db = this.ctx;
1605
- // Optimize the whole body ONCE — read-side MV rewrite suppressed so it reads its
1606
- // sources, not the backing it populates — then derive the body's key + determinism
1607
- // from, and compile its scheduler from, the SAME optimized plan.
1608
- const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => this.ctx.optimizer.optimize(analyzed, db));
1609
- const root = rootRelationalNode(optimized);
1610
- if (!root)
1611
- throw cannotMaterialize(mv.name, 'its body produced no relational output');
1612
- // Backing key = the body's provable unique key. A bag (no provable key — a key-dropping
1613
- // projection, a `union all` of overlapping inputs, …) has no row identity to key a
1614
- // materialization on, so it must be a set. An all-columns pseudo-key is admitted only
1615
- // when the body is provably a set (`keysOf` gates it on `isSet`); a bag with an
1616
- // all-columns "key" still resolves to empty here and rejects, else duplicates would
1617
- // collide on the backing key.
1618
- //
1619
- // One carve-out: a keyless body whose source key survives through value-preserving
1620
- // passthrough lineage (the parallel-migration collation-weakening shape) is keyed on
1621
- // the COARSENED lineage key instead of rejected — the same `deriveCoarsenedBackingKey`
1622
- // derivation `deriveBackingShape` keyed the backing with, over the same fully-optimized
1623
- // body, so the two agree by construction. Colliding rows then last-write-win under the
1624
- // floor's collation-keyed `replace-all` diff (docs/materialized-views.md § Coarsened
1625
- // backing keys); the create emitter owns the key-coarsening warning.
1626
- if (keysOf(root).length === 0 && deriveCoarsenedBackingKey(root) === undefined) {
1627
- throw cannotMaterialize(mv.name, 'its body has no provable unique key — it is a bag (e.g. a key-dropping '
1628
- + 'projection or a `union all` of overlapping inputs), so it must be a set');
1629
- }
1630
- // Whole-body determinism: a non-deterministic body can never be kept equal to its
1631
- // plain view by any maintenance, so it is a hard reject unless the schema-determinism
1632
- // gate is lifted. Mirrors the per-arm determinism rejects (and the DDL gate).
1633
- if (!db.options.getBooleanOption('nondeterministic_schema')) {
1634
- const nonDet = findNonDeterministic(analyzed);
1635
- if (nonDet) {
1636
- throw cannotMaterialize(mv.name, `its body is non-deterministic (${nonDet}); a materialized view body must be `
1637
- + 'reproducible to stay equal to its plain view (set `pragma nondeterministic_schema` to override)');
1638
- }
1639
- }
1640
- // Every source the body reads (set-op legs, every join source, …) so a write to any of
1641
- // them triggers a rebuild. Collected from the (pre-physical) analyzed plan, where every
1642
- // source is a bare `TableReferenceNode` — the optimized plan may have wrapped them in
1643
- // physical access nodes.
1644
- const tableRefs = [...collectTableRefs(analyzed).values()];
1645
- const sourceBases = [...new Set(tableRefs.map(ref => `${ref.tableSchema.schemaName}.${ref.tableSchema.name}`.toLowerCase()))];
1646
- if (sourceBases.length === 0)
1647
- throw cannotMaterialize(mv.name, 'its body reads no source table');
1648
- const backing = this.ctx._findTable(mv.name, mv.schemaName);
1649
- if (!backing) {
1650
- throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
1651
- }
1652
- // ── Cost gate + size reject ──
1653
- // Full-rebuild is the floor: an EMPTY sound set resolves to it (`selectMaintenanceStrategy`).
1654
- // Cost the rebuild against the LARGEST participating source — every write re-evaluates the
1655
- // whole body, so the largest source it scans governs whether the per-write rebuild is
1656
- // pathological (e.g. a tiny driving table joined to a huge lookup gates on the lookup).
1657
- // Re-resolve each source's CURRENT schema (the analyzed plan node may carry a pre-`analyze`
1658
- // snapshot whose `statistics` predates the latest counts) so the size gate reflects the
1659
- // live source size at create time.
1660
- const statsProvider = this.ctx.optimizer.getStats();
1661
- let largestSchema = tableRefs[0].tableSchema;
1662
- let largestRows = -1;
1663
- for (const ref of tableRefs) {
1664
- const live = this.liveSourceSchema(ref);
1665
- const rows = statsProvider.tableRows(live) ?? DEFAULT_SOURCE_ROWS;
1666
- if (rows > largestRows) {
1667
- largestRows = rows;
1668
- largestSchema = live;
1669
- }
1670
- }
1671
- const sourceStats = this.estimateMaintenanceStats(largestSchema, backing.columns.length, /*hasPredicate*/ false);
1672
- // Size reject: full-rebuild is the only sound strategy here, so a source past the
1673
- // configurable threshold makes every write pathological. `0` disables the reject.
1674
- const rebuildThreshold = db.options.getNumberOption('materialized_view_rebuild_row_threshold');
1675
- if (isFullRebuildPathological(sourceStats, rebuildThreshold)) {
1676
- const largestBase = `${largestSchema.schemaName}.${largestSchema.name}`.toLowerCase();
1677
- throw cannotMaterialize(mv.name, `its only sound maintenance strategy is a full body rebuild, but its largest source '${largestBase}' has `
1678
- + `~${sourceStats.tableRows} rows, over the materialized_view_rebuild_row_threshold (${rebuildThreshold}) — `
1679
- + `every write would re-scan it. Raise or disable the threshold `
1680
- + `(\`pragma materialized_view_rebuild_row_threshold = 0\`)`);
1681
- }
1682
- // Compile the whole optimized body once into a reusable scheduler (no key filter).
1683
- const bodyScheduler = new Scheduler(emitPlanNode(optimized, new EmissionContext(db)));
1684
- const chosenStrategy = selectMaintenanceStrategy([], Math.max(1, sourceStats.tableRows * 0.01), sourceStats);
1685
- if (chosenStrategy !== 'full-rebuild') {
1686
- throw new QuereusError(`Internal error: cost gate selected '${chosenStrategy}' for the full-rebuild floor of materialized view '${mv.name}'`, StatusCode.INTERNAL);
1687
- }
1688
- return {
1689
- kind: 'full-rebuild',
1690
- mv,
1691
- sourceBase: sourceBases[0],
1692
- backingSchema: mv.schemaName,
1693
- backingTableName: mv.name,
1694
- chosenStrategy,
1695
- sourceStats,
1696
- bodyScheduler,
1697
- sourceBases,
1698
- };
1699
- }
1700
- /**
1701
- * Compile the key-filtered residual for a binding into a reusable {@link Scheduler}:
1702
- * the analyzed body with a key-equality filter injected on `T`'s `TableReferenceNode`
1703
- * (parameterized `${paramPrefix}0…`), then optimized + emitted. Mirrors the assertion
1704
- * evaluator's residual compilation (`database-assertions.ts`) so the two cannot drift.
1705
- * Returns `null` if `injectKeyFilter` could not target `T` (the arm builder then falls
1706
- * through to the full-rebuild floor).
1707
- */
1708
- compileResidual(analyzed, relKey, bindColumns, paramPrefix) {
1709
- const db = this.ctx;
1710
- const rewritten = injectKeyFilter(analyzed, relKey, bindColumns, paramPrefix);
1711
- if (rewritten === analyzed)
1712
- return null; // could not parameterize the residual → floor
1713
- // Suppress the read-side rewrite: the residual is the MV's own body (+ a key
1714
- // filter) compiled to maintain its backing, so it must stay over the source.
1715
- const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => this.ctx.optimizer.optimize(rewritten, db));
1716
- const instruction = emitPlanNode(optimized, new EmissionContext(db));
1717
- return new Scheduler(instruction);
1718
- }
1719
- /**
1720
- * Execute a cached key-filtered residual for one affected key tuple, returning its
1721
- * result rows (0 or 1 for the aggregate shape; 0..N for the lateral-TVF fan-out shape).
1722
- * Bound through a fresh {@link RuntimeContext} on the live `db` so the residual's source
1723
- * scan reuses `T`'s transaction connection and reads this statement's pending writes
1724
- * (reads-own-writes) — the synchronous analogue of
1725
- * `database-assertions.ts:executeResidualPerTuple`. Shared by the residual-recompute
1726
- * (`'gk'`) and prefix-delete (`'pk'`) arms.
1727
- */
1728
- async runResidual(residualScheduler, bindParamPrefix, keyTuple) {
1729
- const params = {};
1730
- for (let i = 0; i < keyTuple.length; i++) {
1731
- params[`${bindParamPrefix}${i}`] = keyTuple[i];
1732
- }
1733
- return this.runScheduler(residualScheduler, params);
1734
- }
1735
- /**
1736
- * Run a cached maintenance scheduler to completion against **live mid-transaction source
1737
- * state** and collect its result rows. Bound through a fresh strict {@link RuntimeContext}
1738
- * on the live `db` so the scan reuses the source's transaction connection and reads this
1739
- * statement's pending writes (reads-own-writes). The no-`stmt`, fresh-context shape is the
1740
- * synchronous analogue of `database-assertions.ts:executeResidualPerTuple`. Shared by the
1741
- * key-filtered residual arms ({@link runResidual}, parameterized) and the whole-body
1742
- * full-rebuild arm ({@link applyFullRebuild}, no params).
1743
- */
1744
- async runScheduler(scheduler, params) {
1745
- const rctx = {
1746
- db: this.ctx,
1747
- stmt: undefined,
1748
- params,
1749
- context: createStrictRowContextMap(),
1750
- tableContexts: wrapTableContextsStrict(new Map()),
1751
- enableMetrics: false,
1752
- };
1753
- const result = await scheduler.run(rctx);
1754
- const rows = [];
1755
- if (isAsyncIterable(result)) {
1756
- for await (const r of result)
1757
- rows.push(r);
1758
- }
1759
- return rows;
1760
- }
1761
753
  /**
1762
754
  * Maintain a `'full-rebuild'` MV: re-evaluate the **whole** body against live
1763
755
  * mid-transaction source state and replace the backing transactionally. Run the cached
1764
756
  * {@link FullRebuildPlan.bodyScheduler} to completion (no params — reads-own-writes via
1765
757
  * the same fresh-context path the residual arms use), collect every recomputed row, and
1766
- * apply a single `'replace-all'` {@link MaintenanceOp}: a keyed diff (by backing PK) of
758
+ * apply a single `'replace-all'` MaintenanceOp: a keyed diff (by backing PK) of
1767
759
  * the recomputed rows against the backing's current pending-layer contents (insert/
1768
760
  * update/delete, identical rows skipped). The diff rides the backing's **pending**
1769
761
  * `TransactionLayer`, so it commits/rolls-back in lockstep with the source write, and the
@@ -1773,557 +765,20 @@ export class MaterializedViewManager {
1773
765
  * rebuilds wholesale. It is therefore deferred to a single end-of-statement flush
1774
766
  * ({@link flushDeferredRebuilds}) rather than run per source row, so a bulk statement
1775
767
  * rebuilds exactly once; this is that one rebuild. An empty body (zero rows) yields a
1776
- * `'replace-all' []`, which empties the backing.
768
+ * `'replace-all' []`, which empties the backing. Kept on the manager (delegating the
769
+ * scan / backing write to the apply-module free helpers) so the per-statement-flush
770
+ * deferral suite can instrument it to count rebuilds.
1777
771
  */
1778
772
  async applyFullRebuild(plan, cache) {
1779
- const rows = await this.runScheduler(plan.bodyScheduler, {});
773
+ const rows = await runScheduler(this.ctx, plan.bodyScheduler, {});
1780
774
  const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
1781
775
  if (!backing) {
1782
776
  throw new QuereusError(`Internal error: backing table '${plan.backingTableName}' for materialized view '${plan.mv.name}' not found`, StatusCode.INTERNAL);
1783
777
  }
1784
- const host = this.backingHost(backing);
1785
- const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
778
+ const host = backingHost(this.ctx, backing);
779
+ const connection = await getBackingConnection(this.ctx, host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
1786
780
  return host.applyMaintenance(connection, [{ kind: 'replace-all', rows }]);
1787
781
  }
1788
- /**
1789
- * Compute a **forward** key-filtered residual plan's per-row backing delta and apply it:
1790
- * derive the affected binding key(s) from the changed row (OLD ∪ NEW, deduped), re-run
1791
- * the key-filtered residual against live source state for each, and apply the **keyed
1792
- * diff**: a non-empty recomputed slice is upserted (the backing key IS the affected key,
1793
- * so the upsert replaces the old row wholesale — no delete-first — and the host's
1794
- * value-identical upsert skip turns a no-op recompute into ZERO effective changes
1795
- * instead of delete+insert churn); an emptied slice (residual returns nothing) emits the
1796
- * point delete, removing the stale backing row (nothing reported if it was already
1797
- * absent). Returns the effective {@link BackingRowChange}(s) the backing layer realized,
1798
- * for the MV-over-MV cascade — a real same-key change now reports one `update`.
1799
- *
1800
- * Shared by the single-source aggregate (`'residual-recompute'`, group key, ≤1 row per
1801
- * key) and the 1:1-join (`'join-residual'`, the driving table `T`'s PK, exactly the one
1802
- * joined row per key) arms — both bind on the forward driving source via
1803
- * {@link ForwardResidualPlan}; the only difference is the binding (group vs PK).
1804
- *
1805
- * Per-row recompute is correct without per-statement batching: every change to a key
1806
- * triggers a full recompute of that key's slice from live (reads-own-writes) state, so
1807
- * the last change to touch a key writes the authoritative backing row. Batching/dedup
1808
- * across a whole statement is an affordability optimization deferred with the
1809
- * statement-flush boundary (see the ticket handoff).
1810
- */
1811
- async applyForwardResidual(plan, change, cache) {
1812
- // Distinct affected keys (OLD ∪ NEW), deduped on the backing-key values: a
1813
- // non-key-changing update recomputes the group once; a key-changing update
1814
- // recomputes both the old and the new group.
1815
- const affected = new Map();
1816
- const addFrom = (row) => {
1817
- const keyVals = plan.backingPkSourceCols.map(sc => row[sc]);
1818
- const dedupKey = canonKeyValues(keyVals);
1819
- if (affected.has(dedupKey))
1820
- return;
1821
- affected.set(dedupKey, {
1822
- keyTuple: plan.bindColumns.map(c => row[c]),
1823
- keyVals,
1824
- deleteKey: buildPrimaryKeyFromValues(keyVals, plan.backingPkDefinition),
1825
- });
1826
- };
1827
- if (change.op === 'insert')
1828
- addFrom(change.newRow);
1829
- else if (change.op === 'delete')
1830
- addFrom(change.oldRow);
1831
- else {
1832
- addFrom(change.oldRow);
1833
- addFrom(change.newRow);
1834
- }
1835
- const ops = [];
1836
- for (const { keyTuple, keyVals, deleteKey } of affected.values()) {
1837
- const recomputed = await this.runResidual(plan.residualScheduler, plan.bindParamPrefix, keyTuple);
1838
- // Keep only the recomputed rows whose backing key equals the affected key.
1839
- // The residual for key K must only contribute K's slice; any other row is
1840
- // spurious and is dropped. This is the soundness net for an emptied group: when
1841
- // no source row matches the key, a *correct* grouped residual returns zero rows,
1842
- // but a constant-pinned multi-column grouped aggregate is mis-collapsed by the
1843
- // optimizer into a *scalar* aggregate that emits one all-NULL `count=0` row over
1844
- // the empty input (a pre-existing optimizer bug, filed separately as
1845
- // `fix/optimizer-constant-group-aggregate-empty-input-spurious-row`). That row's
1846
- // key ≠ K, so it is filtered here and the delete-without-upsert correctly removes
1847
- // the emptied group's backing row.
1848
- const slice = recomputed.filter(row => this.residualRowMatchesKey(plan, row, keyVals));
1849
- if (slice.length === 0) {
1850
- // Emptied slice: delete-without-upsert removes the stale backing row (the
1851
- // host reports nothing if the key was already absent).
1852
- ops.push({ kind: 'delete-key', key: deleteKey });
1853
- }
1854
- else {
1855
- // The slice shares the affected backing key, so each upsert REPLACES the old
1856
- // backing row — no delete-first — and the host's value-identical skip
1857
- // (vtab/backing-host.ts) suppresses a recompute that changed nothing.
1858
- for (const row of slice)
1859
- ops.push({ kind: 'upsert', row });
1860
- }
1861
- }
1862
- if (ops.length === 0)
1863
- return [];
1864
- const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
1865
- if (!backing) {
1866
- throw new QuereusError(`Internal error: backing table '${plan.backingTableName}' for materialized view '${plan.mv.name}' not found`, StatusCode.INTERNAL);
1867
- }
1868
- const host = this.backingHost(backing);
1869
- const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
1870
- return host.applyMaintenance(connection, ops);
1871
- }
1872
- /**
1873
- * True iff `row`'s backing primary-key columns equal `keyVals` (the affected binding
1874
- * key, in `backingPkDefinition` order), under each column's collation. Used to keep
1875
- * only the residual row(s) belonging to the recomputed key — see
1876
- * {@link applyForwardResidual}.
1877
- */
1878
- residualRowMatchesKey(plan, row, keyVals) {
1879
- for (let i = 0; i < plan.backingPkDefinition.length; i++) {
1880
- const d = plan.backingPkDefinition[i];
1881
- if (compareSqlValues(row[d.index], keyVals[i], d.collation) !== 0)
1882
- return false;
1883
- }
1884
- return true;
1885
- }
1886
- /**
1887
- * Dispatch a `'join-residual'` plan on **which source changed**. A write to the driving
1888
- * table `T` (`changedBase === plan.sourceBase`) is the forward case — recompute the one
1889
- * joined row keyed on `T`'s PK, identical to a size-1 `'row'`-binding residual — so it
1890
- * delegates straight to {@link applyForwardResidual} (delete old backing slice → run the
1891
- * `T`-keyed residual → upsert). A write to the lookup table `P` is the reverse case,
1892
- * handled by {@link applyLookupResidual}.
1893
- */
1894
- async applyJoinResidual(plan, change, changedBase, cache) {
1895
- if (changedBase === plan.sourceBase) {
1896
- return this.applyForwardResidual(plan, change, cache);
1897
- }
1898
- return this.applyLookupResidual(plan, change, cache);
1899
- }
1900
- /**
1901
- * Maintain a `'join-residual'` MV for a **lookup-side (`P`)** change: refresh the joined
1902
- * rows referencing each affected `P` key. Derive the affected `P` key(s) from the changed
1903
- * row (OLD ∪ NEW, deduped on `P`'s PK), and for each run the in-scope lookup-keyed residual
1904
- * (`… where P.pk = :pk0`, the body's WHERE retained) against live source state — returning
1905
- * every currently in-scope joined row, each carrying its `T.pk` backing key — and **upsert**
1906
- * each.
1907
- *
1908
- * **Upsert-only is sound for a no-WHERE / `T`-only-WHERE body.** For an inner/cross join with
1909
- * enforced RI and a predicate that cannot reference `P`, the *set* of backing rows referencing
1910
- * a given `P` row is `{ T : T.fk = P.pk }`, determined entirely by `T.fk` (a `T` column the
1911
- * `P` write cannot change), and the WHERE — over `T` only — cannot flip on a `P` write. So a
1912
- * `P` change can only re-derive the lookup-projected columns of those existing backing rows
1913
- * (an upsert at the unchanged `T.pk` key), never add or remove one: a `P` insert with no
1914
- * referencing `T` rows yields an empty residual (no-op); a `P` delete is only admissible (RI)
1915
- * when no `T` references it (empty residual); a `P` payload update upserts the affected rows
1916
- * with the new value.
1917
- *
1918
- * **A `P`-referencing WHERE needs the delete-capable pass.** When the body WHERE references
1919
- * `P`, a `P` write can flip a joined row's WHERE truth and so add or remove its backing row —
1920
- * which the in-scope upsert above (it returns *only* in-scope rows) could never delete. The
1921
- * builder then supplies `lookupMembershipResidualScheduler` (the body with the WHERE stripped,
1922
- * keyed on `P`). Per affected `P` key this runs both residuals against the same live state and
1923
- * applies the **keyed diff**: it **deletes** only the membership keys the in-scope recompute no
1924
- * longer produces (rows that left scope — the delete keys come from live `T` via the join, so
1925
- * they match existing backing keys and touch nothing belonging to another `P`; membership and
1926
- * in-scope rows read the same live state, so their key bytes match exactly), and **upserts**
1927
- * every in-scope row. A row leaving scope is deleted (removed); a row entering scope is
1928
- * upserted (added); an unchanged in-scope row's upsert is suppressed by the host's
1929
- * value-identical skip (vtab/backing-host.ts) — ZERO effective changes instead of the former
1930
- * delete+insert refresh churn; a changed in-scope row reports one `update`. The membership
1931
- * residual MUST ignore the WHERE — else a row leaving scope would never be deleted.
1932
- *
1933
- * A `T`-side membership change (insert/delete/FK-move) is the *forward* path's job and fires
1934
- * its own maintenance. Returns the effective {@link BackingRowChange}(s) for the MV-over-MV
1935
- * cascade. Per-row recompute is correct without batching for the same
1936
- * last-write-wins-against-live-state reason as {@link applyForwardResidual}.
1937
- */
1938
- async applyLookupResidual(plan, change, cache) {
1939
- // Distinct affected lookup keys (OLD ∪ NEW), deduped on `P`'s PK values.
1940
- const affected = new Map();
1941
- const addFrom = (row) => {
1942
- const keyTuple = plan.lookupBindColumns.map(c => row[c]);
1943
- const dedupKey = canonKeyValues(keyTuple);
1944
- if (!affected.has(dedupKey))
1945
- affected.set(dedupKey, keyTuple);
1946
- };
1947
- if (change.op === 'insert')
1948
- addFrom(change.newRow);
1949
- else if (change.op === 'delete')
1950
- addFrom(change.oldRow);
1951
- else {
1952
- addFrom(change.oldRow);
1953
- addFrom(change.newRow);
1954
- }
1955
- const ops = [];
1956
- for (const keyTuple of affected.values()) {
1957
- const recomputed = await this.runResidual(plan.lookupResidualScheduler, plan.lookupBindParamPrefix, keyTuple);
1958
- // Delete-capable (P-referencing WHERE): keyed diff of the membership residual
1959
- // (WHERE stripped) against the in-scope recompute — delete ONLY the membership
1960
- // keys the recompute no longer produces (rows that left the WHERE scope), not
1961
- // every member. Both residuals read the same live state, so a surviving row's
1962
- // key bytes match exactly (the byte-canonical set lookup is exact). Deletes
1963
- // precede upserts, preserving the prior arm's ordering discipline.
1964
- if (plan.lookupMembershipResidualScheduler) {
1965
- const produced = new Set(recomputed.map(row => canonKeyValues(plan.backingPkDefinition.map(d => row[d.index]))));
1966
- const members = await this.runResidual(plan.lookupMembershipResidualScheduler, plan.lookupBindParamPrefix, keyTuple);
1967
- for (const row of members) {
1968
- const keyVals = plan.backingPkDefinition.map(d => row[d.index]);
1969
- if (produced.has(canonKeyValues(keyVals)))
1970
- continue; // still in scope — upserted below
1971
- ops.push({ kind: 'delete-key', key: buildPrimaryKeyFromValues(keyVals, plan.backingPkDefinition) });
1972
- }
1973
- }
1974
- // Upsert every in-scope row; the host's value-identical skip suppresses the
1975
- // unchanged ones (an in-scope refresh that changed nothing reports nothing).
1976
- for (const row of recomputed)
1977
- ops.push({ kind: 'upsert', row });
1978
- }
1979
- if (ops.length === 0)
1980
- return [];
1981
- const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
1982
- if (!backing) {
1983
- throw new QuereusError(`Internal error: backing table '${plan.backingTableName}' for materialized view '${plan.mv.name}' not found`, StatusCode.INTERNAL);
1984
- }
1985
- const host = this.backingHost(backing);
1986
- const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
1987
- return host.applyMaintenance(connection, ops);
1988
- }
1989
- /**
1990
- * Build a `'prefix-delete'` plan for a single-source lateral-TVF fan-out body
1991
- * (`select T.pk…, …, f.* from T cross join lateral tvf(<args over T>) f`), or return
1992
- * `null` on a shape mismatch (the caller falls through to the full-rebuild floor). The
1993
- * backing PK is the composite product key `(T.pk ∪ tvf-key)` that `keysOf` advertises
1994
- * through the lateral join; the base PK is its leading prefix. See {@link PrefixDeletePlan}
1995
- * and `docs/incremental-maintenance.md` § prefix-delete.
1996
- *
1997
- * Soundness gates (a mismatch on any returns `null` → floor): exactly one lateral TVF and
1998
- * one join (no nested/multi TVF, no aggregate over the fan-out); the TVF advertises a
1999
- * per-call key; the base PK projected and forming the **leading prefix** of the backing PK
2000
- * with a non-empty TVF-key tail (so each base row's fan-out rows are individually
2001
- * addressable and a by-prefix delete selects exactly one base row's slice). An `order by`
2002
- * over the fan-out that reorders the composite key so the base PK no longer leads is a
2003
- * `null` fall-through (the floor maintains it wholesale). The body's WHERE, if any, is part
2004
- * of the residual (so an out-of-scope base row fans out to zero rows), exactly as in the
2005
- * aggregate arm. A **non-deterministic** TVF / argument is the one hard reject (its
2006
- * arm-specific determinism diagnostic must survive).
2007
- */
2008
- buildLateralTvfPrefixDeletePlan(mv, analyzed, tableRef, sourceBase) {
2009
- // Exactly one lateral TVF and one join. A second base table is already excluded by
2010
- // the single-source check upstream; this falls to the floor for a second TVF / chained
2011
- // lateral join (`t join lateral tvf1 join lateral tvf2`).
2012
- if (countNodeType(analyzed, PlanNodeType.TableFunctionCall) !== 1)
2013
- return null;
2014
- if (countJoins(analyzed) !== 1)
2015
- return null;
2016
- // An aggregate over the fan-out is a different shape → floor (the TVF route is taken
2017
- // before the aggregate route, so an `... group by` over a lateral TVF lands here).
2018
- if (findAggregate(analyzed))
2019
- return null;
2020
- // Determinism: a residual must reproduce exactly what `select <body>` returns, so a
2021
- // volatile TVF (or a volatile argument expression) is a HARD reject.
2022
- const tvf = findTableFunctionCall(analyzed);
2023
- if (!tvf) {
2024
- // Unreachable — countNodeType(...) === 1 above guarantees one exists.
2025
- throw new QuereusError(`Internal error: lateral TVF node not found for materialized view '${mv.name}'`, StatusCode.INTERNAL);
2026
- }
2027
- if (tvf.physical.deterministic === false) {
2028
- throw cannotMaterialize(mv.name, `it fans out through a non-deterministic table-valued function '${tvf.functionName}' (a row-time fan-out must be reproducible from the base row)`);
2029
- }
2030
- for (const operand of tvf.operands) {
2031
- const det = checkDeterministic(operand);
2032
- if (!det.valid)
2033
- throw cannotMaterialize(mv.name, `it passes a non-deterministic argument (${det.expression}) to the lateral table-valued function`);
2034
- }
2035
- // The lateral TVF must advertise a per-call key, so the composite product key is a
2036
- // real column key `(base PK ∪ TVF key)` rather than the all-columns / `isSet`
2037
- // fallback. Without one the fan-out rows are not individually addressable by a proper
2038
- // key — the by-prefix delete + keyed upsert would be unsound — so fall to the floor.
2039
- // `getType().keys` carries the validated advertisement (an out-of-range key
2040
- // advertisement is dropped, leaving this empty), so it is the authoritative
2041
- // "did the TVF advertise a usable key" signal.
2042
- if (tvf.getType().keys.length === 0)
2043
- return null;
2044
- // Base T's PK source columns.
2045
- const sourceSchema = tableRef.tableSchema;
2046
- const sourcePkCols = sourceSchema.primaryKeyDefinition.map(d => d.index);
2047
- if (sourcePkCols.length === 0)
2048
- return null;
2049
- // Backing table + its physical PK (the composite product key).
2050
- const backing = this.ctx._findTable(mv.name, mv.schemaName);
2051
- if (!backing) {
2052
- throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
2053
- }
2054
- const backingPkDefinition = backing.primaryKeyDefinition.map(d => ({ index: d.index, desc: d.desc, collation: d.collation }));
2055
- // Map each output attribute to a base-T source column (or `undefined` for a TVF
2056
- // output column). T's attributes pass through the join unchanged, so a base-PK
2057
- // output column resolves to a T column while a TVF-key output column does not.
2058
- const sourceAttrToCol = new Map();
2059
- tableRef.getAttributes().forEach((a, i) => sourceAttrToCol.set(a.id, i));
2060
- const producingByAttrId = collectProducingExprs(analyzed);
2061
- const rootAttrs = relationalAttributes(analyzed);
2062
- if (!rootAttrs)
2063
- return null;
2064
- // Prefix soundness: the LEADING `basePrefixLen` backing-PK columns must each
2065
- // project (transitively) a distinct base-T PK column, their set must equal the base
2066
- // PK, and there must be a non-empty TVF-key tail. So the base PK is the leading
2067
- // prefix of the composite product key and the by-prefix delete selects exactly one
2068
- // base row's fan-out. A composite key that did not form this way falls to the floor.
2069
- const basePrefixLen = sourcePkCols.length;
2070
- if (backingPkDefinition.length <= basePrefixLen)
2071
- return null;
2072
- const basePkSet = new Set(sourcePkCols);
2073
- const leadingSourceCols = new Set();
2074
- const backingPrefixSourceCols = [];
2075
- for (let i = 0; i < basePrefixLen; i++) {
2076
- const d = backingPkDefinition[i];
2077
- const attr = rootAttrs[d.index];
2078
- const sc = attr ? resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
2079
- if (sc === undefined || !basePkSet.has(sc))
2080
- return null; // base PK not the leading prefix → floor
2081
- // Soundness precondition for the binary prefix scan (the property
2082
- // `prefix-delete-noncase-collation-regression-test` locks in): the backing base-PK
2083
- // column MUST inherit the source PK column's collation. The btree orders this prefix
2084
- // by `d.collation`, but the keyed diff's existing-slice read (`scanEffective` with
2085
- // the base prefix, in `applyPrefixDelete`) early-terminates the prefix scan on a
2086
- // BINARY compare (scan-layer.ts) — sound ONLY because source-PK uniqueness under that
2087
- // collation collapses each collation class to a single binary value, so a base row's
2088
- // fan-out rows are binary-homogeneous and contiguous. A backing collation MORE
2089
- // permissive than the source's would let collation-equal/binary-different base rows
2090
- // interleave and break that. The backing column derives its collation from the body
2091
- // relation's type (deriveBackingShape), so a mismatch is an internal derivation bug —
2092
- // fail loud rather than register an unsound plan.
2093
- const backingColl = normalizeCollation(d.collation);
2094
- const sourceColl = normalizeCollation(sourceSchema.columns[sc]?.collation);
2095
- if (backingColl !== sourceColl) {
2096
- throw new QuereusError(`Internal error: materialized view '${mv.name}' backing base-PK column `
2097
- + `'${backing.columns[d.index]?.name ?? d.index}' has collation '${backingColl}' but its source `
2098
- + `primary-key column '${sourceSchema.columns[sc]?.name ?? sc}' has collation '${sourceColl}'; `
2099
- + `the prefix-delete arm's binary prefix scan requires the backing base-PK column to inherit the `
2100
- + `source PK collation (see scan-layer.ts early-termination)`, StatusCode.INTERNAL);
2101
- }
2102
- leadingSourceCols.add(sc);
2103
- backingPrefixSourceCols.push(sc);
2104
- }
2105
- if (leadingSourceCols.size !== basePkSet.size)
2106
- return null; // prefix does not cover the base PK → floor
2107
- // The TVF-key tail must NOT re-use a base-PK column — else the fan-out rows would
2108
- // not be distinguished and the "key" would be base-only (defensive: the product key
2109
- // places the TVF key, a distinct relation's columns, in the tail). Otherwise → floor.
2110
- for (let i = basePrefixLen; i < backingPkDefinition.length; i++) {
2111
- const d = backingPkDefinition[i];
2112
- const attr = rootAttrs[d.index];
2113
- const sc = attr ? resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
2114
- if (sc !== undefined && basePkSet.has(sc))
2115
- return null;
2116
- }
2117
- // Compile + cache the base-PK-keyed residual once (the body with `T.pk = :pk0 AND …`
2118
- // injected on T). Re-run per affected base key against the live transaction; it
2119
- // re-runs the lateral join + TVF for that single base row, fanning out to N rows.
2120
- const relKey = `${sourceBase}#${tableRef.id ?? 'unknown'}`;
2121
- const residualScheduler = this.compileResidual(analyzed, relKey, sourcePkCols, 'pk');
2122
- if (!residualScheduler)
2123
- return null; // could not parameterize the residual → floor
2124
- // ── Cost gate ──
2125
- // The fan-out residual shares the residual-recompute cost shape (a key-filtered
2126
- // re-execution of the body); the fan-out factor (rows per base key) is not known at
2127
- // create, so we cost it as a residual and record the choice for substrate parity.
2128
- // The synchronous reject-at-create / degrade-to-rebuild machinery stays dormant, as
2129
- // it does for the other arms (a TVF whose fan-out is pathological is not detectable
2130
- // without fan-out stats — deferred with the fanning-keyed-join follow-up).
2131
- const soundStrategies = ['residual-recompute'];
2132
- const hasPredicate = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
2133
- const sourceStats = this.estimateMaintenanceStats(sourceSchema, backing.columns.length, hasPredicate);
2134
- const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
2135
- const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
2136
- if (chosenStrategy !== 'residual-recompute') {
2137
- throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
2138
- }
2139
- return {
2140
- kind: 'prefix-delete',
2141
- mv,
2142
- sourceBase,
2143
- backingSchema: mv.schemaName,
2144
- backingTableName: mv.name,
2145
- chosenStrategy,
2146
- sourceStats,
2147
- binding: { kind: 'row', keyColumns: [...sourcePkCols] },
2148
- degradeToRebuild: false,
2149
- residualScheduler,
2150
- bindParamPrefix: 'pk',
2151
- bindColumns: sourcePkCols,
2152
- backingPkDefinition,
2153
- basePrefixLength: basePrefixLen,
2154
- backingPrefixSourceCols,
2155
- };
2156
- }
2157
- /**
2158
- * Compute a `'prefix-delete'` plan's per-row backing delta and apply it: derive the
2159
- * affected base key(s) from the changed row (OLD ∪ NEW, deduped on the base key), and
2160
- * for each — re-run the base-PK-keyed residual against live source state and apply the
2161
- * **keyed diff against the existing effective fan-out slice** (read via the host's
2162
- * `scanEffective` with the base prefix, pending over committed — the same contiguous
2163
- * range the former wholesale `'delete-by-prefix'` removed): delete ONLY the existing
2164
- * keys the recompute no longer produces, upsert every recomputed row (the host's
2165
- * value-identical skip suppresses the unchanged ones). A base-PK-changing UPDATE
2166
- * recomputes both the OLD base key (slice diffs to all-deletes; the residual returns
2167
- * nothing for the now-absent old PK) and the NEW base key (new fan-out upserted); a
2168
- * DELETE diffs the old slice to all-deletes; an INSERT diffs against an empty slice
2169
- * (all upserts). An emptied/shrunk fan-out keeps the delete-without-upsert exactly —
2170
- * a disappearance is never "skipped". Returns the effective
2171
- * {@link BackingRowChange}(s) the backing layer realized, for the MV-over-MV cascade.
2172
- *
2173
- * Prefix-scan soundness is unchanged from the wholesale arm: the diff's slice read
2174
- * uses the same binary `equalityPrefix` scan `'delete-by-prefix'` used, sound under
2175
- * the build-time collation gate (the backing base-PK prefix inherits the source PK
2176
- * collation, and source-PK uniqueness collapses each collation class to one binary
2177
- * value). The stored slice's prefix bytes always equal the OLD image's (the slice was
2178
- * projected from that very source row), and OLD ∪ NEW both iterate, so a case-only
2179
- * base-PK rewrite still converges: the OLD-prefix pass pairs the slice with the
2180
- * recomputed rows (key pairing is collation-aware — the btree's identity — so a
2181
- * collation-equal key is REPLACED by its upsert, never also deleted) and the byte
2182
- * change surfaces as `update`s that re-key the stored bytes.
2183
- *
2184
- * Structurally the same as {@link applyForwardResidual}, differing only in the
2185
- * **prefix-slice** diff (one base row owns N backing rows sharing the prefix) and the
2186
- * **N-row** residual. Per-row recompute is correct without per-statement batching: the
2187
- * residual reads live (reads-own-writes) state, so the last write to a base key produces
2188
- * the authoritative slice. (Statement-level dedup of distinct base keys is the same
2189
- * affordability optimization deferred for the aggregate arm.)
2190
- */
2191
- async applyPrefixDelete(plan, change, cache) {
2192
- // Distinct affected base keys (OLD ∪ NEW), deduped on the base-PK values. `keyTuple`
2193
- // binds the residual (`pk{i}`); `prefix` is the slice's leading-PK equality key (the
2194
- // base-PK values in backing-PK order — identical here since the base PK leads the
2195
- // backing PK, but kept distinct for clarity).
2196
- const affected = new Map();
2197
- const addFrom = (row) => {
2198
- const keyTuple = plan.bindColumns.map(c => row[c]);
2199
- const dedupKey = canonKeyValues(keyTuple);
2200
- if (affected.has(dedupKey))
2201
- return;
2202
- affected.set(dedupKey, { keyTuple, prefix: plan.backingPrefixSourceCols.map(sc => row[sc]) });
2203
- };
2204
- if (change.op === 'insert')
2205
- addFrom(change.newRow);
2206
- else if (change.op === 'delete')
2207
- addFrom(change.oldRow);
2208
- else {
2209
- addFrom(change.oldRow);
2210
- addFrom(change.newRow);
2211
- }
2212
- // Resolved up front (unlike the point-op arms): the keyed diff reads the existing
2213
- // effective slice before any op exists. The former wholesale arm always emitted ops,
2214
- // so this resolves no more connections than it did.
2215
- const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
2216
- if (!backing) {
2217
- throw new QuereusError(`Internal error: backing table '${plan.backingTableName}' for materialized view '${plan.mv.name}' not found`, StatusCode.INTERNAL);
2218
- }
2219
- const host = this.backingHost(backing);
2220
- const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`, cache);
2221
- const ops = [];
2222
- for (const { keyTuple, prefix } of affected.values()) {
2223
- const recomputed = await this.runResidual(plan.residualScheduler, plan.bindParamPrefix, keyTuple);
2224
- // The residual for base key K filters T to K, so every row it returns shares K's
2225
- // base-PK prefix; the prefix-match guard is a defensive soundness net (mirrors
2226
- // the aggregate arm's `residualRowMatchesKey`).
2227
- const slice = recomputed.filter(row => this.residualRowMatchesBasePrefix(plan, row, prefix));
2228
- // Existing effective fan-out rows for this base prefix (pending over committed).
2229
- const existing = [];
2230
- for await (const row of host.scanEffective(connection, { equalityPrefix: prefix })) {
2231
- existing.push(row);
2232
- }
2233
- // Keyed diff. Key pairing is collation-aware over the full backing PK (the btree's
2234
- // identity): a recomputed row whose key is collation-equal to an existing row
2235
- // REPLACES it via the upsert below, so it must not also be deleted. Deletes precede
2236
- // upserts (the wholesale arm's ordering discipline). The delete keys are built from
2237
- // the EXISTING rows' stored values, so the host's collation-aware point lookup
2238
- // always finds them.
2239
- for (const ex of existing) {
2240
- if (slice.some(row => this.backingPkEqual(plan.backingPkDefinition, row, ex)))
2241
- continue;
2242
- ops.push({
2243
- kind: 'delete-key',
2244
- key: buildPrimaryKeyFromValues(plan.backingPkDefinition.map(d => ex[d.index]), plan.backingPkDefinition),
2245
- });
2246
- }
2247
- for (const row of slice)
2248
- ops.push({ kind: 'upsert', row });
2249
- }
2250
- if (ops.length === 0)
2251
- return [];
2252
- return host.applyMaintenance(connection, ops);
2253
- }
2254
- /**
2255
- * True iff two backing rows agree on every backing-PK column under that column's
2256
- * collation — the btree's key identity. Pairs an existing slice row with the
2257
- * recomputed row that replaces it in {@link applyPrefixDelete}'s keyed diff.
2258
- */
2259
- backingPkEqual(pkDef, a, b) {
2260
- for (const d of pkDef) {
2261
- if (compareSqlValues(a[d.index], b[d.index], d.collation) !== 0)
2262
- return false;
2263
- }
2264
- return true;
2265
- }
2266
- /**
2267
- * True iff `row`'s **leading** (base-prefix) backing-PK columns equal `prefixVals` (the
2268
- * affected base key, in backing-PK order), under each column's collation. Keeps only the
2269
- * residual fan-out row(s) belonging to the recomputed base key — see
2270
- * {@link applyPrefixDelete}.
2271
- */
2272
- residualRowMatchesBasePrefix(plan, row, prefixVals) {
2273
- for (let i = 0; i < plan.basePrefixLength; i++) {
2274
- const d = plan.backingPkDefinition[i];
2275
- if (compareSqlValues(row[d.index], prefixVals[i], d.collation) !== 0)
2276
- return false;
2277
- }
2278
- return true;
2279
- }
2280
- /**
2281
- * Assemble {@link MaintenanceSourceStats} for the cost gate from the optimizer's
2282
- * StatsProvider and tuning. `tableRows` / `distinctGroupsEstimate` come from the
2283
- * provider (heuristic defaults when absent); `forwardBodyCost` is estimated from the
2284
- * forward cost helpers (a scan + optional filter + projection of the source — the
2285
- * covering-index body shape); `fallbackRatio` carries the detection kernel's
2286
- * `deltaPerRowFallbackRatio` for the no-stats residual path.
2287
- */
2288
- /**
2289
- * The CURRENT `TableSchema` of a source `TableReferenceNode`, re-resolved through the
2290
- * schema manager. A plan node captures the schema as of plan-build; a later `analyze`
2291
- * replaces the catalog entry with one carrying fresh `statistics`, so the stale captured
2292
- * schema would report pre-`analyze` row counts. Re-resolving keeps the floor's size gate
2293
- * on the live source size. Falls back to the node's captured schema if the name no longer
2294
- * resolves (it always should — the body planned).
2295
- */
2296
- liveSourceSchema(ref) {
2297
- const captured = ref.tableSchema;
2298
- return this.ctx._findTable(captured.name, captured.schemaName) ?? captured;
2299
- }
2300
- estimateMaintenanceStats(sourceSchema, projectionCount, hasPredicate) {
2301
- const optimizer = this.ctx.optimizer;
2302
- const statsProvider = optimizer.getStats();
2303
- const tableRows = statsProvider.tableRows(sourceSchema) ?? DEFAULT_SOURCE_ROWS;
2304
- const forwardBodyCost = seqScanCost(tableRows)
2305
- + (hasPredicate ? filterCost(tableRows) : 0)
2306
- + projectCost(tableRows, projectionCount);
2307
- const stats = {
2308
- tableRows,
2309
- forwardBodyCost,
2310
- fallbackRatio: optimizer.tuning.deltaPerRowFallbackRatio,
2311
- };
2312
- // `distinctValues` is an optional, per-column StatsProvider method. For the
2313
- // covering-index shape the source PK is the grouping key; a single-column PK
2314
- // yields a usable distinct-groups estimate (which only feeds the never-chosen-here
2315
- // residual cost). Multi-column PKs leave it unset → residual takes the no-stats path.
2316
- const pkDef = sourceSchema.primaryKeyDefinition;
2317
- if (pkDef.length === 1 && statsProvider.distinctValues) {
2318
- const pkColName = sourceSchema.columns[pkDef[0].index]?.name;
2319
- if (pkColName !== undefined) {
2320
- const distinct = statsProvider.distinctValues(sourceSchema, pkColName);
2321
- if (distinct !== undefined)
2322
- stats.distinctGroupsEstimate = distinct;
2323
- }
2324
- }
2325
- return stats;
2326
- }
2327
782
  /* ──────────────── row-time covering enforcement ──────────────── */
2328
783
  /**
2329
784
  * Resolve the linked, enforcement-ready covering MV for a UNIQUE constraint on
@@ -2449,7 +904,7 @@ export class MaterializedViewManager {
2449
904
  // structure is never linked to one — defensively skip if reached.
2450
905
  if (plan.kind !== 'inverse-projection')
2451
906
  return [];
2452
- const [srcSchemaName, srcTableName] = plan.sourceBase.split('.');
907
+ const [srcSchemaName, srcTableName] = splitBaseKey(plan.sourceBase);
2453
908
  const sourceSchema = this.ctx._findTable(srcTableName, srcSchemaName);
2454
909
  if (!sourceSchema)
2455
910
  return [];
@@ -2482,8 +937,22 @@ export class MaterializedViewManager {
2482
937
  const backing = this.ctx.schemaManager.getTable(plan.backingSchema, plan.backingTableName);
2483
938
  if (!backing)
2484
939
  return [];
2485
- const host = this.backingHost(backing);
2486
- const connection = await this.getBackingConnection(host, `${plan.backingSchema}.${plan.backingTableName}`);
940
+ const host = backingHost(this.ctx, backing);
941
+ const connection = await getBackingConnection(this.ctx, host, `${plan.backingSchema}.${plan.backingTableName}`);
942
+ // Both comparisons below run per scanned backing row, so resolve their collations
943
+ // once here, against this database (a redefined NOCASE must be honored).
944
+ //
945
+ // These are the source column's **declared** collations, NOT the constraint's
946
+ // enforcement collation (`uniqueEnforcementCollations`, which prefers the index's
947
+ // per-column collation). That is deliberate: this method generates conflict
948
+ // *candidates* which the re-validators then filter under the index collation, and
949
+ // the declared-collation candidate set is a sound superset only because
950
+ // `findRowTimeCoveringStructure` declines any MV whose index collation is finer or
951
+ // incomparable (`coveringMvHonorsIndexCollation`). Swapping in the enforcement
952
+ // collation here would break that pairing — do not "DRY" these together.
953
+ const resolver = this.ctx.getCollationResolver();
954
+ const ucCollationFns = resolveCollationFunctions(resolver, uc.columns.map(c => sourceSchema.columns[c]?.collation));
955
+ const pkCollationFns = resolveCollationFunctions(resolver, pkDef.map(d => d.collation));
2487
956
  const conflicts = [];
2488
957
  // Fast path: a backing-PK prefix scan keyed on `newRow`'s UC values. The
2489
958
  // covering-index shape guarantees the leading backing-PK columns are the UC
@@ -2498,8 +967,7 @@ export class MaterializedViewManager {
2498
967
  for await (const backingRow of host.scanEffective(connection, { equalityPrefix })) {
2499
968
  let match = true;
2500
969
  for (let k = 0; k < uc.columns.length; k++) {
2501
- const coll = sourceSchema.columns[uc.columns[k]]?.collation;
2502
- if (compareSqlValues(newRow[uc.columns[k]], backingRow[ucBackingCols[k]], coll) !== 0) {
970
+ if (compareSqlValuesFast(newRow[uc.columns[k]], backingRow[ucBackingCols[k]], ucCollationFns[k]) !== 0) {
2503
971
  match = false;
2504
972
  break;
2505
973
  }
@@ -2510,7 +978,7 @@ export class MaterializedViewManager {
2510
978
  // Exclude the row currently being written (its own source PK).
2511
979
  let isSelf = sourcePk.length === newSourcePk.length;
2512
980
  for (let i = 0; isSelf && i < sourcePk.length; i++) {
2513
- if (compareSqlValues(sourcePk[i], newSourcePk[i], pkDef[i]?.collation) !== 0)
981
+ if (compareSqlValuesFast(sourcePk[i], newSourcePk[i], pkCollationFns[i]) !== 0)
2514
982
  isSelf = false;
2515
983
  }
2516
984
  if (isSelf)
@@ -2584,481 +1052,4 @@ export class MaterializedViewManager {
2584
1052
  return prefix;
2585
1053
  }
2586
1054
  }
2587
- /* ─────────────────────────── helpers ─────────────────────────── */
2588
- /** True for the default (binary) collation: an absent name or a case-insensitive
2589
- * `BINARY`. Non-binary collations gate off the prefix-scan fast path (see
2590
- * {@link MaterializedViewManager.tryBuildCoveringPrefix}). */
2591
- function isBinaryCollation(collation) {
2592
- return collation === undefined || collation.toUpperCase() === 'BINARY';
2593
- }
2594
- /** Canonical upper-case collation name (absent ⇒ `BINARY`). Used to compare a backing-PK
2595
- * column's collation against its source PK column's at plan-build (see
2596
- * {@link MaterializedViewManager.buildLateralTvfPrefixDeletePlan}). */
2597
- function normalizeCollation(collation) {
2598
- return (collation ?? 'BINARY').toUpperCase();
2599
- }
2600
- function mvKey(schemaName, name) {
2601
- return `${schemaName}.${name}`.toLowerCase();
2602
- }
2603
- /** Every source base (lowercased `schema.table`) a plan must be indexed under in
2604
- * `rowTimeBySource`. Single-source arms read one base; the 1:1-join arm also reads
2605
- * the lookup base, so a write to `P` fires maintenance too; the full-rebuild floor reads
2606
- * every source its body touches (set-op legs, all join sources). */
2607
- function planSourceBases(plan) {
2608
- if (plan.kind === 'full-rebuild') {
2609
- return plan.sourceBases;
2610
- }
2611
- if (plan.kind === 'join-residual' && plan.lookupBase !== plan.sourceBase) {
2612
- return [plan.sourceBase, plan.lookupBase];
2613
- }
2614
- return [plan.sourceBase];
2615
- }
2616
- /** Walk the whole plan; return the string form of the first non-deterministic scalar
2617
- * expression (a `random()`/`now()`/volatile UDF, anywhere in the body), or `undefined`
2618
- * when the body is fully deterministic. The full-rebuild floor's whole-body determinism
2619
- * gate uses this — a non-deterministic body can never be kept equal to its plain view.
2620
- * `physical.deterministic` is computed lazily and propagates from leaves, so checking each
2621
- * scalar node is sound on either the pre-physical or optimized plan. */
2622
- function findNonDeterministic(node) {
2623
- if (isScalarNode(node)) {
2624
- const det = checkDeterministic(node);
2625
- if (!det.valid)
2626
- return det.expression ?? node.toString();
2627
- }
2628
- for (const child of node.getChildren()) {
2629
- const found = findNonDeterministic(child);
2630
- if (found)
2631
- return found;
2632
- }
2633
- return undefined;
2634
- }
2635
- /** Walk the whole plan; return the NAME of the first function whose schema is not declared
2636
- * REPLICABLE (bit-identical across peers/platforms/app-versions — built-ins auto-qualify),
2637
- * or `undefined` when every function in the body qualifies. Mirrors {@link findNonDeterministic}'s
2638
- * `getChildren()` recursion so nested calls (a UDF inside a builtin inside a UDF) and the
2639
- * WHERE / GROUP BY / aggregate-arg / TVF-arg positions are all reached. The structural
2640
- * `'functionSchema' in node` test covers all four function-bearing node kinds uniformly —
2641
- * scalar (`function.ts`), aggregate (`aggregate-function.ts`), TVF call
2642
- * (`table-function-call.ts`), and TVF reference (`reference.ts`) — without per-type imports.
2643
- * Window functions live in a separate builtin-only registry with no UDF registration path and
2644
- * carry no scalar/aggregate/TVF `functionSchema` on these nodes, so they are inherently
2645
- * replicable and are never flagged. Consumed only when the backing host declares
2646
- * `requiresReplicableDerivations` (see {@link MaterializedViewManager.buildMaintenancePlan}). */
2647
- function findNonReplicableFunction(node) {
2648
- if ('functionSchema' in node) {
2649
- const schema = node.functionSchema;
2650
- if (schema.replicable !== true)
2651
- return schema.name;
2652
- }
2653
- for (const child of node.getChildren()) {
2654
- const found = findNonReplicableFunction(child);
2655
- if (found)
2656
- return found;
2657
- }
2658
- return undefined;
2659
- }
2660
- /** The built-in collation names. These are pure JS string operations (`<`/`>`,
2661
- * locale-independent `toLowerCase()`, ASCII-space trim), so they are bit-identical
2662
- * across peers' JS engines and auto-qualify as REPLICABLE — exactly parallel to why
2663
- * built-in functions do. A custom collation must opt in with `replicable: true` at
2664
- * registration. Short-circuiting on name (regardless of `collationSource`) keeps the
2665
- * walk free of rank reasoning: a `default` BINARY and an `explicit` NOCASE both pass;
2666
- * only a custom name is ever subjected to `_isCollationReplicable`. */
2667
- const BUILTIN_COLLATION_NAMES = new Set(['BINARY', 'NOCASE', 'RTRIM']);
2668
- /** True when `collation` is a non-builtin name the database does not assert REPLICABLE.
2669
- * `undefined`/builtin/replicable ⇒ not offending. */
2670
- function collationIsOffending(collation, db) {
2671
- if (collation === undefined)
2672
- return false;
2673
- const norm = normalizeCollationName(collation);
2674
- if (BUILTIN_COLLATION_NAMES.has(norm))
2675
- return false;
2676
- return !db._isCollationReplicable(norm);
2677
- }
2678
- /**
2679
- * The collation analogue of {@link findNonReplicableFunction}: return the NAME of the
2680
- * first collation that governs derived bytes and is neither built-in nor declared
2681
- * REPLICABLE, or `undefined` when every collation qualifies. Two sources, soundness-first
2682
- * (any non-builtin non-replicable collation anywhere rejects — see the soundness note in
2683
- * `replicable-collation-class`):
2684
- *
2685
- * 1. **Body scalars** — every fold/order/key site (explicit `COLLATE`, a declared/default
2686
- * column collation, a comparison's effective collation, ORDER BY / GROUP BY / DISTINCT
2687
- * keys) resolves through some scalar node whose `getType().collationName` carries the
2688
- * name. One `getChildren()` walk reading that field uniformly reaches them all, including
2689
- * nested COLLATE, subquery/CTE/set-op legs, and MV-over-MV bodies (whose source columns
2690
- * carry the producing backing's published collation).
2691
- * 2. **Backing key** — a custom collation can govern the backing key MERGE without appearing
2692
- * on any body scalar type (a maintained table declared with an explicit
2693
- * `UNIQUE (… COLLATE custom)` or PK collation the SELECT body never names). The body walk
2694
- * alone would miss it, so the maintained table's own PK column collations + declared
2695
- * secondary UNIQUE per-column enforcement collations (resolving an index-derived override
2696
- * via {@link uniqueEnforcementCollations}) are checked directly — the robust closure.
2697
- *
2698
- * Consumed only when the backing host declares `requiresReplicableDerivations`.
2699
- */
2700
- function findNonReplicableCollation(node, mv, db) {
2701
- const bodyOffender = findNonReplicableBodyCollation(node, db);
2702
- if (bodyOffender !== undefined)
2703
- return bodyOffender;
2704
- return findNonReplicableKeyCollation(mv, db);
2705
- }
2706
- /** Source 1: walk the plan; first scalar node whose resolved `collationName` is a
2707
- * non-builtin non-replicable collation. Mirrors {@link findNonReplicableFunction}'s
2708
- * recursion so every body position is reached. */
2709
- function findNonReplicableBodyCollation(node, db) {
2710
- if (isScalarNode(node)) {
2711
- const collation = node.getType().collationName;
2712
- if (collationIsOffending(collation, db))
2713
- return normalizeCollationName(collation);
2714
- }
2715
- for (const child of node.getChildren()) {
2716
- const found = findNonReplicableBodyCollation(child, db);
2717
- if (found !== undefined)
2718
- return found;
2719
- }
2720
- return undefined;
2721
- }
2722
- /** Source 2: the maintained table's backing-key collations — PK column collations and
2723
- * declared secondary UNIQUE per-column enforcement collations. First non-builtin
2724
- * non-replicable name returns. */
2725
- function findNonReplicableKeyCollation(mv, db) {
2726
- for (const pk of mv.primaryKeyDefinition) {
2727
- if (collationIsOffending(pk.collation, db))
2728
- return normalizeCollationName(pk.collation);
2729
- }
2730
- for (const uc of mv.uniqueConstraints ?? []) {
2731
- for (const collation of uniqueEnforcementCollations(mv, uc)) {
2732
- if (collationIsOffending(collation, db))
2733
- return normalizeCollationName(collation);
2734
- }
2735
- }
2736
- return undefined;
2737
- }
2738
- /** Canonical, order-stable, bigint-safe string for a key tuple — used to dedup the
2739
- * distinct affected backing keys of a single change in the residual-recompute arm. */
2740
- function canonKeyValues(values) {
2741
- return JSON.stringify(values, (_k, v) => (typeof v === 'bigint' ? `${v}n` : v));
2742
- }
2743
- /** Aggregate node types (logical + physical) — the analyzed plan may carry any. */
2744
- const AGGREGATE_NODE_TYPES = new Set([
2745
- PlanNodeType.Aggregate,
2746
- PlanNodeType.StreamAggregate,
2747
- PlanNodeType.HashAggregate,
2748
- ]);
2749
- /** Find the first aggregate node anywhere in the plan. */
2750
- function findAggregate(node) {
2751
- if (AGGREGATE_NODE_TYPES.has(node.nodeType))
2752
- return node;
2753
- for (const child of node.getChildren()) {
2754
- const found = findAggregate(child);
2755
- if (found)
2756
- return found;
2757
- }
2758
- return undefined;
2759
- }
2760
- /**
2761
- * Join-bearing PlanNodeTypes (logical + physical). `optimizeForAnalysis` stops
2762
- * before physical join selection, so the analyzed plan carries the logical
2763
- * {@link PlanNodeType.Join}; the physical variants are included so the
2764
- * eligibility gate stays correct if analysis ever surfaces them.
2765
- */
2766
- const JOIN_NODE_TYPES = new Set([
2767
- PlanNodeType.Join,
2768
- PlanNodeType.NestedLoopJoin,
2769
- PlanNodeType.HashJoin,
2770
- PlanNodeType.MergeJoin,
2771
- PlanNodeType.FanOutLookupJoin,
2772
- PlanNodeType.AsofScan,
2773
- ]);
2774
- /** True if any node in the plan has the given type (recursive `getChildren` walk). */
2775
- function containsNodeType(node, type) {
2776
- if (node.nodeType === type)
2777
- return true;
2778
- for (const child of node.getChildren()) {
2779
- if (containsNodeType(child, type))
2780
- return true;
2781
- }
2782
- return false;
2783
- }
2784
- /** True if the plan carries any join node (logical or physical). Used by the
2785
- * row-time gate, which is single-source — any join is ineligible. */
2786
- function containsAnyJoin(node) {
2787
- for (const t of JOIN_NODE_TYPES) {
2788
- if (containsNodeType(node, t))
2789
- return true;
2790
- }
2791
- return false;
2792
- }
2793
- /** Count nodes of the given type (recursive `getChildren` walk). Used by the
2794
- * lateral-TVF gate to reject nested/multiple TVFs. */
2795
- function countNodeType(node, type) {
2796
- let n = node.nodeType === type ? 1 : 0;
2797
- for (const child of node.getChildren())
2798
- n += countNodeType(child, type);
2799
- return n;
2800
- }
2801
- /** Count join nodes (logical + physical) in the plan — used to reject a chained
2802
- * lateral join (the admitted lateral-TVF shape carries exactly one). */
2803
- function countJoins(node) {
2804
- let n = 0;
2805
- for (const t of JOIN_NODE_TYPES)
2806
- n += countNodeType(node, t);
2807
- return n;
2808
- }
2809
- /** Find the first {@link TableFunctionCallNode} anywhere in the plan, or `undefined`. */
2810
- function findTableFunctionCall(node) {
2811
- if (node instanceof TableFunctionCallNode)
2812
- return node;
2813
- for (const child of node.getChildren()) {
2814
- const found = findTableFunctionCall(child);
2815
- if (found)
2816
- return found;
2817
- }
2818
- return undefined;
2819
- }
2820
- /** Collect `relationKey → TableReferenceNode` over a plan. */
2821
- function collectTableRefs(node, out = new Map()) {
2822
- if (node instanceof TableReferenceNode) {
2823
- const base = `${node.tableSchema.schemaName}.${node.tableSchema.name}`.toLowerCase();
2824
- out.set(`${base}#${node.id ?? 'unknown'}`, node);
2825
- }
2826
- for (const child of node.getChildren())
2827
- collectTableRefs(child, out);
2828
- return out;
2829
- }
2830
- /**
2831
- * Merge attribute provenance (output attr id → producing scalar expr) from every
2832
- * node that exposes it. Physical aggregates expose `getProducingExprs()`; the
2833
- * logical {@link AggregateNode} present in the pre-physical analyzed plan does
2834
- * not, so its group-by → output-attr mapping is reconstructed directly here.
2835
- */
2836
- function collectProducingExprs(node, out = new Map()) {
2837
- const fn = node.getProducingExprs;
2838
- if (typeof fn === 'function') {
2839
- for (const [attrId, expr] of fn.call(node)) {
2840
- if (!out.has(attrId))
2841
- out.set(attrId, expr);
2842
- }
2843
- }
2844
- else if (node instanceof AggregateNode) {
2845
- const attrs = node.getAttributes();
2846
- node.groupBy.forEach((expr, i) => {
2847
- const attr = attrs[i];
2848
- if (attr && !out.has(attr.id))
2849
- out.set(attr.id, expr);
2850
- });
2851
- node.aggregates.forEach((agg, i) => {
2852
- const attr = attrs[node.groupBy.length + i];
2853
- if (attr && !out.has(attr.id))
2854
- out.set(attr.id, agg.expression);
2855
- });
2856
- }
2857
- for (const child of node.getChildren())
2858
- collectProducingExprs(child, out);
2859
- return out;
2860
- }
2861
- /**
2862
- * Transitive provenance: chase an output-attr → producing `ColumnReference` chain (a
2863
- * Project-over-Aggregate or a passthrough-through-Join adds a hop the single-hop
2864
- * {@link resolveSourceCol} cannot follow) until landing on a base-source column, or
2865
- * `undefined` (e.g. a TVF-output column with no base-source identity). Shared by the
2866
- * aggregate-residual and lateral-TVF arms.
2867
- */
2868
- function resolveTransitiveSourceCol(attrId, sourceAttrToCol, producingByAttrId) {
2869
- const seen = new Set();
2870
- let cur = attrId;
2871
- while (cur !== undefined && !seen.has(cur)) {
2872
- seen.add(cur);
2873
- const direct = sourceAttrToCol.get(cur);
2874
- if (direct !== undefined)
2875
- return direct;
2876
- const expr = producingByAttrId.get(cur);
2877
- if (expr instanceof ColumnReferenceNode) {
2878
- cur = expr.attributeId;
2879
- continue;
2880
- }
2881
- return undefined;
2882
- }
2883
- return undefined;
2884
- }
2885
- /**
2886
- * True iff the analyzed join body's WHERE references the lookup table `P` (or any base other
2887
- * than the driving `T`) — the classification the join-residual arm uses to decide whether the
2888
- * lookup side must be delete-capable (see {@link MaterializedViewManager.buildJoinResidualPlan}).
2889
- * The body WHERE — possibly split by predicate-pushdown — surfaces as one or more
2890
- * {@link FilterNode}s above/around the join; the join's own `ON` condition lives inside the
2891
- * JoinNode (not a Filter) and so is excluded. Each column a filter predicate references is
2892
- * resolved against `T`'s attribute→source-column map (transitively); a reference that does NOT
2893
- * resolve to a `T` column is a `P` (the arm requires exactly two base refs, `T` and `P`) — or
2894
- * otherwise non-`T` — reference. Conservative by construction: an unresolved reference counts as
2895
- * lookup-referencing, so the cheaper `T`-only upsert-only path is taken only when **every**
2896
- * filter column provably belongs to `T`.
2897
- */
2898
- function bodyWhereReferencesLookup(analyzed, tAttrToCol, producingByAttrId) {
2899
- const filterAttrs = new Set();
2900
- collectFilterPredicateAttrs(analyzed, filterAttrs);
2901
- for (const attrId of filterAttrs) {
2902
- if (resolveTransitiveSourceCol(attrId, tAttrToCol, producingByAttrId) === undefined)
2903
- return true;
2904
- }
2905
- return false;
2906
- }
2907
- /** Collect every attribute id referenced by a ColumnReferenceNode inside any {@link FilterNode}
2908
- * predicate in the plan (the body WHERE; the join `ON` condition is not a Filter). */
2909
- function collectFilterPredicateAttrs(node, out) {
2910
- if (node instanceof FilterNode)
2911
- collectColumnRefAttrs(node.predicate, out);
2912
- for (const child of node.getChildren())
2913
- collectFilterPredicateAttrs(child, out);
2914
- }
2915
- /** Collect every {@link ColumnReferenceNode} attribute id in a scalar subtree. */
2916
- function collectColumnRefAttrs(node, out) {
2917
- if (node instanceof ColumnReferenceNode)
2918
- out.add(node.attributeId);
2919
- for (const child of node.getChildren())
2920
- collectColumnRefAttrs(child, out);
2921
- }
2922
- /**
2923
- * True iff any {@link FilterNode} predicate in the body (the body WHERE) is non-deterministic.
2924
- * The join-residual arm embeds the body WHERE in every residual (forward, in-scope reverse, and
2925
- * — when delete-capable — membership), so a volatile predicate (`random()`/`now()`/a volatile
2926
- * UDF) would make them irreproducible and diverge from the plain view. The arm therefore declines
2927
- * such a body (returns `null` → the full-rebuild floor, which applies the **pragma-gated**
2928
- * whole-body determinism reject — rejected without `pragma nondeterministic_schema`, accepted as a
2929
- * wholesale rebuild with it), preserving the pre-WHERE-widening behavior rather than building an
2930
- * unsound bounded-delta residual.
2931
- */
2932
- function bodyWhereIsNonDeterministic(analyzed) {
2933
- const visit = (node) => {
2934
- if (node instanceof FilterNode && !checkDeterministic(node.predicate).valid)
2935
- return true;
2936
- for (const child of node.getChildren()) {
2937
- if (visit(child))
2938
- return true;
2939
- }
2940
- return false;
2941
- };
2942
- return visit(analyzed);
2943
- }
2944
- /** Read the output attributes of a block's final relational statement. */
2945
- function relationalAttributes(block) {
2946
- const children = block.getChildren();
2947
- for (let i = children.length - 1; i >= 0; i--) {
2948
- const child = children[i];
2949
- if (typeof child.getAttributes === 'function')
2950
- return child.getAttributes();
2951
- }
2952
- return undefined;
2953
- }
2954
- /** The root relational node of a block's final relational statement — the node whose
2955
- * attributes {@link relationalAttributes} reads — or `undefined`. Feeds the shared
2956
- * coverage-prover join predicates ({@link proveOneToOneJoin}) for the join-residual arm. */
2957
- function rootRelationalNode(block) {
2958
- const children = block.getChildren();
2959
- for (let i = children.length - 1; i >= 0; i--) {
2960
- const child = children[i];
2961
- if (isRelationalNode(child))
2962
- return child;
2963
- }
2964
- return undefined;
2965
- }
2966
- /**
2967
- * The diagnostic for a create-time **hard** reject — one of the four non-shape rejections
2968
- * the cost-gated-with-floor model keeps (non-determinism, bag/no-key, no relational output,
2969
- * size). Names the MV and steers to a plain `view` (live re-evaluation) or
2970
- * `create table ... as <body>` (a one-off snapshot) — never a refresh policy, never an
2971
- * internal implementation detail. Used by the arm builders (for their arm-specific
2972
- * determinism diagnostic) and by {@link MaterializedViewManager.buildFullRebuildPlan}.
2973
- */
2974
- function cannotMaterialize(mvName, detail) {
2975
- return new QuereusError(`materialized view '${mvName}' cannot be materialized: ${detail}. For this body, use a `
2976
- + `plain 'create view' (live re-evaluation) or 'create table ... as <body>' (a one-off snapshot)`, StatusCode.UNSUPPORTED);
2977
- }
2978
- /**
2979
- * The diagnostic for the create-time **replicable-determinism** reject — distinct from
2980
- * {@link cannotMaterialize} because the fix here is not "use a plain view": the body is
2981
- * fine, it just calls a function the backing host requires be REPLICABLE. So this names the
2982
- * function and steers to declaring it `replicable: true` at registration (built-ins qualify
2983
- * automatically). Fires only when the resolved backing host declares
2984
- * `requiresReplicableDerivations`. `StatusCode.UNSUPPORTED`.
2985
- */
2986
- function nonReplicableDerivationError(mvName, fnName) {
2987
- return new QuereusError(`materialized view '${mvName}' cannot be materialized on this backing host: it calls non-replicable `
2988
- + `function '${fnName}'. This host requires every function in the body to be bit-identical across `
2989
- + `peers/platforms; declare the function \`replicable: true\` at registration (built-in functions `
2990
- + `qualify automatically)`, StatusCode.UNSUPPORTED);
2991
- }
2992
- /**
2993
- * The diagnostic for the create-time **replicable-collation** reject — the collation
2994
- * analogue of {@link nonReplicableDerivationError}. The body is fine; it just folds or
2995
- * orders (comparison / ORDER BY / GROUP BY / DISTINCT / backing key) under a collation the
2996
- * backing host requires be bit-identical across peers — so this does NOT steer to a plain
2997
- * view. It names the collation and steers to declaring it `replicable: true` at registration
2998
- * (built-in collations qualify automatically). Fires only when the resolved backing host
2999
- * declares `requiresReplicableDerivations`. `StatusCode.UNSUPPORTED`.
3000
- */
3001
- function nonReplicableCollationDerivationError(mvName, collationName) {
3002
- return new QuereusError(`materialized view '${mvName}' cannot be materialized on this backing host: it folds or orders under `
3003
- + `non-replicable collation '${collationName}'. This host requires every collation in the body to be `
3004
- + `bit-identical across peers/platforms; declare the collation \`replicable: true\` at registration `
3005
- + `(built-in collations qualify automatically)`, StatusCode.UNSUPPORTED);
3006
- }
3007
- /**
3008
- * True iff a computed projection expression can be evaluated as a pure function of the
3009
- * changed source row — i.e. it contains no subquery / relational subtree (cross-row) and
3010
- * every column reference resolves to a source column (no correlated / outer reference).
3011
- * This is the "shape" gate distinct from the determinism gate (a determinism failure is
3012
- * caught earlier by `checkDeterministic`); a `false` here is a `null` fall-through to the
3013
- * full-rebuild floor, not a hard reject.
3014
- */
3015
- function isSingleRowEvaluable(expr, sourceDescriptor) {
3016
- const visit = (node) => {
3017
- if (node !== expr && isRelationalNode(node))
3018
- return false; // a subquery / relational subtree
3019
- if (node instanceof ColumnReferenceNode && sourceDescriptor[node.attributeId] === undefined) {
3020
- return false; // references a value outside the source row
3021
- }
3022
- for (const child of node.getChildren()) {
3023
- if (!visit(child))
3024
- return false;
3025
- }
3026
- return true;
3027
- };
3028
- return visit(expr);
3029
- }
3030
- /**
3031
- * Compile a deterministic scalar plan node into a per-source-row evaluator by reusing
3032
- * the runtime: emit the node once, then run it against a row context that maps each
3033
- * source attribute id to its column index in the changed row. Reusing the runtime
3034
- * (rather than a hand-rolled scalar interpreter) guarantees a computed backing value is
3035
- * byte-for-byte what `select <body>` would produce — the materialized-view ≡ view
3036
- * contract. The gated forms (deterministic scalars over a single row, no subqueries —
3037
- * see {@link assertSingleRowEvaluable}) resolve synchronously; a Promise result would
3038
- * signal an unsupported async form and is surfaced loudly rather than silently awaited.
3039
- */
3040
- function compileSourceRowEvaluator(db, expr, sourceDescriptor) {
3041
- const instruction = emitPlanNode(expr, new EmissionContext(db));
3042
- const scheduler = new Scheduler(instruction);
3043
- const context = new RowContextMap();
3044
- let currentRow = [];
3045
- // Installed once; the getter reads the closed-over `currentRow`, refreshed per call.
3046
- context.set(sourceDescriptor, () => currentRow);
3047
- const rctx = {
3048
- db,
3049
- stmt: undefined,
3050
- params: {},
3051
- context,
3052
- tableContexts: new Map(),
3053
- enableMetrics: false,
3054
- };
3055
- return (row) => {
3056
- currentRow = row;
3057
- const result = scheduler.run(rctx);
3058
- if (result instanceof Promise) {
3059
- throw new QuereusError('a row-time projection expression evaluated asynchronously (unexpected for a gated single-row scalar)', StatusCode.INTERNAL);
3060
- }
3061
- return result;
3062
- };
3063
- }
3064
1055
  //# sourceMappingURL=database-materialized-views.js.map