@quereus/quereus 4.3.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (788) 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 +3 -3
  10. package/dist/src/core/database-external-changes.d.ts.map +1 -1
  11. package/dist/src/core/database-external-changes.js +13 -10
  12. package/dist/src/core/database-external-changes.js.map +1 -1
  13. package/dist/src/core/database-internal.d.ts.map +1 -1
  14. package/dist/src/core/database-materialized-views-analysis.d.ts +199 -0
  15. package/dist/src/core/database-materialized-views-analysis.d.ts.map +1 -0
  16. package/dist/src/core/database-materialized-views-analysis.js +502 -0
  17. package/dist/src/core/database-materialized-views-analysis.js.map +1 -0
  18. package/dist/src/core/database-materialized-views-apply.d.ts +422 -0
  19. package/dist/src/core/database-materialized-views-apply.d.ts.map +1 -0
  20. package/dist/src/core/database-materialized-views-apply.js +929 -0
  21. package/dist/src/core/database-materialized-views-apply.js.map +1 -0
  22. package/dist/src/core/database-materialized-views-plan-builders.d.ts +267 -0
  23. package/dist/src/core/database-materialized-views-plan-builders.d.ts.map +1 -0
  24. package/dist/src/core/database-materialized-views-plan-builders.js +1423 -0
  25. package/dist/src/core/database-materialized-views-plan-builders.js.map +1 -0
  26. package/dist/src/core/database-materialized-views-plans.d.ts +641 -0
  27. package/dist/src/core/database-materialized-views-plans.d.ts.map +1 -0
  28. package/dist/src/core/database-materialized-views-plans.js +13 -0
  29. package/dist/src/core/database-materialized-views-plans.js.map +1 -0
  30. package/dist/src/core/database-materialized-views.d.ts +108 -860
  31. package/dist/src/core/database-materialized-views.d.ts.map +1 -1
  32. package/dist/src/core/database-materialized-views.js +212 -2165
  33. package/dist/src/core/database-materialized-views.js.map +1 -1
  34. package/dist/src/core/database-transaction.d.ts +10 -1
  35. package/dist/src/core/database-transaction.d.ts.map +1 -1
  36. package/dist/src/core/database-transaction.js +16 -4
  37. package/dist/src/core/database-transaction.js.map +1 -1
  38. package/dist/src/core/database-watchers.d.ts.map +1 -1
  39. package/dist/src/core/database-watchers.js +2 -1
  40. package/dist/src/core/database-watchers.js.map +1 -1
  41. package/dist/src/core/database.d.ts +99 -13
  42. package/dist/src/core/database.d.ts.map +1 -1
  43. package/dist/src/core/database.js +270 -50
  44. package/dist/src/core/database.js.map +1 -1
  45. package/dist/src/core/derived-row-validator.d.ts +1 -1
  46. package/dist/src/core/derived-row-validator.d.ts.map +1 -1
  47. package/dist/src/core/derived-row-validator.js +5 -3
  48. package/dist/src/core/derived-row-validator.js.map +1 -1
  49. package/dist/src/core/statement.d.ts +8 -0
  50. package/dist/src/core/statement.d.ts.map +1 -1
  51. package/dist/src/core/statement.js +98 -18
  52. package/dist/src/core/statement.js.map +1 -1
  53. package/dist/src/emit/ast-stringify.d.ts.map +1 -1
  54. package/dist/src/emit/ast-stringify.js +24 -13
  55. package/dist/src/emit/ast-stringify.js.map +1 -1
  56. package/dist/src/func/builtins/aggregate.d.ts.map +1 -1
  57. package/dist/src/func/builtins/aggregate.js +127 -21
  58. package/dist/src/func/builtins/aggregate.js.map +1 -1
  59. package/dist/src/func/builtins/explain.d.ts.map +1 -1
  60. package/dist/src/func/builtins/explain.js +2 -1
  61. package/dist/src/func/builtins/explain.js.map +1 -1
  62. package/dist/src/func/builtins/json.d.ts.map +1 -1
  63. package/dist/src/func/builtins/json.js +2 -1
  64. package/dist/src/func/builtins/json.js.map +1 -1
  65. package/dist/src/func/builtins/mutation.d.ts.map +1 -1
  66. package/dist/src/func/builtins/mutation.js +3 -2
  67. package/dist/src/func/builtins/mutation.js.map +1 -1
  68. package/dist/src/func/registration.d.ts +6 -1
  69. package/dist/src/func/registration.d.ts.map +1 -1
  70. package/dist/src/func/registration.js +16 -0
  71. package/dist/src/func/registration.js.map +1 -1
  72. package/dist/src/index.d.ts +16 -6
  73. package/dist/src/index.d.ts.map +1 -1
  74. package/dist/src/index.js +33 -7
  75. package/dist/src/index.js.map +1 -1
  76. package/dist/src/parser/ast.d.ts +2 -2
  77. package/dist/src/parser/lexer.d.ts +11 -1
  78. package/dist/src/parser/lexer.d.ts.map +1 -1
  79. package/dist/src/parser/lexer.js +40 -34
  80. package/dist/src/parser/lexer.js.map +1 -1
  81. package/dist/src/parser/parser.d.ts.map +1 -1
  82. package/dist/src/parser/parser.js +16 -9
  83. package/dist/src/parser/parser.js.map +1 -1
  84. package/dist/src/planner/analysis/authored-inverse.js +1 -1
  85. package/dist/src/planner/analysis/comparison-collation.d.ts +52 -0
  86. package/dist/src/planner/analysis/comparison-collation.d.ts.map +1 -1
  87. package/dist/src/planner/analysis/comparison-collation.js +84 -7
  88. package/dist/src/planner/analysis/comparison-collation.js.map +1 -1
  89. package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
  90. package/dist/src/planner/analysis/constraint-extractor.js +64 -17
  91. package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
  92. package/dist/src/planner/analysis/coverage-prover.d.ts +1 -1
  93. package/dist/src/planner/analysis/coverage-prover.js +1 -1
  94. package/dist/src/planner/analysis/equi-correlation.d.ts +26 -0
  95. package/dist/src/planner/analysis/equi-correlation.d.ts.map +1 -0
  96. package/dist/src/planner/analysis/equi-correlation.js +62 -0
  97. package/dist/src/planner/analysis/equi-correlation.js.map +1 -0
  98. package/dist/src/planner/analysis/query-rewrite-matcher.d.ts +54 -18
  99. package/dist/src/planner/analysis/query-rewrite-matcher.d.ts.map +1 -1
  100. package/dist/src/planner/analysis/query-rewrite-matcher.js +86 -42
  101. package/dist/src/planner/analysis/query-rewrite-matcher.js.map +1 -1
  102. package/dist/src/planner/analysis/sat-checker.d.ts +8 -1
  103. package/dist/src/planner/analysis/sat-checker.d.ts.map +1 -1
  104. package/dist/src/planner/analysis/sat-checker.js +117 -22
  105. package/dist/src/planner/analysis/sat-checker.js.map +1 -1
  106. package/dist/src/planner/analysis/scalar-invertibility.d.ts +13 -4
  107. package/dist/src/planner/analysis/scalar-invertibility.d.ts.map +1 -1
  108. package/dist/src/planner/analysis/scalar-invertibility.js +12 -3
  109. package/dist/src/planner/analysis/scalar-invertibility.js.map +1 -1
  110. package/dist/src/planner/analysis/scalar-subqueries.d.ts +28 -0
  111. package/dist/src/planner/analysis/scalar-subqueries.d.ts.map +1 -0
  112. package/dist/src/planner/analysis/scalar-subqueries.js +60 -0
  113. package/dist/src/planner/analysis/scalar-subqueries.js.map +1 -0
  114. package/dist/src/planner/analysis/update-lineage.d.ts +1 -1
  115. package/dist/src/planner/analysis/update-lineage.js +1 -1
  116. package/dist/src/planner/analysis/update-lineage.js.map +1 -1
  117. package/dist/src/planner/analysis/view-complement.d.ts +2 -2
  118. package/dist/src/planner/building/delete.js +2 -2
  119. package/dist/src/planner/building/delete.js.map +1 -1
  120. package/dist/src/planner/building/dml-target.d.ts +2 -2
  121. package/dist/src/planner/building/dml-target.js +2 -2
  122. package/dist/src/planner/building/insert.d.ts.map +1 -1
  123. package/dist/src/planner/building/insert.js +53 -2
  124. package/dist/src/planner/building/insert.js.map +1 -1
  125. package/dist/src/planner/building/schema-resolution.d.ts +3 -0
  126. package/dist/src/planner/building/schema-resolution.d.ts.map +1 -1
  127. package/dist/src/planner/building/schema-resolution.js +4 -5
  128. package/dist/src/planner/building/schema-resolution.js.map +1 -1
  129. package/dist/src/planner/building/select.d.ts.map +1 -1
  130. package/dist/src/planner/building/select.js +18 -4
  131. package/dist/src/planner/building/select.js.map +1 -1
  132. package/dist/src/planner/building/update.js +2 -2
  133. package/dist/src/planner/building/update.js.map +1 -1
  134. package/dist/src/planner/building/view-mutation-builder.js +7 -7
  135. package/dist/src/planner/building/view-mutation-builder.js.map +1 -1
  136. package/dist/src/planner/building/with.d.ts.map +1 -1
  137. package/dist/src/planner/building/with.js +5 -8
  138. package/dist/src/planner/building/with.js.map +1 -1
  139. package/dist/src/planner/cache/materialization-advisory.d.ts +34 -2
  140. package/dist/src/planner/cache/materialization-advisory.d.ts.map +1 -1
  141. package/dist/src/planner/cache/materialization-advisory.js +185 -42
  142. package/dist/src/planner/cache/materialization-advisory.js.map +1 -1
  143. package/dist/src/planner/cache/reference-graph.d.ts +14 -5
  144. package/dist/src/planner/cache/reference-graph.d.ts.map +1 -1
  145. package/dist/src/planner/cache/reference-graph.js +31 -39
  146. package/dist/src/planner/cache/reference-graph.js.map +1 -1
  147. package/dist/src/planner/cost/index.d.ts +30 -5
  148. package/dist/src/planner/cost/index.d.ts.map +1 -1
  149. package/dist/src/planner/cost/index.js +26 -0
  150. package/dist/src/planner/cost/index.js.map +1 -1
  151. package/dist/src/planner/debug.d.ts +3 -2
  152. package/dist/src/planner/debug.d.ts.map +1 -1
  153. package/dist/src/planner/debug.js +9 -3
  154. package/dist/src/planner/debug.js.map +1 -1
  155. package/dist/src/planner/framework/characteristics.d.ts +46 -19
  156. package/dist/src/planner/framework/characteristics.d.ts.map +1 -1
  157. package/dist/src/planner/framework/characteristics.js +50 -134
  158. package/dist/src/planner/framework/characteristics.js.map +1 -1
  159. package/dist/src/planner/framework/context.d.ts +8 -44
  160. package/dist/src/planner/framework/context.d.ts.map +1 -1
  161. package/dist/src/planner/framework/context.js +0 -91
  162. package/dist/src/planner/framework/context.js.map +1 -1
  163. package/dist/src/planner/framework/pass.d.ts +7 -0
  164. package/dist/src/planner/framework/pass.d.ts.map +1 -1
  165. package/dist/src/planner/framework/pass.js +84 -0
  166. package/dist/src/planner/framework/pass.js.map +1 -1
  167. package/dist/src/planner/framework/registry.d.ts +13 -42
  168. package/dist/src/planner/framework/registry.d.ts.map +1 -1
  169. package/dist/src/planner/framework/registry.js +14 -182
  170. package/dist/src/planner/framework/registry.js.map +1 -1
  171. package/dist/src/planner/mutation/backward-body.d.ts +2 -2
  172. package/dist/src/planner/mutation/cte-flatten.js +1 -1
  173. package/dist/src/planner/mutation/decomposition.d.ts +2 -2
  174. package/dist/src/planner/mutation/decomposition.js +1 -1
  175. package/dist/src/planner/mutation/lens-enforcement.d.ts.map +1 -1
  176. package/dist/src/planner/mutation/lens-enforcement.js +2 -1
  177. package/dist/src/planner/mutation/lens-enforcement.js.map +1 -1
  178. package/dist/src/planner/mutation/multi-source.d.ts +8 -8
  179. package/dist/src/planner/mutation/multi-source.js +11 -11
  180. package/dist/src/planner/mutation/multi-source.js.map +1 -1
  181. package/dist/src/planner/mutation/propagate.d.ts +1 -1
  182. package/dist/src/planner/mutation/propagate.js +1 -1
  183. package/dist/src/planner/mutation/scope-transform.d.ts +2 -2
  184. package/dist/src/planner/mutation/scope-transform.js +3 -3
  185. package/dist/src/planner/mutation/scope-transform.js.map +1 -1
  186. package/dist/src/planner/mutation/set-op.d.ts +1 -1
  187. package/dist/src/planner/mutation/set-op.d.ts.map +1 -1
  188. package/dist/src/planner/mutation/set-op.js +3 -2
  189. package/dist/src/planner/mutation/set-op.js.map +1 -1
  190. package/dist/src/planner/mutation/single-source.d.ts +3 -3
  191. package/dist/src/planner/mutation/single-source.js +9 -9
  192. package/dist/src/planner/mutation/single-source.js.map +1 -1
  193. package/dist/src/planner/nodes/aggregate-function.d.ts +3 -1
  194. package/dist/src/planner/nodes/aggregate-function.d.ts.map +1 -1
  195. package/dist/src/planner/nodes/aggregate-function.js +1 -0
  196. package/dist/src/planner/nodes/aggregate-function.js.map +1 -1
  197. package/dist/src/planner/nodes/aggregate-node.d.ts +1 -0
  198. package/dist/src/planner/nodes/aggregate-node.d.ts.map +1 -1
  199. package/dist/src/planner/nodes/aggregate-node.js +8 -1
  200. package/dist/src/planner/nodes/aggregate-node.js.map +1 -1
  201. package/dist/src/planner/nodes/alias-node.d.ts.map +1 -1
  202. package/dist/src/planner/nodes/alias-node.js +3 -1
  203. package/dist/src/planner/nodes/alias-node.js.map +1 -1
  204. package/dist/src/planner/nodes/asof-scan-node.d.ts.map +1 -1
  205. package/dist/src/planner/nodes/asof-scan-node.js +3 -2
  206. package/dist/src/planner/nodes/asof-scan-node.js.map +1 -1
  207. package/dist/src/planner/nodes/asserted-keys-node.d.ts +1 -1
  208. package/dist/src/planner/nodes/asserted-keys-node.d.ts.map +1 -1
  209. package/dist/src/planner/nodes/asserted-keys-node.js +5 -2
  210. package/dist/src/planner/nodes/asserted-keys-node.js.map +1 -1
  211. package/dist/src/planner/nodes/async-gather-node.d.ts.map +1 -1
  212. package/dist/src/planner/nodes/async-gather-node.js +3 -1
  213. package/dist/src/planner/nodes/async-gather-node.js.map +1 -1
  214. package/dist/src/planner/nodes/block.js +3 -3
  215. package/dist/src/planner/nodes/block.js.map +1 -1
  216. package/dist/src/planner/nodes/bloom-join-node.d.ts +2 -0
  217. package/dist/src/planner/nodes/bloom-join-node.d.ts.map +1 -1
  218. package/dist/src/planner/nodes/bloom-join-node.js +5 -1
  219. package/dist/src/planner/nodes/bloom-join-node.js.map +1 -1
  220. package/dist/src/planner/nodes/cache-node.d.ts +19 -2
  221. package/dist/src/planner/nodes/cache-node.d.ts.map +1 -1
  222. package/dist/src/planner/nodes/cache-node.js +43 -3
  223. package/dist/src/planner/nodes/cache-node.js.map +1 -1
  224. package/dist/src/planner/nodes/cte-node.d.ts +16 -1
  225. package/dist/src/planner/nodes/cte-node.d.ts.map +1 -1
  226. package/dist/src/planner/nodes/cte-node.js +16 -3
  227. package/dist/src/planner/nodes/cte-node.js.map +1 -1
  228. package/dist/src/planner/nodes/distinct-node.js +3 -3
  229. package/dist/src/planner/nodes/distinct-node.js.map +1 -1
  230. package/dist/src/planner/nodes/dml-executor-node.d.ts +21 -0
  231. package/dist/src/planner/nodes/dml-executor-node.d.ts.map +1 -1
  232. package/dist/src/planner/nodes/dml-executor-node.js.map +1 -1
  233. package/dist/src/planner/nodes/envelope-scan-node.d.ts +1 -1
  234. package/dist/src/planner/nodes/envelope-scan-node.js +1 -1
  235. package/dist/src/planner/nodes/fanout-lookup-join-node.d.ts.map +1 -1
  236. package/dist/src/planner/nodes/fanout-lookup-join-node.js +4 -2
  237. package/dist/src/planner/nodes/fanout-lookup-join-node.js.map +1 -1
  238. package/dist/src/planner/nodes/filter.d.ts +10 -2
  239. package/dist/src/planner/nodes/filter.d.ts.map +1 -1
  240. package/dist/src/planner/nodes/filter.js +81 -14
  241. package/dist/src/planner/nodes/filter.js.map +1 -1
  242. package/dist/src/planner/nodes/hash-aggregate.d.ts +12 -1
  243. package/dist/src/planner/nodes/hash-aggregate.d.ts.map +1 -1
  244. package/dist/src/planner/nodes/hash-aggregate.js +28 -1
  245. package/dist/src/planner/nodes/hash-aggregate.js.map +1 -1
  246. package/dist/src/planner/nodes/internal-recursive-cte-ref-node.d.ts +3 -1
  247. package/dist/src/planner/nodes/internal-recursive-cte-ref-node.d.ts.map +1 -1
  248. package/dist/src/planner/nodes/internal-recursive-cte-ref-node.js +1 -0
  249. package/dist/src/planner/nodes/internal-recursive-cte-ref-node.js.map +1 -1
  250. package/dist/src/planner/nodes/join-node.d.ts +3 -1
  251. package/dist/src/planner/nodes/join-node.d.ts.map +1 -1
  252. package/dist/src/planner/nodes/join-node.js +5 -5
  253. package/dist/src/planner/nodes/join-node.js.map +1 -1
  254. package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts.map +1 -1
  255. package/dist/src/planner/nodes/lens-auxiliary-access-node.js +4 -1
  256. package/dist/src/planner/nodes/lens-auxiliary-access-node.js.map +1 -1
  257. package/dist/src/planner/nodes/limit-offset.d.ts +1 -0
  258. package/dist/src/planner/nodes/limit-offset.d.ts.map +1 -1
  259. package/dist/src/planner/nodes/limit-offset.js +4 -3
  260. package/dist/src/planner/nodes/limit-offset.js.map +1 -1
  261. package/dist/src/planner/nodes/merge-join-node.d.ts +2 -0
  262. package/dist/src/planner/nodes/merge-join-node.d.ts.map +1 -1
  263. package/dist/src/planner/nodes/merge-join-node.js +6 -1
  264. package/dist/src/planner/nodes/merge-join-node.js.map +1 -1
  265. package/dist/src/planner/nodes/ordinal-slice-node.d.ts.map +1 -1
  266. package/dist/src/planner/nodes/ordinal-slice-node.js +3 -1
  267. package/dist/src/planner/nodes/ordinal-slice-node.js.map +1 -1
  268. package/dist/src/planner/nodes/plan-node.d.ts +68 -5
  269. package/dist/src/planner/nodes/plan-node.d.ts.map +1 -1
  270. package/dist/src/planner/nodes/plan-node.js +137 -24
  271. package/dist/src/planner/nodes/plan-node.js.map +1 -1
  272. package/dist/src/planner/nodes/project-node.d.ts +3 -2
  273. package/dist/src/planner/nodes/project-node.d.ts.map +1 -1
  274. package/dist/src/planner/nodes/project-node.js +1 -0
  275. package/dist/src/planner/nodes/project-node.js.map +1 -1
  276. package/dist/src/planner/nodes/recursive-cte-node.d.ts +30 -1
  277. package/dist/src/planner/nodes/recursive-cte-node.d.ts.map +1 -1
  278. package/dist/src/planner/nodes/recursive-cte-node.js +38 -5
  279. package/dist/src/planner/nodes/recursive-cte-node.js.map +1 -1
  280. package/dist/src/planner/nodes/reference.d.ts +5 -1
  281. package/dist/src/planner/nodes/reference.d.ts.map +1 -1
  282. package/dist/src/planner/nodes/reference.js +3 -0
  283. package/dist/src/planner/nodes/reference.js.map +1 -1
  284. package/dist/src/planner/nodes/remote-query-node.d.ts.map +1 -1
  285. package/dist/src/planner/nodes/remote-query-node.js +3 -1
  286. package/dist/src/planner/nodes/remote-query-node.js.map +1 -1
  287. package/dist/src/planner/nodes/retrieve-node.d.ts.map +1 -1
  288. package/dist/src/planner/nodes/retrieve-node.js +3 -1
  289. package/dist/src/planner/nodes/retrieve-node.js.map +1 -1
  290. package/dist/src/planner/nodes/set-operation-node.d.ts.map +1 -1
  291. package/dist/src/planner/nodes/set-operation-node.js +3 -1
  292. package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
  293. package/dist/src/planner/nodes/sink-node.d.ts.map +1 -1
  294. package/dist/src/planner/nodes/sink-node.js +3 -1
  295. package/dist/src/planner/nodes/sink-node.js.map +1 -1
  296. package/dist/src/planner/nodes/sort.d.ts +2 -1
  297. package/dist/src/planner/nodes/sort.d.ts.map +1 -1
  298. package/dist/src/planner/nodes/sort.js +9 -5
  299. package/dist/src/planner/nodes/sort.js.map +1 -1
  300. package/dist/src/planner/nodes/stream-aggregate.d.ts +12 -1
  301. package/dist/src/planner/nodes/stream-aggregate.d.ts.map +1 -1
  302. package/dist/src/planner/nodes/stream-aggregate.js +32 -3
  303. package/dist/src/planner/nodes/stream-aggregate.js.map +1 -1
  304. package/dist/src/planner/nodes/table-access-nodes.d.ts +2 -1
  305. package/dist/src/planner/nodes/table-access-nodes.d.ts.map +1 -1
  306. package/dist/src/planner/nodes/table-access-nodes.js +2 -0
  307. package/dist/src/planner/nodes/table-access-nodes.js.map +1 -1
  308. package/dist/src/planner/nodes/view-mutation-node.d.ts +2 -2
  309. package/dist/src/planner/nodes/view-mutation-node.d.ts.map +1 -1
  310. package/dist/src/planner/nodes/view-mutation-node.js +5 -1
  311. package/dist/src/planner/nodes/view-mutation-node.js.map +1 -1
  312. package/dist/src/planner/nodes/window-function.d.ts +3 -1
  313. package/dist/src/planner/nodes/window-function.d.ts.map +1 -1
  314. package/dist/src/planner/nodes/window-function.js +1 -0
  315. package/dist/src/planner/nodes/window-function.js.map +1 -1
  316. package/dist/src/planner/optimizer.d.ts +52 -8
  317. package/dist/src/planner/optimizer.d.ts.map +1 -1
  318. package/dist/src/planner/optimizer.js +1014 -895
  319. package/dist/src/planner/optimizer.js.map +1 -1
  320. package/dist/src/planner/rules/access/rule-select-access-path.d.ts.map +1 -1
  321. package/dist/src/planner/rules/access/rule-select-access-path.js +235 -202
  322. package/dist/src/planner/rules/access/rule-select-access-path.js.map +1 -1
  323. package/dist/src/planner/rules/cache/rule-cte-optimization.d.ts.map +1 -1
  324. package/dist/src/planner/rules/cache/rule-cte-optimization.js +8 -1
  325. package/dist/src/planner/rules/cache/rule-cte-optimization.js.map +1 -1
  326. package/dist/src/planner/rules/cache/rule-in-subquery-cache.d.ts +7 -0
  327. package/dist/src/planner/rules/cache/rule-in-subquery-cache.d.ts.map +1 -1
  328. package/dist/src/planner/rules/cache/rule-in-subquery-cache.js +14 -1
  329. package/dist/src/planner/rules/cache/rule-in-subquery-cache.js.map +1 -1
  330. package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts +2 -2
  331. package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts.map +1 -1
  332. package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js +55 -56
  333. package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js.map +1 -1
  334. package/dist/src/planner/rules/cache/rule-mutating-subquery-cache.d.ts.map +1 -1
  335. package/dist/src/planner/rules/cache/rule-mutating-subquery-cache.js +6 -0
  336. package/dist/src/planner/rules/cache/rule-mutating-subquery-cache.js.map +1 -1
  337. package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.d.ts +25 -0
  338. package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.d.ts.map +1 -0
  339. package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.js +176 -0
  340. package/dist/src/planner/rules/cache/rule-nested-loop-right-cache.js.map +1 -0
  341. package/dist/src/planner/rules/cache/rule-scalar-cse.d.ts +1 -1
  342. package/dist/src/planner/rules/cache/rule-scalar-cse.js +1 -1
  343. package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.d.ts +29 -0
  344. package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.d.ts.map +1 -0
  345. package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.js +66 -0
  346. package/dist/src/planner/rules/cache/rule-scalar-subquery-cache.js.map +1 -0
  347. package/dist/src/planner/rules/join/rule-fanout-batched-outer.d.ts +1 -1
  348. package/dist/src/planner/rules/join/rule-fanout-batched-outer.js +1 -1
  349. package/dist/src/planner/rules/join/rule-fanout-lookup-join.d.ts.map +1 -1
  350. package/dist/src/planner/rules/join/rule-fanout-lookup-join.js +1 -54
  351. package/dist/src/planner/rules/join/rule-fanout-lookup-join.js.map +1 -1
  352. package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.d.ts +1 -1
  353. package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js +1 -1
  354. package/dist/src/planner/rules/join/rule-join-elimination.d.ts +1 -1
  355. package/dist/src/planner/rules/join/rule-join-elimination.js +1 -1
  356. package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js +5 -6
  357. package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js.map +1 -1
  358. package/dist/src/planner/rules/predicate/rule-empty-relation-folding.d.ts +1 -1
  359. package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js +1 -1
  360. package/dist/src/planner/rules/predicate/rule-filter-contradiction.d.ts +2 -2
  361. package/dist/src/planner/rules/predicate/rule-filter-contradiction.d.ts.map +1 -1
  362. package/dist/src/planner/rules/predicate/rule-filter-contradiction.js +4 -3
  363. package/dist/src/planner/rules/predicate/rule-filter-contradiction.js.map +1 -1
  364. package/dist/src/planner/rules/predicate/rule-filter-selectivity.d.ts +19 -0
  365. package/dist/src/planner/rules/predicate/rule-filter-selectivity.d.ts.map +1 -0
  366. package/dist/src/planner/rules/predicate/rule-filter-selectivity.js +48 -0
  367. package/dist/src/planner/rules/predicate/rule-filter-selectivity.js.map +1 -0
  368. package/dist/src/planner/rules/predicate/rule-sargable-range-rewrite.js +12 -1
  369. package/dist/src/planner/rules/predicate/rule-sargable-range-rewrite.js.map +1 -1
  370. package/dist/src/planner/rules/retrieve/rule-grow-retrieve.d.ts.map +1 -1
  371. package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js +22 -20
  372. package/dist/src/planner/rules/retrieve/rule-grow-retrieve.js.map +1 -1
  373. package/dist/src/planner/rules/shared/index-style-context.d.ts +39 -0
  374. package/dist/src/planner/rules/shared/index-style-context.d.ts.map +1 -0
  375. package/dist/src/planner/rules/shared/index-style-context.js +26 -0
  376. package/dist/src/planner/rules/shared/index-style-context.js.map +1 -0
  377. package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.d.ts +1 -1
  378. package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.js +1 -1
  379. package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.d.ts +1 -1
  380. package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js +1 -1
  381. package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.d.ts +191 -0
  382. package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.d.ts.map +1 -0
  383. package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.js +623 -0
  384. package/dist/src/planner/rules/subquery/rule-scalar-agg-decorrelation.js.map +1 -0
  385. package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.d.ts +10 -1
  386. package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.d.ts.map +1 -1
  387. package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js +242 -51
  388. package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js.map +1 -1
  389. package/dist/src/planner/stats/analyze.d.ts.map +1 -1
  390. package/dist/src/planner/stats/analyze.js +2 -20
  391. package/dist/src/planner/stats/analyze.js.map +1 -1
  392. package/dist/src/planner/stats/histogram.d.ts.map +1 -1
  393. package/dist/src/planner/stats/histogram.js +4 -0
  394. package/dist/src/planner/stats/histogram.js.map +1 -1
  395. package/dist/src/planner/util/fd-utils.d.ts +2 -3
  396. package/dist/src/planner/util/fd-utils.d.ts.map +1 -1
  397. package/dist/src/planner/util/fd-utils.js +2 -3
  398. package/dist/src/planner/util/fd-utils.js.map +1 -1
  399. package/dist/src/planner/validation/plan-validator.d.ts +16 -0
  400. package/dist/src/planner/validation/plan-validator.d.ts.map +1 -1
  401. package/dist/src/planner/validation/plan-validator.js +36 -0
  402. package/dist/src/planner/validation/plan-validator.js.map +1 -1
  403. package/dist/src/runtime/async-util.d.ts +26 -0
  404. package/dist/src/runtime/async-util.d.ts.map +1 -1
  405. package/dist/src/runtime/async-util.js +139 -55
  406. package/dist/src/runtime/async-util.js.map +1 -1
  407. package/dist/src/runtime/cache/shared-cache.d.ts +7 -0
  408. package/dist/src/runtime/cache/shared-cache.d.ts.map +1 -1
  409. package/dist/src/runtime/cache/shared-cache.js +34 -1
  410. package/dist/src/runtime/cache/shared-cache.js.map +1 -1
  411. package/dist/src/runtime/context-helpers.d.ts +32 -4
  412. package/dist/src/runtime/context-helpers.d.ts.map +1 -1
  413. package/dist/src/runtime/context-helpers.js +26 -9
  414. package/dist/src/runtime/context-helpers.js.map +1 -1
  415. package/dist/src/runtime/deferred-constraint-queue.d.ts.map +1 -1
  416. package/dist/src/runtime/deferred-constraint-queue.js +2 -1
  417. package/dist/src/runtime/deferred-constraint-queue.js.map +1 -1
  418. package/dist/src/runtime/emission-context.d.ts +27 -3
  419. package/dist/src/runtime/emission-context.d.ts.map +1 -1
  420. package/dist/src/runtime/emission-context.js +38 -7
  421. package/dist/src/runtime/emission-context.js.map +1 -1
  422. package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
  423. package/dist/src/runtime/emit/add-constraint.js +7 -1
  424. package/dist/src/runtime/emit/add-constraint.js.map +1 -1
  425. package/dist/src/runtime/emit/aggregate.d.ts +2 -3
  426. package/dist/src/runtime/emit/aggregate.d.ts.map +1 -1
  427. package/dist/src/runtime/emit/aggregate.js +23 -28
  428. package/dist/src/runtime/emit/aggregate.js.map +1 -1
  429. package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
  430. package/dist/src/runtime/emit/alter-table.js +98 -9
  431. package/dist/src/runtime/emit/alter-table.js.map +1 -1
  432. package/dist/src/runtime/emit/analyze.d.ts.map +1 -1
  433. package/dist/src/runtime/emit/analyze.js +2 -1
  434. package/dist/src/runtime/emit/analyze.js.map +1 -1
  435. package/dist/src/runtime/emit/asof-scan.d.ts.map +1 -1
  436. package/dist/src/runtime/emit/asof-scan.js +13 -10
  437. package/dist/src/runtime/emit/asof-scan.js.map +1 -1
  438. package/dist/src/runtime/emit/async-gather.d.ts.map +1 -1
  439. package/dist/src/runtime/emit/async-gather.js +4 -3
  440. package/dist/src/runtime/emit/async-gather.js.map +1 -1
  441. package/dist/src/runtime/emit/between.d.ts.map +1 -1
  442. package/dist/src/runtime/emit/between.js +2 -1
  443. package/dist/src/runtime/emit/between.js.map +1 -1
  444. package/dist/src/runtime/emit/binary.d.ts.map +1 -1
  445. package/dist/src/runtime/emit/binary.js +122 -14
  446. package/dist/src/runtime/emit/binary.js.map +1 -1
  447. package/dist/src/runtime/emit/block.d.ts.map +1 -1
  448. package/dist/src/runtime/emit/block.js +2 -1
  449. package/dist/src/runtime/emit/block.js.map +1 -1
  450. package/dist/src/runtime/emit/bloom-join.d.ts.map +1 -1
  451. package/dist/src/runtime/emit/bloom-join.js +20 -8
  452. package/dist/src/runtime/emit/bloom-join.js.map +1 -1
  453. package/dist/src/runtime/emit/cache.d.ts +7 -3
  454. package/dist/src/runtime/emit/cache.d.ts.map +1 -1
  455. package/dist/src/runtime/emit/cache.js +25 -7
  456. package/dist/src/runtime/emit/cache.js.map +1 -1
  457. package/dist/src/runtime/emit/case.d.ts.map +1 -1
  458. package/dist/src/runtime/emit/case.js +66 -45
  459. package/dist/src/runtime/emit/case.js.map +1 -1
  460. package/dist/src/runtime/emit/cast.d.ts.map +1 -1
  461. package/dist/src/runtime/emit/cast.js +2 -1
  462. package/dist/src/runtime/emit/cast.js.map +1 -1
  463. package/dist/src/runtime/emit/constraint-check.d.ts.map +1 -1
  464. package/dist/src/runtime/emit/constraint-check.js +13 -7
  465. package/dist/src/runtime/emit/constraint-check.js.map +1 -1
  466. package/dist/src/runtime/emit/create-assertion.d.ts.map +1 -1
  467. package/dist/src/runtime/emit/create-assertion.js +2 -1
  468. package/dist/src/runtime/emit/create-assertion.js.map +1 -1
  469. package/dist/src/runtime/emit/create-index.d.ts.map +1 -1
  470. package/dist/src/runtime/emit/create-index.js +12 -1
  471. package/dist/src/runtime/emit/create-index.js.map +1 -1
  472. package/dist/src/runtime/emit/create-table.d.ts.map +1 -1
  473. package/dist/src/runtime/emit/create-table.js +12 -1
  474. package/dist/src/runtime/emit/create-table.js.map +1 -1
  475. package/dist/src/runtime/emit/cte-reference.d.ts.map +1 -1
  476. package/dist/src/runtime/emit/cte-reference.js +2 -1
  477. package/dist/src/runtime/emit/cte-reference.js.map +1 -1
  478. package/dist/src/runtime/emit/cte.d.ts.map +1 -1
  479. package/dist/src/runtime/emit/cte.js +45 -21
  480. package/dist/src/runtime/emit/cte.js.map +1 -1
  481. package/dist/src/runtime/emit/ddl-transaction-policy.d.ts +50 -0
  482. package/dist/src/runtime/emit/ddl-transaction-policy.d.ts.map +1 -0
  483. package/dist/src/runtime/emit/ddl-transaction-policy.js +69 -0
  484. package/dist/src/runtime/emit/ddl-transaction-policy.js.map +1 -0
  485. package/dist/src/runtime/emit/delete.d.ts.map +1 -1
  486. package/dist/src/runtime/emit/delete.js +2 -1
  487. package/dist/src/runtime/emit/delete.js.map +1 -1
  488. package/dist/src/runtime/emit/distinct.d.ts.map +1 -1
  489. package/dist/src/runtime/emit/distinct.js +2 -1
  490. package/dist/src/runtime/emit/distinct.js.map +1 -1
  491. package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
  492. package/dist/src/runtime/emit/dml-executor.js +138 -74
  493. package/dist/src/runtime/emit/dml-executor.js.map +1 -1
  494. package/dist/src/runtime/emit/drop-assertion.d.ts.map +1 -1
  495. package/dist/src/runtime/emit/drop-assertion.js +2 -1
  496. package/dist/src/runtime/emit/drop-assertion.js.map +1 -1
  497. package/dist/src/runtime/emit/drop-index.d.ts.map +1 -1
  498. package/dist/src/runtime/emit/drop-index.js +17 -1
  499. package/dist/src/runtime/emit/drop-index.js.map +1 -1
  500. package/dist/src/runtime/emit/drop-table.d.ts.map +1 -1
  501. package/dist/src/runtime/emit/drop-table.js +11 -1
  502. package/dist/src/runtime/emit/drop-table.js.map +1 -1
  503. package/dist/src/runtime/emit/eager-prefetch.d.ts.map +1 -1
  504. package/dist/src/runtime/emit/eager-prefetch.js +2 -1
  505. package/dist/src/runtime/emit/eager-prefetch.js.map +1 -1
  506. package/dist/src/runtime/emit/envelope-scan.d.ts.map +1 -1
  507. package/dist/src/runtime/emit/envelope-scan.js +2 -1
  508. package/dist/src/runtime/emit/envelope-scan.js.map +1 -1
  509. package/dist/src/runtime/emit/fanout-lookup-join.d.ts.map +1 -1
  510. package/dist/src/runtime/emit/fanout-lookup-join.js +3 -2
  511. package/dist/src/runtime/emit/fanout-lookup-join.js.map +1 -1
  512. package/dist/src/runtime/emit/filter.d.ts.map +1 -1
  513. package/dist/src/runtime/emit/filter.js +8 -3
  514. package/dist/src/runtime/emit/filter.js.map +1 -1
  515. package/dist/src/runtime/emit/hash-aggregate.d.ts.map +1 -1
  516. package/dist/src/runtime/emit/hash-aggregate.js +16 -11
  517. package/dist/src/runtime/emit/hash-aggregate.js.map +1 -1
  518. package/dist/src/runtime/emit/insert.d.ts.map +1 -1
  519. package/dist/src/runtime/emit/insert.js +2 -1
  520. package/dist/src/runtime/emit/insert.js.map +1 -1
  521. package/dist/src/runtime/emit/join.d.ts.map +1 -1
  522. package/dist/src/runtime/emit/join.js +20 -6
  523. package/dist/src/runtime/emit/join.js.map +1 -1
  524. package/dist/src/runtime/emit/limit-offset.d.ts.map +1 -1
  525. package/dist/src/runtime/emit/limit-offset.js +2 -1
  526. package/dist/src/runtime/emit/limit-offset.js.map +1 -1
  527. package/dist/src/runtime/emit/literal.d.ts.map +1 -1
  528. package/dist/src/runtime/emit/literal.js +2 -1
  529. package/dist/src/runtime/emit/literal.js.map +1 -1
  530. package/dist/src/runtime/emit/materialized-view-helpers.d.ts +22 -0
  531. package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -1
  532. package/dist/src/runtime/emit/materialized-view-helpers.js +128 -16
  533. package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -1
  534. package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -1
  535. package/dist/src/runtime/emit/materialized-view.js +6 -0
  536. package/dist/src/runtime/emit/materialized-view.js.map +1 -1
  537. package/dist/src/runtime/emit/merge-join.d.ts.map +1 -1
  538. package/dist/src/runtime/emit/merge-join.js +14 -4
  539. package/dist/src/runtime/emit/merge-join.js.map +1 -1
  540. package/dist/src/runtime/emit/ordinal-slice.d.ts.map +1 -1
  541. package/dist/src/runtime/emit/ordinal-slice.js +2 -1
  542. package/dist/src/runtime/emit/ordinal-slice.js.map +1 -1
  543. package/dist/src/runtime/emit/pragma.d.ts.map +1 -1
  544. package/dist/src/runtime/emit/pragma.js +2 -1
  545. package/dist/src/runtime/emit/pragma.js.map +1 -1
  546. package/dist/src/runtime/emit/project.d.ts.map +1 -1
  547. package/dist/src/runtime/emit/project.js +7 -2
  548. package/dist/src/runtime/emit/project.js.map +1 -1
  549. package/dist/src/runtime/emit/recursive-cte.d.ts.map +1 -1
  550. package/dist/src/runtime/emit/recursive-cte.js +61 -2
  551. package/dist/src/runtime/emit/recursive-cte.js.map +1 -1
  552. package/dist/src/runtime/emit/returning.d.ts.map +1 -1
  553. package/dist/src/runtime/emit/returning.js +7 -2
  554. package/dist/src/runtime/emit/returning.js.map +1 -1
  555. package/dist/src/runtime/emit/scalar-function.d.ts.map +1 -1
  556. package/dist/src/runtime/emit/scalar-function.js +2 -1
  557. package/dist/src/runtime/emit/scalar-function.js.map +1 -1
  558. package/dist/src/runtime/emit/scan.d.ts.map +1 -1
  559. package/dist/src/runtime/emit/scan.js +55 -17
  560. package/dist/src/runtime/emit/scan.js.map +1 -1
  561. package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
  562. package/dist/src/runtime/emit/schema-declarative.js +6 -5
  563. package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
  564. package/dist/src/runtime/emit/sequencing.d.ts.map +1 -1
  565. package/dist/src/runtime/emit/sequencing.js +2 -1
  566. package/dist/src/runtime/emit/sequencing.js.map +1 -1
  567. package/dist/src/runtime/emit/set-operation.d.ts.map +1 -1
  568. package/dist/src/runtime/emit/set-operation.js +6 -5
  569. package/dist/src/runtime/emit/set-operation.js.map +1 -1
  570. package/dist/src/runtime/emit/sink.d.ts.map +1 -1
  571. package/dist/src/runtime/emit/sink.js +2 -1
  572. package/dist/src/runtime/emit/sink.js.map +1 -1
  573. package/dist/src/runtime/emit/sort.d.ts.map +1 -1
  574. package/dist/src/runtime/emit/sort.js +2 -1
  575. package/dist/src/runtime/emit/sort.js.map +1 -1
  576. package/dist/src/runtime/emit/subquery.d.ts.map +1 -1
  577. package/dist/src/runtime/emit/subquery.js +42 -23
  578. package/dist/src/runtime/emit/subquery.js.map +1 -1
  579. package/dist/src/runtime/emit/table-valued-function.d.ts.map +1 -1
  580. package/dist/src/runtime/emit/table-valued-function.js +2 -1
  581. package/dist/src/runtime/emit/table-valued-function.js.map +1 -1
  582. package/dist/src/runtime/emit/temporal-arithmetic.d.ts.map +1 -1
  583. package/dist/src/runtime/emit/temporal-arithmetic.js +2 -1
  584. package/dist/src/runtime/emit/temporal-arithmetic.js.map +1 -1
  585. package/dist/src/runtime/emit/transaction.d.ts.map +1 -1
  586. package/dist/src/runtime/emit/transaction.js +2 -1
  587. package/dist/src/runtime/emit/transaction.js.map +1 -1
  588. package/dist/src/runtime/emit/unary.d.ts.map +1 -1
  589. package/dist/src/runtime/emit/unary.js +2 -1
  590. package/dist/src/runtime/emit/unary.js.map +1 -1
  591. package/dist/src/runtime/emit/update.d.ts.map +1 -1
  592. package/dist/src/runtime/emit/update.js +2 -1
  593. package/dist/src/runtime/emit/update.js.map +1 -1
  594. package/dist/src/runtime/emit/values.d.ts.map +1 -1
  595. package/dist/src/runtime/emit/values.js +2 -1
  596. package/dist/src/runtime/emit/values.js.map +1 -1
  597. package/dist/src/runtime/emit/view-mutation.d.ts +1 -1
  598. package/dist/src/runtime/emit/view-mutation.d.ts.map +1 -1
  599. package/dist/src/runtime/emit/view-mutation.js +6 -3
  600. package/dist/src/runtime/emit/view-mutation.js.map +1 -1
  601. package/dist/src/runtime/emit/window.d.ts.map +1 -1
  602. package/dist/src/runtime/emit/window.js +26 -9
  603. package/dist/src/runtime/emit/window.js.map +1 -1
  604. package/dist/src/runtime/emitters.d.ts +11 -3
  605. package/dist/src/runtime/emitters.d.ts.map +1 -1
  606. package/dist/src/runtime/emitters.js +13 -22
  607. package/dist/src/runtime/emitters.js.map +1 -1
  608. package/dist/src/runtime/foreign-key-actions.d.ts.map +1 -1
  609. package/dist/src/runtime/foreign-key-actions.js +39 -13
  610. package/dist/src/runtime/foreign-key-actions.js.map +1 -1
  611. package/dist/src/runtime/parallel-driver.d.ts +32 -4
  612. package/dist/src/runtime/parallel-driver.d.ts.map +1 -1
  613. package/dist/src/runtime/parallel-driver.js +88 -18
  614. package/dist/src/runtime/parallel-driver.js.map +1 -1
  615. package/dist/src/runtime/scheduler.d.ts +56 -7
  616. package/dist/src/runtime/scheduler.d.ts.map +1 -1
  617. package/dist/src/runtime/scheduler.js +223 -200
  618. package/dist/src/runtime/scheduler.js.map +1 -1
  619. package/dist/src/runtime/strict-flags.d.ts +22 -0
  620. package/dist/src/runtime/strict-flags.d.ts.map +1 -0
  621. package/dist/src/runtime/strict-flags.js +26 -0
  622. package/dist/src/runtime/strict-flags.js.map +1 -0
  623. package/dist/src/runtime/strict-fork.d.ts +4 -4
  624. package/dist/src/runtime/strict-fork.d.ts.map +1 -1
  625. package/dist/src/runtime/strict-fork.js +175 -31
  626. package/dist/src/runtime/strict-fork.js.map +1 -1
  627. package/dist/src/runtime/types.d.ts +102 -4
  628. package/dist/src/runtime/types.d.ts.map +1 -1
  629. package/dist/src/runtime/types.js +37 -0
  630. package/dist/src/runtime/types.js.map +1 -1
  631. package/dist/src/runtime/utils.d.ts.map +1 -1
  632. package/dist/src/runtime/utils.js +8 -17
  633. package/dist/src/runtime/utils.js.map +1 -1
  634. package/dist/src/schema/column.d.ts +8 -0
  635. package/dist/src/schema/column.d.ts.map +1 -1
  636. package/dist/src/schema/column.js.map +1 -1
  637. package/dist/src/schema/ddl-generator.js +5 -0
  638. package/dist/src/schema/ddl-generator.js.map +1 -1
  639. package/dist/src/schema/function.d.ts +79 -0
  640. package/dist/src/schema/function.d.ts.map +1 -1
  641. package/dist/src/schema/function.js.map +1 -1
  642. package/dist/src/schema/lens-compiler.js +2 -2
  643. package/dist/src/schema/lens-compiler.js.map +1 -1
  644. package/dist/src/schema/lens-prover.d.ts.map +1 -1
  645. package/dist/src/schema/lens-prover.js +2 -3
  646. package/dist/src/schema/lens-prover.js.map +1 -1
  647. package/dist/src/schema/lens.d.ts +1 -1
  648. package/dist/src/schema/manager.d.ts.map +1 -1
  649. package/dist/src/schema/manager.js +7 -2
  650. package/dist/src/schema/manager.js.map +1 -1
  651. package/dist/src/schema/rename-rewriter.d.ts +61 -0
  652. package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
  653. package/dist/src/schema/rename-rewriter.js +77 -0
  654. package/dist/src/schema/rename-rewriter.js.map +1 -1
  655. package/dist/src/schema/table.d.ts +36 -8
  656. package/dist/src/schema/table.d.ts.map +1 -1
  657. package/dist/src/schema/table.js +61 -11
  658. package/dist/src/schema/table.js.map +1 -1
  659. package/dist/src/schema/unique-enforcement.d.ts +28 -5
  660. package/dist/src/schema/unique-enforcement.d.ts.map +1 -1
  661. package/dist/src/schema/unique-enforcement.js +37 -5
  662. package/dist/src/schema/unique-enforcement.js.map +1 -1
  663. package/dist/src/schema/view.d.ts +1 -1
  664. package/dist/src/schema/view.js +1 -1
  665. package/dist/src/types/json-type.d.ts.map +1 -1
  666. package/dist/src/types/json-type.js +18 -8
  667. package/dist/src/types/json-type.js.map +1 -1
  668. package/dist/src/types/logical-type.d.ts +26 -0
  669. package/dist/src/types/logical-type.d.ts.map +1 -1
  670. package/dist/src/types/logical-type.js.map +1 -1
  671. package/dist/src/util/comparison.d.ts +83 -59
  672. package/dist/src/util/comparison.d.ts.map +1 -1
  673. package/dist/src/util/comparison.js +191 -131
  674. package/dist/src/util/comparison.js.map +1 -1
  675. package/dist/src/util/json-canonical.d.ts +34 -0
  676. package/dist/src/util/json-canonical.d.ts.map +1 -0
  677. package/dist/src/util/json-canonical.js +32 -0
  678. package/dist/src/util/json-canonical.js.map +1 -0
  679. package/dist/src/util/key-serializer.d.ts +10 -3
  680. package/dist/src/util/key-serializer.d.ts.map +1 -1
  681. package/dist/src/util/key-serializer.js +43 -17
  682. package/dist/src/util/key-serializer.js.map +1 -1
  683. package/dist/src/util/key-tuple-codec.d.ts +37 -0
  684. package/dist/src/util/key-tuple-codec.d.ts.map +1 -0
  685. package/dist/src/util/key-tuple-codec.js +65 -0
  686. package/dist/src/util/key-tuple-codec.js.map +1 -0
  687. package/dist/src/util/latches.d.ts +29 -3
  688. package/dist/src/util/latches.d.ts.map +1 -1
  689. package/dist/src/util/latches.js +60 -6
  690. package/dist/src/util/latches.js.map +1 -1
  691. package/dist/src/util/patterns.d.ts +22 -8
  692. package/dist/src/util/patterns.d.ts.map +1 -1
  693. package/dist/src/util/patterns.js +87 -16
  694. package/dist/src/util/patterns.js.map +1 -1
  695. package/dist/src/util/qualified-name.d.ts +10 -0
  696. package/dist/src/util/qualified-name.d.ts.map +1 -0
  697. package/dist/src/util/qualified-name.js +15 -0
  698. package/dist/src/util/qualified-name.js.map +1 -0
  699. package/dist/src/vtab/backing-host.d.ts +8 -1
  700. package/dist/src/vtab/backing-host.d.ts.map +1 -1
  701. package/dist/src/vtab/backing-host.js +7 -0
  702. package/dist/src/vtab/backing-host.js.map +1 -1
  703. package/dist/src/vtab/best-access-plan.d.ts +18 -0
  704. package/dist/src/vtab/best-access-plan.d.ts.map +1 -1
  705. package/dist/src/vtab/best-access-plan.js +15 -0
  706. package/dist/src/vtab/best-access-plan.js.map +1 -1
  707. package/dist/src/vtab/capabilities.d.ts +44 -0
  708. package/dist/src/vtab/capabilities.d.ts.map +1 -1
  709. package/dist/src/vtab/filter-info.d.ts +42 -0
  710. package/dist/src/vtab/filter-info.d.ts.map +1 -1
  711. package/dist/src/vtab/filter-info.js +137 -1
  712. package/dist/src/vtab/filter-info.js.map +1 -1
  713. package/dist/src/vtab/idx-str.d.ts +62 -0
  714. package/dist/src/vtab/idx-str.d.ts.map +1 -0
  715. package/dist/src/vtab/idx-str.js +130 -0
  716. package/dist/src/vtab/idx-str.js.map +1 -0
  717. package/dist/src/vtab/index-descriptor.d.ts +122 -0
  718. package/dist/src/vtab/index-descriptor.d.ts.map +1 -0
  719. package/dist/src/vtab/index-descriptor.js +104 -0
  720. package/dist/src/vtab/index-descriptor.js.map +1 -0
  721. package/dist/src/vtab/mapping-advertisement.d.ts +1 -1
  722. package/dist/src/vtab/memory/index.d.ts +12 -4
  723. package/dist/src/vtab/memory/index.d.ts.map +1 -1
  724. package/dist/src/vtab/memory/index.js +17 -9
  725. package/dist/src/vtab/memory/index.js.map +1 -1
  726. package/dist/src/vtab/memory/layer/base.d.ts +85 -31
  727. package/dist/src/vtab/memory/layer/base.d.ts.map +1 -1
  728. package/dist/src/vtab/memory/layer/base.js +127 -86
  729. package/dist/src/vtab/memory/layer/base.js.map +1 -1
  730. package/dist/src/vtab/memory/layer/connection.d.ts +27 -0
  731. package/dist/src/vtab/memory/layer/connection.d.ts.map +1 -1
  732. package/dist/src/vtab/memory/layer/connection.js +36 -1
  733. package/dist/src/vtab/memory/layer/connection.js.map +1 -1
  734. package/dist/src/vtab/memory/layer/interface.d.ts +24 -4
  735. package/dist/src/vtab/memory/layer/interface.d.ts.map +1 -1
  736. package/dist/src/vtab/memory/layer/manager.d.ts +212 -16
  737. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  738. package/dist/src/vtab/memory/layer/manager.js +754 -170
  739. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  740. package/dist/src/vtab/memory/layer/plan-filter.d.ts +26 -1
  741. package/dist/src/vtab/memory/layer/plan-filter.d.ts.map +1 -1
  742. package/dist/src/vtab/memory/layer/plan-filter.js +29 -11
  743. package/dist/src/vtab/memory/layer/plan-filter.js.map +1 -1
  744. package/dist/src/vtab/memory/layer/safe-iterate.d.ts +2 -2
  745. package/dist/src/vtab/memory/layer/safe-iterate.d.ts.map +1 -1
  746. package/dist/src/vtab/memory/layer/safe-iterate.js +2 -2
  747. package/dist/src/vtab/memory/layer/safe-iterate.js.map +1 -1
  748. package/dist/src/vtab/memory/layer/scan-layer.d.ts +5 -1
  749. package/dist/src/vtab/memory/layer/scan-layer.d.ts.map +1 -1
  750. package/dist/src/vtab/memory/layer/scan-layer.js +59 -24
  751. package/dist/src/vtab/memory/layer/scan-layer.js.map +1 -1
  752. package/dist/src/vtab/memory/layer/scan-plan.d.ts.map +1 -1
  753. package/dist/src/vtab/memory/layer/scan-plan.js +13 -21
  754. package/dist/src/vtab/memory/layer/scan-plan.js.map +1 -1
  755. package/dist/src/vtab/memory/layer/transaction.d.ts +169 -7
  756. package/dist/src/vtab/memory/layer/transaction.d.ts.map +1 -1
  757. package/dist/src/vtab/memory/layer/transaction.js +334 -9
  758. package/dist/src/vtab/memory/layer/transaction.js.map +1 -1
  759. package/dist/src/vtab/memory/module.d.ts +11 -4
  760. package/dist/src/vtab/memory/module.d.ts.map +1 -1
  761. package/dist/src/vtab/memory/module.js +35 -16
  762. package/dist/src/vtab/memory/module.js.map +1 -1
  763. package/dist/src/vtab/memory/table.d.ts +30 -4
  764. package/dist/src/vtab/memory/table.d.ts.map +1 -1
  765. package/dist/src/vtab/memory/table.js +63 -15
  766. package/dist/src/vtab/memory/table.js.map +1 -1
  767. package/dist/src/vtab/memory/utils/predicate.d.ts +9 -1
  768. package/dist/src/vtab/memory/utils/predicate.d.ts.map +1 -1
  769. package/dist/src/vtab/memory/utils/predicate.js +35 -14
  770. package/dist/src/vtab/memory/utils/predicate.js.map +1 -1
  771. package/dist/src/vtab/memory/utils/primary-key-encode.d.ts.map +1 -1
  772. package/dist/src/vtab/memory/utils/primary-key-encode.js +12 -6
  773. package/dist/src/vtab/memory/utils/primary-key-encode.js.map +1 -1
  774. package/dist/src/vtab/memory/utils/primary-key.d.ts +6 -1
  775. package/dist/src/vtab/memory/utils/primary-key.d.ts.map +1 -1
  776. package/dist/src/vtab/memory/utils/primary-key.js +12 -8
  777. package/dist/src/vtab/memory/utils/primary-key.js.map +1 -1
  778. package/dist/src/vtab/module.d.ts +38 -14
  779. package/dist/src/vtab/module.d.ts.map +1 -1
  780. package/dist/src/vtab/table.d.ts +41 -2
  781. package/dist/src/vtab/table.d.ts.map +1 -1
  782. package/dist/src/vtab/table.js +19 -0
  783. package/dist/src/vtab/table.js.map +1 -1
  784. package/package.json +5 -4
  785. package/dist/src/planner/rules/cache/rule-materialization-advisory.d.ts +0 -18
  786. package/dist/src/planner/rules/cache/rule-materialization-advisory.d.ts.map +0 -1
  787. package/dist/src/planner/rules/cache/rule-materialization-advisory.js +0 -65
  788. package/dist/src/planner/rules/cache/rule-materialization-advisory.js.map +0 -1
@@ -0,0 +1,1423 @@
1
+ /**
2
+ * Materialized-view maintenance — **plan builders**. The cost-gated builder that compiles
3
+ * an MV body into a {@link MaintenancePlan} (the bounded-delta arms plus the full-rebuild
4
+ * floor) and the residual-compile / source-stats helpers they share. Extracted from
5
+ * database-materialized-views.ts as free functions over {@link MaterializedViewManagerContext};
6
+ * the sole caller in the manager is `registerMaterializedView` → {@link buildMaintenancePlan}.
7
+ */
8
+ import { QuereusError } from '../common/errors.js';
9
+ import { StatusCode } from '../common/types.js';
10
+ import { ColumnReferenceNode } from '../planner/nodes/reference.js';
11
+ import { AggregateFunctionCallNode } from '../planner/nodes/aggregate-function.js';
12
+ import { isAggregateFunctionSchema } from '../schema/function.js';
13
+ import { PhysicalType } from '../types/logical-type.js';
14
+ import { checkDeterministic } from '../planner/validation/determinism-validator.js';
15
+ import { emitPlanNode } from '../runtime/emitters.js';
16
+ import { EmissionContext } from '../runtime/emission-context.js';
17
+ import { Scheduler } from '../runtime/scheduler.js';
18
+ import { PlanNodeType } from '../planner/nodes/plan-node-type.js';
19
+ import { injectKeyFilter } from '../planner/analysis/key-filter.js';
20
+ import { keysOf } from '../planner/util/fd-utils.js';
21
+ import { deriveCoarsenedBackingKey, resolveValuePreservingSourceCol } from '../planner/analysis/coarsened-key.js';
22
+ import { proveOneToOneJoin } from '../planner/analysis/coverage-prover.js';
23
+ import { CapabilityDetectors } from '../planner/framework/characteristics.js';
24
+ import { selectMaintenanceStrategy, isFullRebuildPathological, seqScanCost, filterCost, projectCost, } from '../planner/cost/index.js';
25
+ import { tryResolveBackingHost } from '../runtime/emit/materialized-view-helpers.js';
26
+ import { compilePredicate } from '../vtab/memory/utils/predicate.js';
27
+ import { findNonReplicableFunction, findNonReplicableCollation, findNonDeterministic, normalizeCollation, isBinaryCollation, findAggregate, containsNodeType, containsAnyJoin, countNodeType, countJoins, findTableFunctionCall, collectTableRefs, collectProducingExprs, resolveTransitiveSourceCol, bodyWhereReferencesLookup, bodyWhereIsNonDeterministic, relationalAttributes, rootRelationalNode, cannotMaterialize, nonReplicableDerivationError, nonReplicableCollationDerivationError, isSingleRowEvaluable, compileSourceRowEvaluator, } from './database-materialized-views-analysis.js';
28
+ import { BINARY_COLLATION } from '../util/comparison.js';
29
+ /** Fallback source row estimate when the StatsProvider has no count (mirrors the
30
+ * optimizer's naive default). Only feeds the create-time maintenance cost gate. */
31
+ const DEFAULT_SOURCE_ROWS = 1000;
32
+ /** Create-time estimate of the fraction of changes to a **tighten-only** (min/max) delta
33
+ * aggregate that retract (a delete / update-old-image) and so re-derive their group from
34
+ * the key-filtered residual instead of pure arithmetic. Feeds the `'delta-aggregate'` cost
35
+ * blend ({@link maintenanceCost}); kept well below 1 so an insert-dominated body still
36
+ * prefers the delta arm, yet non-zero so a tighten body costs more than a pure-group one. */
37
+ const DELTA_TIGHTEN_FALLBACK_RATIO = 0.25;
38
+ /**
39
+ * Snapshot a backing table's physical primary key as the maintenance plans consume it: the
40
+ * declared `(index, desc, collation)` triple plus the collation comparator that name resolves
41
+ * to on this database. Every arm builds its `backingPkDefinition` through here, so the per-row
42
+ * key comparisons in `database-materialized-views-apply.ts` never do a registry lookup — and
43
+ * never silently degrade a custom collation to byte order.
44
+ *
45
+ * NOTE: this freezes the comparator into the plan. Re-registering a collation name with
46
+ * `db.registerCollation(...)` after the plan was built leaves the plan on the old comparator
47
+ * until the MV is re-registered — which any schema change does, but a bare `registerCollation`
48
+ * does not. Same exposure as index comparators, which resolve once at table open; if that ever
49
+ * needs fixing, fix it for both.
50
+ *
51
+ * NOTE: `resolver` throws on an unregistered collation name. Unreachable today — the memory
52
+ * module rejects the backing `create` first — so this never converts a working `create
53
+ * materialized view` into an error. If a backing host is ever added that tolerates an
54
+ * unregistered name on a key column, this becomes the first site to throw, at plan-build time.
55
+ */
56
+ function resolveBackingPkColumns(backing, resolver) {
57
+ return backing.primaryKeyDefinition.map(d => ({
58
+ index: d.index,
59
+ desc: d.desc,
60
+ collation: d.collation,
61
+ collationFn: d.collation ? resolver(d.collation) : BINARY_COLLATION,
62
+ }));
63
+ }
64
+ /**
65
+ * Build the row-time maintenance plan for an MV — **cost-gated, with a floor, never a
66
+ * shape allowlist**. The builder tries to match a bounded-delta arm by shape
67
+ * ({@link tryBuildBoundedDeltaArm}); a body that matches **none** falls through to the
68
+ * always-correct {@link buildFullRebuildPlan} floor (re-evaluate the whole body, replace
69
+ * the backing transactionally). **No body is rejected for its shape.** Only five
70
+ * create-time rejections remain, all non-shape (docs/invariants.md MV-006):
71
+ * - a **non-deterministic** body without `pragma nondeterministic_schema` — a hard reject
72
+ * in the matched arm (so the arm-specific determinism diagnostic survives) or, for a
73
+ * body matching no arm, in the floor's whole-body determinism check;
74
+ * - a **bag** (no provable unique key) — the floor's `keysOf` reject (a duplicate-producing
75
+ * body usually fails the set contract earlier, at create-fill);
76
+ * - a body with **no relational output**;
77
+ * - a body that **reads no source table** (`select 42`) — the floor's `sourceBases` reject;
78
+ * there is no source to index the plan under and no write that could dirty it;
79
+ * - a **full-rebuild-only body over a source past the size threshold**
80
+ * ({@link isFullRebuildPathological}, the `materialized_view_rebuild_row_threshold` option).
81
+ *
82
+ * The single source may itself be another MV's backing table (an MV-over-MV body):
83
+ * `building/select.ts` rewrites a reference to `mv1` into a `TableReference` against
84
+ * `mv1`'s backing table, so the source base is `mv1`'s backing base and the same checks
85
+ * evaluate against the (keyed `memory`) backing schema unchanged. A write to `mv1` then
86
+ * drives `mv2` via the cascade in {@link maintainRowTime}.
87
+ *
88
+ * Eligibility is a *cost choice* among the body's structurally-sound strategies
89
+ * ({@link selectMaintenanceStrategy}): the bounded-delta arms are preferred by the argmin
90
+ * cost gate, and full-rebuild is selected exactly when no bounded-delta arm is sound (an
91
+ * empty sound set resolves to the floor) — so an existing eligible shape is unaffected.
92
+ */
93
+ export function buildMaintenancePlan(ctx, mv) {
94
+ const db = ctx;
95
+ // Analyze the MV's own body to compile maintenance; suppress the read-side
96
+ // rewrite so the body stays over its SOURCE table, not re-pointed at this
97
+ // MV's backing (which the maintenance plan is what keeps consistent).
98
+ const analyzed = db.schemaManager.withSuppressedMaterializedViewRewrite(() => {
99
+ const { plan } = ctx._buildPlan([mv.derivation.selectAst]);
100
+ return ctx.optimizer.optimizeForAnalysis(plan, db);
101
+ });
102
+ // Replicable-determinism gate — host-conditional, inert by default. A backing host
103
+ // whose backing replicates across peers (the sync-store) sets
104
+ // `requiresReplicableDerivations` so a platform-dependent UDF or collation cannot
105
+ // diverge peers. Checked here — after `analyzed`, before arm selection — so it applies
106
+ // regardless of which maintenance arm wins, over the SAME analyzed plan the determinism
107
+ // gate walks (nested calls, WHERE / GROUP BY / aggregate-arg / TVF-arg positions all
108
+ // reached). It sits NEXT TO, not in place of, the determinism gate: the two are
109
+ // orthogonal, so this is NOT lifted by `pragma nondeterministic_schema`. A memory/store
110
+ // host leaves the flag undefined ⇒ this block is skipped ⇒ zero behavior change.
111
+ // Idempotent (same body ⇒ same verdict), so it is also desirable on re-register /
112
+ // catalog import: a tampered catalog cannot smuggle a non-replicable body past a
113
+ // demanding host. Two gates of the same shape: a non-replicable FUNCTION (which the
114
+ // body walk's function-bearing nodes carry) and a non-replicable COLLATION (which rides
115
+ // each scalar node's resolved type plus the backing key's declared collations).
116
+ // Resolve the host LENIENTLY: at the create-time gate registration of an
117
+ // `alter table … set maintained` attach, a module that materializes its durable
118
+ // backing late (lamina's `ensureBackingForAttach`, after this gate) has no host
119
+ // yet. The host is used here ONLY for the host-conditional, default-inert
120
+ // `requiresReplicableDerivations` gate — a host that sets it (the synced-store
121
+ // flavor) always exists by plan-build time, so skipping the gate when the host
122
+ // is absent never lets a non-replicable body slip past. The reconcile resolves
123
+ // the host for real, and the maintenance arms re-resolve it per use.
124
+ const host = tryResolveBackingHost(db, mv);
125
+ if (host?.requiresReplicableDerivations) {
126
+ const offendingFn = findNonReplicableFunction(analyzed);
127
+ if (offendingFn)
128
+ throw nonReplicableDerivationError(mv.name, offendingFn);
129
+ const offendingCollation = findNonReplicableCollation(analyzed, mv, db);
130
+ if (offendingCollation)
131
+ throw nonReplicableCollationDerivationError(mv.name, offendingCollation);
132
+ }
133
+ // Try a bounded-delta arm; a shape that fits none falls through to the floor.
134
+ const boundedDelta = tryBuildBoundedDeltaArm(ctx, mv, analyzed);
135
+ // A residual arm additionally carries the whole-body scheduler — the per-statement
136
+ // degrade-to-rebuild escape: when a statement's distinct affected-key count makes k
137
+ // residual runs cost more than one rebuild (`shouldDegradeToRebuild`), the flush runs
138
+ // this and applies a single `'replace-all'` diff instead. Compiled here (not per arm
139
+ // builder) so the three residual arms share one compilation site; inverse-projection
140
+ // never degrades (always the cheapest arm) and the floor has its own bodyScheduler.
141
+ // NOTE: adds one whole-body optimize+emit per residual-MV (re)registration; if
142
+ // registration latency ever matters (e.g. bulk catalog import), compile this lazily
143
+ // on the first degrade instead.
144
+ if (boundedDelta && boundedDelta.kind !== 'inverse-projection' && boundedDelta.kind !== 'full-rebuild') {
145
+ boundedDelta.fullRebuildScheduler = compileWholeBodyScheduler(ctx, analyzed);
146
+ }
147
+ const plan = boundedDelta ?? buildFullRebuildPlan(ctx, mv, analyzed);
148
+ // Precompute the NOT-NULL ordering-seeded PK skew guard once, here at plan build —
149
+ // which `alter … drop not null` re-runs (it recompiles the row-time plan live), so the
150
+ // guarded set is recomputed at exactly the moment the skew can appear. Gated to
151
+ // non-empty so the field stays `undefined` (zero per-write cost) for the common case.
152
+ const guarded = computeNullGuardColumns(mv, analyzed);
153
+ plan.nullGuardColumns = guarded.length > 0 ? guarded : undefined;
154
+ return plan;
155
+ }
156
+ /**
157
+ * Backing columns that would silently store a NULL into a declared-NOT-NULL physical-PK
158
+ * column if the source loosened — the reachable "NOT-NULL ordering-seeded PK over a
159
+ * loosened source" skew, precomputed for the row-time guard
160
+ * ({@link MaterializedViewManager.maintainRowTime}). A backing column `i` is guarded when
161
+ * it is (1) declared NOT NULL, (2) a physical-PK member, AND (3) its re-derived body output
162
+ * column `i` is now nullable. Backing column `i` corresponds 1:1 to body output column `i`
163
+ * (`deriveBackingShape` builds the backing column list positionally from the body's output),
164
+ * and `deriveBackingShape`'s own `notNull = type.nullable === false` derivation means the
165
+ * body-nullability term agrees with the backing declaration by construction.
166
+ *
167
+ * The body-nullability term (3) is the real discriminator: the NOT-NULL/physical-PK set
168
+ * alone is non-empty for almost every MV (the logical-key PK column is normally the NOT-NULL
169
+ * source PK), so guarding on (1)+(2) only would scan essentially every maintained write.
170
+ * (3) is true only for the rare loosened-source skew, so the returned set — hence
171
+ * `plan.nullGuardColumns` — is empty (the field stays `undefined`) for the common case. A
172
+ * physical-PK column declared *nullable* (a nullable-source ordering column) fails (1) and is
173
+ * never guarded — it self-consistently stores NULL and must keep working.
174
+ */
175
+ function computeNullGuardColumns(mv, analyzed) {
176
+ const root = rootRelationalNode(analyzed);
177
+ if (!root)
178
+ return [];
179
+ const bodyColumns = root.getType().columns;
180
+ const pkIndices = new Set(mv.primaryKeyDefinition.map(d => d.index));
181
+ const guarded = [];
182
+ for (let i = 0; i < mv.columns.length; i++) {
183
+ const col = mv.columns[i];
184
+ if (col.notNull !== true)
185
+ continue; // nullable-declared PK column stays permitted
186
+ if (!pkIndices.has(i))
187
+ continue; // physical-PK member only
188
+ const bodyCol = bodyColumns[i];
189
+ if (bodyCol && bodyCol.type.nullable !== false) { // body now yields nullable
190
+ guarded.push({ index: i, name: col.name });
191
+ }
192
+ }
193
+ return guarded;
194
+ }
195
+ /**
196
+ * Route the analyzed body to the matching bounded-delta arm, or return `null` when its
197
+ * shape fits **no** bounded-delta arm (the caller then builds the full-rebuild floor).
198
+ * Each arm builder likewise returns `null` on a sub-shape mismatch and falls through
199
+ * here. The arms keep only **determinism** as a hard reject (so their arm-specific
200
+ * determinism diagnostic survives — see the individual builders); every other mismatch
201
+ * is a `null` fall-through. Bag / no-output / size rejects live in the floor.
202
+ */
203
+ export function tryBuildBoundedDeltaArm(ctx, mv, analyzed) {
204
+ // A body that reads no source table has no bounded-delta arm → floor (which rejects
205
+ // a sourceless body). (A self-join / TVF fan-out surfaces ≥2 refs or a TVF node.)
206
+ const tableRefs = [...collectTableRefs(analyzed).values()];
207
+ if (tableRefs.length === 0)
208
+ return null;
209
+ // Shapes no bounded-delta arm models — a window function reads across the partition,
210
+ // set ops / recursive CTEs / DISTINCT / row caps are out of the bounded-delta model.
211
+ // They are NOT rejected: a deterministic, keyed such body is maintained by the floor.
212
+ if (containsNodeType(analyzed, PlanNodeType.Window))
213
+ return null;
214
+ if (containsNodeType(analyzed, PlanNodeType.Distinct))
215
+ return null;
216
+ if (containsNodeType(analyzed, PlanNodeType.SetOperation))
217
+ return null;
218
+ if (containsNodeType(analyzed, PlanNodeType.RecursiveCTE))
219
+ return null;
220
+ if (mv.derivation.selectAst.type === 'select' && (mv.derivation.selectAst.limit !== undefined || mv.derivation.selectAst.offset !== undefined)) {
221
+ return null;
222
+ }
223
+ const tableRef = tableRefs[0];
224
+ const sourceSchema = tableRef.tableSchema;
225
+ const sourceBase = `${sourceSchema.schemaName}.${sourceSchema.name}`.toLowerCase();
226
+ // Single base source `T` joined to ONE lateral table-valued function — a fan-out
227
+ // body (each base row produces N backing rows) → the prefix-delete arm. Routed
228
+ // *before* the generic join branch below, because a lateral fan-out surfaces BOTH a
229
+ // Join and a TableFunctionCall. A multi-base TVF body falls to the floor.
230
+ if (containsNodeType(analyzed, PlanNodeType.TableFunctionCall)) {
231
+ if (tableRefs.length !== 1)
232
+ return null;
233
+ return buildLateralTvfPrefixDeletePlan(ctx, mv, analyzed, tableRef, sourceBase);
234
+ }
235
+ // Any join → the provably-1:1 join-residual arm. A fanning (non-1:1) join, an outer
236
+ // join, a >2-source join, an aggregate over a join, or a partial WHERE returns `null`
237
+ // from the builder → floor. (The lateral-TVF fan-out above is matched first because
238
+ // it also surfaces a join node.)
239
+ if (containsAnyJoin(analyzed)) {
240
+ return buildJoinResidualPlan(ctx, mv, analyzed, tableRefs);
241
+ }
242
+ // A non-join multi-source body (e.g. a WHERE-subquery over a second table) has no
243
+ // bounded-delta arm → floor.
244
+ if (tableRefs.length > 1)
245
+ return null;
246
+ // Single-source aggregate (`group by` over bare columns) → residual-recompute arm.
247
+ // Each changed source row belongs to exactly one group; maintenance recomputes that
248
+ // group's backing row from live state. A scalar aggregate (no GROUP BY) falls to the
249
+ // floor.
250
+ const aggregate = findAggregate(analyzed);
251
+ if (aggregate) {
252
+ return buildAggregateResidualPlan(ctx, mv, analyzed, tableRef, sourceBase, aggregate);
253
+ }
254
+ // The covering-index shape → inverse-projection arm (the default single-source arm).
255
+ return buildInverseProjectionPlan(ctx, mv, analyzed, tableRef, sourceBase);
256
+ }
257
+ /**
258
+ * Build an `'inverse-projection'` plan for the covering-index shape: a single
259
+ * row-preserving source `T` with a primary key, a linear
260
+ * `TableReference → optional Filter → Project → optional Sort` body, a projection that
261
+ * resolves every source PK column (and every backing-key column) to a **passthrough**
262
+ * source column — non-key columns may instead be a **deterministic scalar expression**
263
+ * over the source row — and a partial WHERE evaluable on a single source row. Returns
264
+ * `null` on any **shape** mismatch (the caller falls through to the full-rebuild floor);
265
+ * a **non-deterministic** computed column is the one hard reject (its arm-specific
266
+ * determinism diagnostic must survive rather than fall through to the floor's generic one).
267
+ */
268
+ export function buildInverseProjectionPlan(ctx, mv, analyzed, tableRef, sourceBase) {
269
+ const db = ctx;
270
+ const sourceSchema = tableRef.tableSchema;
271
+ const sourcePkCols = sourceSchema.primaryKeyDefinition.map(d => d.index);
272
+ if (sourcePkCols.length === 0)
273
+ return null; // source has no PK → floor
274
+ const backing = ctx._findTable(mv.name, mv.schemaName);
275
+ if (!backing) {
276
+ throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
277
+ }
278
+ // Projection classification: each backing output column is either a passthrough
279
+ // source column (a pure permutation entry) or a deterministic scalar expression
280
+ // over the single source row. A passthrough makes maintenance a column copy; an
281
+ // expression column evaluates `project(sourceRow)` via the runtime (still a pure
282
+ // per-row function — O(log n), no body re-execution). PK / backing-key columns
283
+ // must stay passthrough (the backing key and the inverse-projection conflict map
284
+ // depend on it); non-key columns may be computed.
285
+ //
286
+ // "Passthrough" is value-preserving lineage (`resolveValuePreservingSourceCol`):
287
+ // a bare column reference, OR one wrapped in `collate` / a no-op `cast` — those
288
+ // wrappers copy the source VALUE verbatim, so the column-copy maintenance is
289
+ // exact. This is what lets the collation-weakening migration shape (`select b
290
+ // collate nocase as b from t`) register here with its coarsened backing key:
291
+ // the per-row upsert is keyed under the backing PK's (output) collation, so a
292
+ // colliding source row last-write-wins into the shared backing row, and a
293
+ // delete of one colliding sibling removes the shared row (the documented
294
+ // anomaly — docs/materialized-views.md § Coarsened backing keys).
295
+ const sourceAttrToCol = new Map();
296
+ const sourceDescriptor = [];
297
+ tableRef.getAttributes().forEach((a, i) => {
298
+ sourceAttrToCol.set(a.id, i);
299
+ sourceDescriptor[a.id] = i;
300
+ });
301
+ const producingByAttrId = collectProducingExprs(analyzed);
302
+ const rootAttrs = relationalAttributes(analyzed);
303
+ if (!rootAttrs)
304
+ return null; // no relational output → floor (which hard-rejects it)
305
+ const projectors = [];
306
+ for (let outCol = 0; outCol < rootAttrs.length; outCol++) {
307
+ const attr = rootAttrs[outCol];
308
+ const sourceCol = attr ? resolveValuePreservingSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
309
+ if (sourceCol !== undefined) {
310
+ projectors.push({ kind: 'passthrough', sourceCol });
311
+ continue;
312
+ }
313
+ // Computed column: a deterministic scalar over the source row. A
314
+ // non-deterministic producer is a HARD reject (the arm-specific determinism
315
+ // diagnostic must survive — so `random()` fails on *determinism*, not by silently
316
+ // falling to a still-rejected floor); a deterministic-but-unsupported *shape*
317
+ // (no resolvable producer, a subquery / cross-row reference, an async form)
318
+ // returns `null` → the floor.
319
+ const colName = attr?.name ?? `#${outCol}`;
320
+ const producing = attr ? producingByAttrId.get(attr.id) : undefined;
321
+ if (!producing)
322
+ return null;
323
+ const det = checkDeterministic(producing);
324
+ if (!det.valid) {
325
+ throw cannotMaterialize(mv.name, `it projects a non-deterministic expression column '${colName}' (${det.expression}); `
326
+ + `a row-time backing value must be reproducible from the source row`);
327
+ }
328
+ if (!isSingleRowEvaluable(producing, sourceDescriptor))
329
+ return null;
330
+ let evalFn;
331
+ try {
332
+ evalFn = compileSourceRowEvaluator(db, producing, sourceDescriptor);
333
+ }
334
+ catch {
335
+ return null; // not row-time maintainable as a single-row scalar → floor
336
+ }
337
+ projectors.push({ kind: 'expr', eval: evalFn });
338
+ }
339
+ // Every source PK column must be projected as a passthrough column so the backing
340
+ // key is a deterministic identity of the source row that `lookupCoveringConflicts`
341
+ // can invert. A PK column produced only via an expression (or not at all) breaks
342
+ // that recovery.
343
+ const passthroughSourceCols = new Set(projectors.flatMap(p => p.kind === 'passthrough' ? [p.sourceCol] : []));
344
+ for (const pk of sourcePkCols) {
345
+ if (!passthroughSourceCols.has(pk))
346
+ return null; // PK not passthrough-projected → floor
347
+ }
348
+ const backingPkDefinition = resolveBackingPkColumns(backing, ctx.getCollationResolver());
349
+ // A computed column may never land in the backing primary key: the btree keys on
350
+ // it and `lookupCoveringConflicts` recovers the source PK from it, both of which
351
+ // require a passthrough source-column identity.
352
+ for (const d of backingPkDefinition) {
353
+ if (projectors[d.index]?.kind !== 'passthrough')
354
+ return null; // computed backing-key col → floor
355
+ }
356
+ // Partial WHERE must be evaluable on a single source row (no subqueries /
357
+ // cross-row references). `compilePredicate` throws on unsupported forms; an
358
+ // unsupported WHERE shape falls to the floor.
359
+ //
360
+ // No owning-table name is passed (lenient qualifier handling), deliberately: the
361
+ // view body may reference the source through a FROM alias, not the source table's
362
+ // own name, so validating against `sourceSchema.name` would wrongly reject a legal
363
+ // aliased body — and this site FLOORS to full-rebuild on any throw rather than
364
+ // erroring, turning a false rejection into a silent maintenance regression.
365
+ // NOTE: to make MV WHERE strict, thread the alias set from the view's FROM into
366
+ // this builder (not currently available here) and validate against it.
367
+ let predicate;
368
+ const bodyWhere = mv.derivation.selectAst.type === 'select' ? mv.derivation.selectAst.where : undefined;
369
+ if (bodyWhere) {
370
+ try {
371
+ predicate = compilePredicate(bodyWhere, sourceSchema.columns);
372
+ }
373
+ catch {
374
+ return null; // WHERE not evaluable on a single source row → floor
375
+ }
376
+ }
377
+ // ── Cost gate (incremental-maintenance-cost-gate) ──
378
+ // The covering-index shape's only structurally-sound maintenance strategy is
379
+ // 'inverse-projection' (O(1) per changed row); 'full-rebuild' is the floor for bodies
380
+ // this arm did NOT match (reached via the `null` fall-through above), so it is not a
381
+ // competitor here. Eligibility is thus a cost choice among the sound strategies (argmin
382
+ // maintenanceCost); for this shape it resolves to inverse-projection while recording the
383
+ // choice + the cost inputs the runtime reuses.
384
+ const soundStrategies = ['inverse-projection'];
385
+ const sourceStats = estimateMaintenanceStats(ctx, sourceSchema, projectors.length, predicate !== undefined);
386
+ // Create-time change-cardinality estimate: ~1% of the source per statement (typical OLTP).
387
+ const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
388
+ const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
389
+ // Defensive: this arm's sound set is exactly ['inverse-projection']. A different choice
390
+ // would mean the set grew without the corresponding apply-arm — fail loud rather than
391
+ // register an unexecutable plan.
392
+ if (chosenStrategy !== 'inverse-projection') {
393
+ throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
394
+ }
395
+ return {
396
+ kind: 'inverse-projection',
397
+ mv,
398
+ sourceBase,
399
+ backingSchema: mv.schemaName,
400
+ backingTableName: mv.name,
401
+ chosenStrategy,
402
+ sourceStats,
403
+ backingPkDefinition,
404
+ projectors,
405
+ predicate,
406
+ };
407
+ }
408
+ /**
409
+ * Build a `'residual-recompute'` plan for a single-source aggregate body
410
+ * (`select g1,…, agg(…) from T [where P] group by g1,…` over **bare** group columns),
411
+ * or return `null` on a shape mismatch (the caller falls through to the full-rebuild
412
+ * floor). Each changed source row belongs to exactly one group `(g1,…)`; maintaining the
413
+ * MV means recomputing that group's backing row from live state — delete the old slice,
414
+ * run the group-keyed residual, upsert the recomputed slice (zero rows when the group
415
+ * emptied). See {@link ResidualRecomputePlan} and `docs/incremental-maintenance.md`
416
+ * § residual-recompute.
417
+ *
418
+ * A **non-deterministic** group/aggregate expression is the one hard reject (the
419
+ * arm-specific determinism diagnostic must survive); every other mismatch — a scalar
420
+ * aggregate, a computed group key, a backing key that is not the group key — returns
421
+ * `null` → the floor.
422
+ *
423
+ * NOTE: the group binding is derived **directly** from the aggregate node's bare GROUP
424
+ * BY columns, not via `extractBindings`. `analyzeRowSpecific`'s `'group'` classification
425
+ * additionally requires the group key to cover a *source* unique key (so it reports
426
+ * `'global'` for the common `group by <non-key>` body), which is the wrong test here —
427
+ * the backing is keyed by the group key regardless of whether it is a source key.
428
+ */
429
+ export function buildAggregateResidualPlan(ctx, mv, analyzed, tableRef, sourceBase, aggregate) {
430
+ // A scalar aggregate (no GROUP BY) is one global row keyed by the empty key — no
431
+ // bounded-delta group binding, so it falls to the floor.
432
+ if (aggregate.groupBy.length === 0)
433
+ return null;
434
+ // Map T's output attributes to source column indices. T is a bare
435
+ // `TableReferenceNode`, so output-column index == source-column index.
436
+ const sourceAttrToCol = new Map();
437
+ tableRef.getAttributes().forEach((a, i) => sourceAttrToCol.set(a.id, i));
438
+ const producingByAttrId = collectProducingExprs(analyzed);
439
+ // Transitive provenance: chase output-attr → producing ColumnReference chains
440
+ // (Project-over-Aggregate adds a hop the single-hop `resolveSourceCol` cannot
441
+ // follow) until landing on a T source column, or `undefined`.
442
+ const resolveToSourceCol = (attrId) => resolveTransitiveSourceCol(attrId, sourceAttrToCol, producingByAttrId);
443
+ // Each GROUP BY expression must be a bare source column (a computed group key has
444
+ // no source-column index to bind / key the backing on) → otherwise the floor.
445
+ const groupColumns = [];
446
+ for (const expr of aggregate.groupBy) {
447
+ if (!(expr instanceof ColumnReferenceNode))
448
+ return null;
449
+ const sourceCol = sourceAttrToCol.get(expr.attributeId);
450
+ if (sourceCol === undefined)
451
+ return null;
452
+ groupColumns.push(sourceCol);
453
+ }
454
+ // Determinism: a residual must reproduce exactly what `select <body>` returns, so a
455
+ // volatile group/aggregate expression (random()/now()/volatile UDF) is a HARD reject.
456
+ for (const expr of aggregate.groupBy) {
457
+ const det = checkDeterministic(expr);
458
+ if (!det.valid)
459
+ throw cannotMaterialize(mv.name, `it groups by a non-deterministic expression (${det.expression})`);
460
+ }
461
+ for (const agg of aggregate.aggregates) {
462
+ const det = checkDeterministic(agg.expression);
463
+ if (!det.valid)
464
+ throw cannotMaterialize(mv.name, `it aggregates a non-deterministic expression (${det.expression})`);
465
+ }
466
+ // Backing table + its physical PK. The aggregate's group-key FD
467
+ // (`propagateAggregateFds`) makes the group key the backing key (via `keysOf`).
468
+ const backing = ctx._findTable(mv.name, mv.schemaName);
469
+ if (!backing) {
470
+ throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
471
+ }
472
+ const backingPkDefinition = resolveBackingPkColumns(backing, ctx.getCollationResolver());
473
+ // Map each backing-PK column back to the source group column it projects, so a
474
+ // changed row's old backing-slice delete key can be built. Every backing-PK column
475
+ // MUST resolve to a GROUP BY source column — else the backing key is not the group
476
+ // key and point-keyed delete+upsert would be unsound → fall to the floor.
477
+ const rootAttrs = relationalAttributes(analyzed);
478
+ if (!rootAttrs)
479
+ return null;
480
+ const groupColumnSet = new Set(groupColumns);
481
+ const backingPkSourceCols = [];
482
+ for (const d of backingPkDefinition) {
483
+ const attr = rootAttrs[d.index];
484
+ const sourceCol = attr ? resolveToSourceCol(attr.id) : undefined;
485
+ if (sourceCol === undefined || !groupColumnSet.has(sourceCol))
486
+ return null;
487
+ backingPkSourceCols.push(sourceCol);
488
+ }
489
+ // Compile + cache the group-keyed residual once (the body with `g1 = :gk0 AND …`
490
+ // injected on T). Re-run per affected group key against the live transaction.
491
+ const relKey = `${sourceBase}#${tableRef.id ?? 'unknown'}`;
492
+ const residualScheduler = compileResidual(ctx, analyzed, relKey, groupColumns, 'gk');
493
+ if (!residualScheduler)
494
+ return null; // could not parameterize the residual → floor
495
+ // Delta-aggregate fast path: build the descriptor when EVERY stored aggregate
496
+ // column is delta-maintainable by its declared algebra (see the builder below).
497
+ // `undefined` on any gate failure — the arm then stays a plain residual, unchanged.
498
+ const delta = buildDeltaAggregateDescriptor(mv, analyzed, tableRef, groupColumns, backingPkDefinition, backingPkSourceCols, rootAttrs, sourceAttrToCol, producingByAttrId);
499
+ // ── Cost gate ──
500
+ // The residual is the structurally-sound incremental arm for an aggregate body;
501
+ // 'full-rebuild' is the always-correct floor for shapes where the residual is NOT
502
+ // sound, so (as with inverse-projection) it is not a competitor here. When the delta
503
+ // descriptor built, 'delta-aggregate' joins the sound set — an O(1)-per-change
504
+ // arithmetic RMW, strictly cheaper than the per-group residual under the cost model,
505
+ // so the argmin picks it. The descriptor is attached only when the gate chose it
506
+ // (cost-strategy decoupling: `kind` stays 'residual-recompute' either way).
507
+ const soundStrategies = delta
508
+ ? ['delta-aggregate', 'residual-recompute']
509
+ : ['residual-recompute'];
510
+ const hasPredicate = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
511
+ const sourceStats = estimateMaintenanceStats(ctx, tableRef.tableSchema, backing.columns.length, hasPredicate);
512
+ // A tighten-only (min/max) column re-derives a retracted group from the residual, so the
513
+ // delta arm's cost is no longer pure arithmetic — feed the create-time expected-rescan
514
+ // fraction into the `'delta-aggregate'` cost blend (maintenanceCost). A tighten body then
515
+ // costs strictly more than a pure-group body but, for this insert-dominated estimate, still
516
+ // below the always-residual arm, so the argmin keeps the delta arm.
517
+ if (delta?.hasTighten)
518
+ sourceStats.deltaTightenFallbackRatio = DELTA_TIGHTEN_FALLBACK_RATIO;
519
+ const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
520
+ const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
521
+ if (!soundStrategies.includes(chosenStrategy)) {
522
+ throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
523
+ }
524
+ return {
525
+ kind: 'residual-recompute',
526
+ mv,
527
+ sourceBase,
528
+ backingSchema: mv.schemaName,
529
+ backingTableName: mv.name,
530
+ chosenStrategy,
531
+ sourceStats,
532
+ binding: { kind: 'group', groupColumns: [...groupColumns] },
533
+ residualScheduler,
534
+ bindParamPrefix: 'gk',
535
+ bindColumns: groupColumns,
536
+ backingPkDefinition,
537
+ backingPkSourceCols,
538
+ delta: chosenStrategy === 'delta-aggregate' ? delta : undefined,
539
+ };
540
+ }
541
+ /**
542
+ * Build the {@link DeltaAggregateDescriptor} for a single-source aggregate body, or
543
+ * return `undefined` when any column fails the delta eligibility gate (the arm then
544
+ * stays a plain residual — a gate failure is never an error). Function-generic by
545
+ * construction: every judgment reads the aggregate call node's registry-resolved
546
+ * schema and its DECLARED {@link AggregateAlgebra} — never an aggregate-name list.
547
+ *
548
+ * The gate, per stored backing column:
549
+ * - a **group-key passthrough** column must resolve (transitively) to a GROUP BY
550
+ * source column that also backs a backing-PK position (so its flush value is the
551
+ * group key itself);
552
+ * - a directly **delta-maintainable aggregate** column must be a plain call (`agg(x)` /
553
+ * `agg()` — no DISTINCT, FILTER, ORDER BY, multi-arg) whose declared algebra has
554
+ * `merge` + `negate` + `decode`, with a **bare source column** argument (the delta
555
+ * steps the raw value);
556
+ * - **exact value domain**: the aggregate's declared return type is INTEGER-physical
557
+ * (count-shaped — exact regardless of argument), OR its argument column's static
558
+ * type is INTEGER-physical (integer sum). A REAL/NUMERIC/TEXT sum drifts under
559
+ * repeated add/subtract and would diverge byte-exactly from the live re-evaluation
560
+ * the oracle compares against — such a column disqualifies the whole MV (residual);
561
+ * - a **tighten-only** column (min/max, and any UDAF whose merge is a join-semilattice
562
+ * like `bit_or`/`bool_or`) — a plain call declaring `merge` + `decode` but NO `negate`
563
+ * — is admitted with `deltaClass: 'tighten'`. It needs no exact-value-domain gate
564
+ * (merge is idempotent selection, not accumulating arithmetic → no drift) and is never
565
+ * retraction-safe: an insert `merge`s the new value in cheaply, but a retracted group
566
+ * re-derives wholesale from the residual (via the descriptor's `hasTighten`) rather
567
+ * than trusting the arithmetic;
568
+ * - a **decomposition-maintained** column (the avg class) — a plain call whose algebra
569
+ * declares `decompose` (a scalar formula over sibling partials, e.g. avg ≡ sum/count)
570
+ * but is not itself directly maintainable — is admitted only when EVERY partial it
571
+ * names ({@link resolveDecomposePartial}) is also stored as a sibling column of this
572
+ * same body and is itself delta-maintainable. It carries no independent accumulation;
573
+ * at flush its value is `combine([finalized(partial) …])`. If any partial is missing
574
+ * or not delta-maintainable the whole MV falls to residual (avg is the first client of
575
+ * this class, not a special case);
576
+ * - any other column shape (a constant, an expression over the group key, …) fails.
577
+ *
578
+ * Whole-body requirements:
579
+ * - **no post-aggregate filter** (HAVING, or an outer WHERE over the aggregate
580
+ * output): such a filter breaks the arithmetic invariant "stored row == the
581
+ * group's full accumulator" — a filtered-out group has contributions but no
582
+ * stored row, so a later delta would rebuild it from the identity and understate
583
+ * the aggregate. The residual re-evaluates the filter per group and stays exact;
584
+ * - a **count(*) multiplicity witness** — a zero-arg delta-maintainable column with
585
+ * `decodeExact` (its stored value must reconstruct exactly, since it decides group
586
+ * emptiness under retraction). Without it, emptiness cannot be told from an
587
+ * all-NULL sum;
588
+ * - **BINARY collations** on every backing-PK (group key) column: the flush's stored-
589
+ * row read is the host's `scanEffective` equality-prefix seek, whose prefix compare
590
+ * is binary (scan-layer.ts). Unlike the prefix-delete arm there is no source-PK
591
+ * uniqueness collapsing collation classes to one byte form here, so a non-BINARY
592
+ * group key could miss a collation-equal / byte-different stored row — residual
593
+ * instead;
594
+ * - a body WHERE (if any) must compile to a single-source-row predicate
595
+ * (`compilePredicate`) so per-row contributions can be scoped exactly.
596
+ *
597
+ * Per-column `retractionSafe` (see {@link DeltaAggregateColumn}) is recorded rather
598
+ * than gated: a not-retraction-safe descriptor still serves the bulk-insert fast path,
599
+ * and the flush re-derives only the retracted groups through the residual.
600
+ */
601
+ export function buildDeltaAggregateDescriptor(mv, analyzed, tableRef, groupColumns, backingPkDefinition, backingPkSourceCols, rootAttrs, sourceAttrToCol, producingByAttrId) {
602
+ const sourceSchema = tableRef.tableSchema;
603
+ const groupColumnSet = new Set(groupColumns);
604
+ // Post-aggregate filter gate: a Filter whose subtree contains the aggregate is a
605
+ // HAVING (or an outer WHERE over the aggregate output). It breaks the arithmetic
606
+ // invariant "stored row == the group's full accumulator" — a filtered-out group has
607
+ // contributions but no stored row, so a later delta would rebuild it from the
608
+ // identity and understate the aggregate. (The body WHERE sits BELOW the aggregate —
609
+ // its Filter subtree contains no aggregate node — and stays delta-eligible via the
610
+ // compiled scope predicate below.)
611
+ if (hasFilterAboveAggregate(analyzed))
612
+ return undefined;
613
+ // Binary-collation gate on the backing PK (the flush's binary equality-prefix read).
614
+ for (const d of backingPkDefinition) {
615
+ if (!isBinaryCollation(d.collation))
616
+ return undefined;
617
+ }
618
+ // Body WHERE → single-row scope predicate (contributions outside it are dropped,
619
+ // mirroring the residual's own WHERE). An uncompilable WHERE shape → residual.
620
+ let predicate;
621
+ const bodyWhere = mv.derivation.selectAst.type === 'select' ? mv.derivation.selectAst.where : undefined;
622
+ if (bodyWhere) {
623
+ try {
624
+ predicate = compilePredicate(bodyWhere, sourceSchema.columns);
625
+ }
626
+ catch {
627
+ return undefined;
628
+ }
629
+ }
630
+ // Chase an output attr's producing chain to its terminal (non-ColumnReference)
631
+ // expression — the aggregate call node for an aggregate output column.
632
+ const resolveTerminalProducing = (attrId) => {
633
+ const seen = new Set();
634
+ let cur = attrId;
635
+ while (!seen.has(cur)) {
636
+ seen.add(cur);
637
+ const expr = producingByAttrId.get(cur);
638
+ if (expr instanceof ColumnReferenceNode) {
639
+ cur = expr.attributeId;
640
+ continue;
641
+ }
642
+ return expr;
643
+ }
644
+ return undefined;
645
+ };
646
+ const aggColumns = [];
647
+ const groupOut = [];
648
+ // Decomposition-maintained columns (the avg class), collected raw here and bound to
649
+ // their sibling stored partials in a second pass below — the partials may project
650
+ // before OR after the decompose column, so they cannot be resolved inline.
651
+ const pendingDecompose = [];
652
+ for (let outCol = 0; outCol < rootAttrs.length; outCol++) {
653
+ const attr = rootAttrs[outCol];
654
+ if (!attr)
655
+ return undefined;
656
+ const sourceCol = resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId);
657
+ if (sourceCol !== undefined) {
658
+ // Group-key passthrough: its flush value is the group key at the matching
659
+ // backing-PK position (covers duplicate projections of a group column).
660
+ if (!groupColumnSet.has(sourceCol))
661
+ return undefined;
662
+ const pkPos = backingPkSourceCols.indexOf(sourceCol);
663
+ if (pkPos < 0)
664
+ return undefined;
665
+ groupOut.push({ backingCol: outCol, pkPos });
666
+ continue;
667
+ }
668
+ const producing = resolveTerminalProducing(attr.id);
669
+ if (!(producing instanceof AggregateFunctionCallNode))
670
+ return undefined;
671
+ // Plain call only: DISTINCT folds duplicates (delta cannot), FILTER/ORDER BY
672
+ // change the contribution set, multi-arg has no single raw value to step.
673
+ if (producing.isDistinct || producing.filter || (producing.orderBy && producing.orderBy.length > 0))
674
+ return undefined;
675
+ if (producing.args.length > 1)
676
+ return undefined;
677
+ const schema = producing.functionSchema;
678
+ if (!isAggregateFunctionSchema(schema))
679
+ return undefined;
680
+ const algebra = schema.algebra;
681
+ // A bare source column argument (both classes below step / decompose over the raw
682
+ // value); a zero-arg (count(*)-shaped) call carries none.
683
+ let argSourceCol;
684
+ if (producing.args.length === 1) {
685
+ const arg = producing.args[0];
686
+ if (!(arg instanceof ColumnReferenceNode))
687
+ return undefined; // bare column args only
688
+ argSourceCol = resolveTransitiveSourceCol(arg.attributeId, sourceAttrToCol, producingByAttrId);
689
+ if (argSourceCol === undefined)
690
+ return undefined;
691
+ }
692
+ // Class routing — declaration-driven, never an aggregate-name list:
693
+ // - **directly delta-maintainable** (`merge` + `negate` (retraction) + `decode` —
694
+ // the backing stores finalized values, not accumulators): accumulate it as an
695
+ // ordinary partial;
696
+ // - else a **decomposition-maintained** column whose value is a formula over
697
+ // sibling partials (`algebra.decompose`, e.g. avg ≡ sum/count) — deferred to the
698
+ // pass below that binds each partial to a stored sibling; it accumulates nothing;
699
+ // - anything else disqualifies the whole MV → residual.
700
+ if (algebra?.negate && algebra.decode) {
701
+ // Exact numeric domain: an INTEGER-physical declared result (count-shaped — the
702
+ // accumulator never holds a float) or an INTEGER-physical argument column
703
+ // (integer sum). Anything else may drift under repeated add/subtract.
704
+ // NOTE: if a REAL-domain sum ever needs the fast path, it needs compensated
705
+ // (Kahan) accumulation or a periodic-rescan discipline — a design change, not a
706
+ // relaxation of this gate.
707
+ const resultExact = schema.returnType.logicalType.physicalType === PhysicalType.INTEGER;
708
+ const argExact = argSourceCol !== undefined
709
+ && sourceSchema.columns[argSourceCol]?.logicalType.physicalType === PhysicalType.INTEGER;
710
+ if (!resultExact && !argExact)
711
+ return undefined;
712
+ const retractionSafe = algebra.decodeExact === true
713
+ || (argSourceCol !== undefined && sourceSchema.columns[argSourceCol]?.notNull === true);
714
+ aggColumns.push({
715
+ backingCol: outCol,
716
+ schema,
717
+ algebra,
718
+ deltaClass: 'group',
719
+ argSourceCol,
720
+ isMultiplicity: producing.args.length === 0,
721
+ retractionSafe,
722
+ });
723
+ continue;
724
+ }
725
+ // Tighten-only (join-semilattice): `merge` + `decode` but NO `negate` — min/max, and
726
+ // any UDAF whose merge is idempotent selection (bit_or/bool_or). An insert `merge`s the
727
+ // new value in cheaply (min/max tighten toward the new extreme); a retraction cannot be
728
+ // undone arithmetically, so a retracted group falls back to the residual
729
+ // (retractionSafe:false, and the descriptor's hasTighten routes the whole group there
730
+ // whether or not a row is stored). NO exact-numeric-domain gate: merge is idempotent
731
+ // selection, not accumulating arithmetic, so there is no float drift to guard — a tighten
732
+ // column over any comparable domain (REAL/TEXT included) stays byte-exact vs the live
733
+ // re-evaluation. A negate+decode column that failed the exact-domain gate above already
734
+ // returned undefined, so only a decode-without-negate aggregate reaches here.
735
+ if (algebra?.decode && !algebra.negate) {
736
+ aggColumns.push({
737
+ backingCol: outCol,
738
+ schema,
739
+ algebra,
740
+ deltaClass: 'tighten',
741
+ argSourceCol,
742
+ isMultiplicity: producing.args.length === 0,
743
+ retractionSafe: false,
744
+ });
745
+ continue;
746
+ }
747
+ if (algebra?.decompose) {
748
+ pendingDecompose.push({ backingCol: outCol, argSourceCol, decomposition: algebra.decompose });
749
+ continue;
750
+ }
751
+ return undefined; // neither directly maintainable, tighten-only, nor decomposable → residual
752
+ }
753
+ // Multiplicity witness: a zero-arg delta-maintainable column whose decode is exact
754
+ // (its merged value decides group emptiness, so the reconstruction must survive
755
+ // retraction). Structural — not name-based. A decompose column is never the witness
756
+ // (it accumulates nothing — the requirement is a real stored count(*)).
757
+ const multiplicityIndex = aggColumns.findIndex(c => c.isMultiplicity && c.algebra.decodeExact === true);
758
+ if (multiplicityIndex < 0)
759
+ return undefined;
760
+ if (aggColumns.length === 0)
761
+ return undefined; // defensive — findAggregate implies ≥1
762
+ // Bind each decomposition column's partials to sibling STORED, delta-maintained columns
763
+ // (every `aggColumns` entry already passed the accumulator gate, so it is itself delta-
764
+ // maintainable). A partial that maps to no such sibling disqualifies the whole MV →
765
+ // residual (honest and visible: the user wrote a body without the partials the formula
766
+ // names). The decompose column's `combine` recomputes exactly per group at flush.
767
+ const decomposeColumns = [];
768
+ for (const pd of pendingDecompose) {
769
+ const partialIndices = [];
770
+ for (const p of pd.decomposition.partials) {
771
+ const idx = resolveDecomposePartial(p, pd.argSourceCol, aggColumns, sourceSchema);
772
+ if (idx === undefined)
773
+ return undefined;
774
+ partialIndices.push(idx);
775
+ }
776
+ decomposeColumns.push({ backingCol: pd.backingCol, partialIndices, combine: pd.decomposition.combine });
777
+ }
778
+ return {
779
+ aggColumns,
780
+ decomposeColumns,
781
+ groupColumns: groupOut,
782
+ multiplicityIndex,
783
+ backingColumnCount: rootAttrs.length,
784
+ retractionSafe: aggColumns.every(c => c.retractionSafe),
785
+ hasTighten: aggColumns.some(c => c.deltaClass === 'tighten'),
786
+ predicate,
787
+ };
788
+ }
789
+ /**
790
+ * Resolve one {@link AggregateDecomposition} partial (e.g. avg's `sum` / `count`) to the
791
+ * index of a sibling stored, delta-maintainable aggregate column — an `aggColumns` entry
792
+ * matching the partial's function name and argument. Returns `undefined` when no such
793
+ * sibling is stored (the decompose column is then not maintainable → the MV falls to the
794
+ * residual). The write-side twin of `query-rewrite-matcher.resolveMergeablePartial`.
795
+ *
796
+ * `'same-arg'` targets a sibling over the decompose column's own argument (`f(thisArg)`);
797
+ * `'star'` targets a zero-arg (`count(*)`-shaped) sibling. The **count(*) fallback** mirrors
798
+ * the read-side relaxation from `feat-mv-agg-rollup-retarget`: a stored `count(*)` substitutes
799
+ * for a `count(x)` divisor only when `x` is declared **NOT NULL** — then `count(*)` excludes
800
+ * the same (zero) NULLs `count(x)` would, so the recombine stays exact.
801
+ */
802
+ function resolveDecomposePartial(p, ownerArgSourceCol, aggColumns, sourceSchema) {
803
+ // NOTE: a partial resolves only to an `aggColumns` entry, which already passed the
804
+ // accumulator gate — a BARE source column argument plus the INTEGER-exact-domain check.
805
+ // That is what keeps the decomposed value byte-exact: a partial like `sum(log x)` (a
806
+ // non-bare, float-producing argument) can never be a stored delta-maintainable sibling,
807
+ // so a true geometric mean is NOT delta-decomposable and correctly falls to the residual.
808
+ const targetArg = p.arg === 'star' ? undefined : ownerArgSourceCol;
809
+ const funcName = p.func.toLowerCase();
810
+ const find = (name, argCol) => aggColumns.findIndex(c => c.schema.name.toLowerCase() === name && c.argSourceCol === argCol);
811
+ let idx = find(funcName, targetArg);
812
+ if (idx < 0 && funcName === 'count' && targetArg !== undefined
813
+ && sourceSchema.columns[targetArg]?.notNull === true) {
814
+ idx = find('count', undefined); // count(*) qualifies as the count(x) divisor when x is NOT NULL
815
+ }
816
+ return idx < 0 ? undefined : idx;
817
+ }
818
+ /**
819
+ * True when any Filter node's subtree contains an aggregate node — i.e. the plan
820
+ * filters the AGGREGATE OUTPUT (HAVING, or an outer WHERE over a grouped subquery).
821
+ * The body WHERE is the complement case: its Filter sits below the aggregate, so its
822
+ * subtree contains none. Used by {@link buildDeltaAggregateDescriptor}'s gate.
823
+ */
824
+ function hasFilterAboveAggregate(node) {
825
+ if (node.nodeType === PlanNodeType.Filter && findAggregate(node) !== undefined)
826
+ return true;
827
+ for (const child of node.getChildren()) {
828
+ if (hasFilterAboveAggregate(child))
829
+ return true;
830
+ }
831
+ return false;
832
+ }
833
+ /**
834
+ * Build a `'join-residual'` plan for a provably-1:1 row-preserving **inner/cross join**
835
+ * body (`select … from T join P on T.fk = P.id`), or return `null` on a shape mismatch
836
+ * (the caller falls through to the full-rebuild floor). The driving table `T` is the one
837
+ * whose PK the optimizer surfaced as the backing key (the 1:1 join collapses the composite
838
+ * product key to `T`'s PK); the other base ref is the lookup `P`. See {@link JoinResidualPlan}
839
+ * and `docs/incremental-maintenance.md` § join-residual.
840
+ *
841
+ * Soundness gates (a mismatch on any returns `null` → floor): exactly two base tables; no
842
+ * aggregate over the join; the backing PK is exactly `T`'s PK projected as passthrough
843
+ * columns (so each changed `T` row maps to one backing row and the reverse residual's rows
844
+ * carry the backing key); the join is provably 1:1 on `T` ({@link proveOneToOneJoin} — no
845
+ * row loss via NOT-NULL FK→PK RI, no fan-out via the join-frame `isUnique(T.pk)`); and the
846
+ * join is **inner/cross** (an outer join would make the lookup-side reverse residual unsound
847
+ * — filtering `P` drops the null-extended rows). A **non-deterministic** projection is the
848
+ * one hard reject (its arm-specific determinism diagnostic must survive).
849
+ *
850
+ * **A body WHERE is now accepted** (it is no longer a blanket reject): the predicate is
851
+ * classified by which base table(s) its columns reference (reusing the per-base-ref
852
+ * attribute→source-column maps below). A `T`-only predicate needs nothing extra — the
853
+ * forward residual already carries it and the membership set `{ T : T.fk = P.pk }` cannot
854
+ * move on a `P` write, so the lookup side stays upsert-only. A predicate referencing `P` (or
855
+ * both sides) switches the lookup side to a **delete-capable** reverse residual by building
856
+ * `lookupMembershipResidualScheduler` (the body with the WHERE stripped, keyed on `P`). See
857
+ * {@link JoinResidualPlan}'s "WHERE handling" note and {@link applyLookupResidual}.
858
+ */
859
+ export function buildJoinResidualPlan(ctx, mv, analyzed, tableRefs) {
860
+ // A >2-source join or an aggregate over the join has no join-residual binding → floor.
861
+ // A body WHERE is no longer rejected here — it is classified (T-only vs P-referencing)
862
+ // below, after `T`/`P` are identified, and routed to the matching lookup-side strategy.
863
+ if (tableRefs.length !== 2)
864
+ return null;
865
+ if (findAggregate(analyzed))
866
+ return null;
867
+ const backing = ctx._findTable(mv.name, mv.schemaName);
868
+ if (!backing) {
869
+ throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
870
+ }
871
+ const backingPkDefinition = resolveBackingPkColumns(backing, ctx.getCollationResolver());
872
+ const rootAttrs = relationalAttributes(analyzed);
873
+ if (!rootAttrs)
874
+ return null;
875
+ const producingByAttrId = collectProducingExprs(analyzed);
876
+ // Per-base-ref attribute → source-column maps. `T` and `P` are bare
877
+ // TableReferenceNodes (output-col index == source-col index).
878
+ const refInfos = tableRefs.map(ref => {
879
+ const attrToCol = new Map();
880
+ ref.getAttributes().forEach((a, i) => attrToCol.set(a.id, i));
881
+ return { ref, attrToCol };
882
+ });
883
+ // Identify the driving table `T` as the one every backing-PK column resolves to, and
884
+ // map each backing-PK column to its `T` source column (the delete key the forward arm
885
+ // builds). A backing-PK column resolving to neither ref — or columns spanning both —
886
+ // means the backing is not keyed on a single source's PK (the join is not provably
887
+ // 1:1, or `keysOf` fell back to all-columns): reject.
888
+ let tIndex;
889
+ const backingPkSourceCols = [];
890
+ for (const d of backingPkDefinition) {
891
+ const attr = rootAttrs[d.index];
892
+ let resolvedRef;
893
+ let resolvedCol;
894
+ for (let i = 0; i < refInfos.length; i++) {
895
+ const sc = attr ? resolveTransitiveSourceCol(attr.id, refInfos[i].attrToCol, producingByAttrId) : undefined;
896
+ if (sc !== undefined) {
897
+ resolvedRef = i;
898
+ resolvedCol = sc;
899
+ break;
900
+ }
901
+ }
902
+ // A backing-PK column resolving to neither ref, or columns spanning both, means the
903
+ // backing is not keyed on a single source's PK (not provably 1:1) → fall to the floor.
904
+ if (resolvedRef === undefined)
905
+ return null;
906
+ if (tIndex === undefined)
907
+ tIndex = resolvedRef;
908
+ else if (tIndex !== resolvedRef)
909
+ return null;
910
+ backingPkSourceCols.push(resolvedCol);
911
+ }
912
+ if (tIndex === undefined)
913
+ return null;
914
+ const tRef = refInfos[tIndex].ref;
915
+ const pRef = refInfos[tIndex === 0 ? 1 : 0].ref;
916
+ const tSchema = tRef.tableSchema;
917
+ const pSchema = pRef.tableSchema;
918
+ const sourceBase = `${tSchema.schemaName}.${tSchema.name}`.toLowerCase();
919
+ const lookupBase = `${pSchema.schemaName}.${pSchema.name}`.toLowerCase();
920
+ if (sourceBase === lookupBase)
921
+ return null; // self-join → floor
922
+ // The backing key must be EXACTLY the driving source's PK (each `T` row → one backing
923
+ // row). `keysOf` surfaced `T.pk` for the 1:1 join; verify it resolved to a real PK key
924
+ // (not the all-columns fallback) by set-equality with `T`'s declared PK.
925
+ const tPkCols = tSchema.primaryKeyDefinition.map(d => d.index);
926
+ if (tPkCols.length === 0)
927
+ return null;
928
+ const backingPkSet = new Set(backingPkSourceCols);
929
+ if (backingPkSet.size !== tPkCols.length || !tPkCols.every(c => backingPkSet.has(c)))
930
+ return null;
931
+ // Prove the join is 1:1 on `T` (no row loss + no fan-out), reusing the coverage
932
+ // prover's shared join predicates over the analyzed body. A fanning / non-1:1 join
933
+ // falls to the floor.
934
+ const root = rootRelationalNode(analyzed);
935
+ if (!root)
936
+ return null;
937
+ const proof = proveOneToOneJoin(root, tSchema);
938
+ if (!proof.ok)
939
+ return null;
940
+ // Restrict to inner/cross: the lookup-side reverse residual filters `P`, which would
941
+ // drop an outer join's null-extended rows (unsound). An outer 1:1 join falls to the floor.
942
+ const topJoin = proof.topJoin;
943
+ const joinType = topJoin && CapabilityDetectors.isJoin(topJoin) ? topJoin.getJoinType() : undefined;
944
+ if (joinType !== 'inner' && joinType !== 'cross')
945
+ return null;
946
+ // Determinism: the residual must reproduce exactly what `select <body>` returns, so a
947
+ // volatile projection (random()/now()/volatile UDF) is a HARD reject.
948
+ for (const attr of rootAttrs) {
949
+ const producing = attr ? producingByAttrId.get(attr.id) : undefined;
950
+ if (!producing)
951
+ continue; // a bare passthrough column has no producing expr to check
952
+ const det = checkDeterministic(producing);
953
+ if (!det.valid) {
954
+ throw cannotMaterialize(mv.name, `it projects a non-deterministic expression (${det.expression}); a row-time backing value must be reproducible from the source rows`);
955
+ }
956
+ }
957
+ // Forward (`T`) residual: the body with `T.pk = :pk0 AND …` injected on `T`. Recomputes
958
+ // the one joined row for a changed `T` row (delegated to `applyForwardResidual`).
959
+ const tRelKey = `${sourceBase}#${tRef.id ?? 'unknown'}`;
960
+ const forwardResidual = compileResidual(ctx, analyzed, tRelKey, tPkCols, 'pk');
961
+ if (!forwardResidual)
962
+ return null;
963
+ // Reverse (`P`) **in-scope** residual: the body — WHERE retained — with `P.pk = :pk0 AND …`
964
+ // injected on `P`. Drives lookup-side maintenance — finds every currently in-scope joined
965
+ // row referencing a changed `P` row.
966
+ const pPkCols = pSchema.primaryKeyDefinition.map(d => d.index);
967
+ if (pPkCols.length === 0)
968
+ return null;
969
+ const pRelKey = `${lookupBase}#${pRef.id ?? 'unknown'}`;
970
+ const reverseResidual = compileResidual(ctx, analyzed, pRelKey, pPkCols, 'pk');
971
+ if (!reverseResidual)
972
+ return null;
973
+ // Classify the body WHERE by which base table(s) its columns reference: a predicate that
974
+ // references `P` (or both sides) can flip a row's WHERE membership on a `P` write, so the
975
+ // lookup side must become delete-capable; a `T`-only predicate cannot move membership, so
976
+ // the upsert-only reverse residual above stays sound. The membership residual is the body
977
+ // with the WHERE **stripped** and the key filter on `P` — it returns every currently
978
+ // referencing `T` row (regardless of scope) so its backing key can be deleted before the
979
+ // in-scope survivors are re-upserted. Absent for a no-WHERE / `T`-only-WHERE body.
980
+ const hasWhere = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
981
+ // A volatile WHERE would make every residual (which embeds it) irreproducible → fall to
982
+ // the floor's pragma-gated whole-body determinism reject, not an unsound bounded-delta arm.
983
+ if (hasWhere && bodyWhereIsNonDeterministic(analyzed))
984
+ return null;
985
+ const whereReferencesLookup = hasWhere
986
+ && bodyWhereReferencesLookup(analyzed, refInfos[tIndex].attrToCol, producingByAttrId);
987
+ let lookupMembershipResidual;
988
+ if (whereReferencesLookup) {
989
+ const membership = compileLookupMembershipResidual(ctx, mv, lookupBase, pPkCols);
990
+ if (!membership)
991
+ return null; // could not strip + re-key the membership residual → floor
992
+ lookupMembershipResidual = membership;
993
+ }
994
+ // ── Cost gate (parity with the other residual arms) ──
995
+ const soundStrategies = ['residual-recompute'];
996
+ const sourceStats = estimateMaintenanceStats(ctx, tSchema, backing.columns.length, hasWhere);
997
+ const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
998
+ const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
999
+ if (chosenStrategy !== 'residual-recompute') {
1000
+ throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
1001
+ }
1002
+ return {
1003
+ kind: 'join-residual',
1004
+ mv,
1005
+ sourceBase,
1006
+ backingSchema: mv.schemaName,
1007
+ backingTableName: mv.name,
1008
+ chosenStrategy,
1009
+ sourceStats,
1010
+ binding: { kind: 'row', keyColumns: [...tPkCols] },
1011
+ residualScheduler: forwardResidual,
1012
+ bindParamPrefix: 'pk',
1013
+ bindColumns: tPkCols,
1014
+ backingPkDefinition,
1015
+ backingPkSourceCols,
1016
+ lookupBase,
1017
+ lookupResidualScheduler: reverseResidual,
1018
+ lookupMembershipResidualScheduler: lookupMembershipResidual,
1019
+ lookupBindColumns: pPkCols,
1020
+ lookupBindParamPrefix: 'pk',
1021
+ };
1022
+ }
1023
+ /**
1024
+ * Compile the **lookup membership** residual for the join-residual arm's delete-capable
1025
+ * lookup side: the MV body with its top-level WHERE **stripped** (membership only) and a
1026
+ * key-equality filter injected on the lookup `P`, keyed `pk0…`. The WHERE is stripped at the
1027
+ * AST level (a shallow clone dropping `where`) and the body re-built + re-analyzed, so only
1028
+ * the WHERE is removed — the join, its `ON` condition, and any projection sub-expressions are
1029
+ * preserved. Re-analysis assigns fresh node ids, so `P`'s reference is re-located by base name
1030
+ * to compute the injection target. Returns `null` if the lookup ref or the key-filter
1031
+ * injection could not be resolved (the caller then falls to the full-rebuild floor).
1032
+ *
1033
+ * Run per affected `P` key, this residual returns **every** `T` row currently joined to `P`
1034
+ * via the join's `ON` condition — irrespective of the WHERE — so {@link applyLookupResidual}
1035
+ * can delete each one's `T.pk` backing key before the in-scope residual re-upserts the
1036
+ * survivors (the membership set the WHERE-bearing reverse residual would otherwise never
1037
+ * shrink).
1038
+ */
1039
+ export function compileLookupMembershipResidual(ctx, mv, lookupBase, pPkCols) {
1040
+ const db = ctx;
1041
+ const strippedAst = { ...mv.derivation.selectAst, where: undefined };
1042
+ const stripped = db.schemaManager.withSuppressedMaterializedViewRewrite(() => {
1043
+ const { plan } = ctx._buildPlan([strippedAst]);
1044
+ return ctx.optimizer.optimizeForAnalysis(plan, db);
1045
+ });
1046
+ // Re-locate `P` in the WHERE-stripped plan by base name (fresh node ids) to build the
1047
+ // injection target key the way `compileResidual`'s callers do.
1048
+ let pRelKey;
1049
+ for (const [relKey, ref] of collectTableRefs(stripped)) {
1050
+ if (`${ref.tableSchema.schemaName}.${ref.tableSchema.name}`.toLowerCase() === lookupBase) {
1051
+ pRelKey = relKey;
1052
+ break;
1053
+ }
1054
+ }
1055
+ if (pRelKey === undefined)
1056
+ return null;
1057
+ return compileResidual(ctx, stripped, pRelKey, pPkCols, 'pk');
1058
+ }
1059
+ /**
1060
+ * Build a `'full-rebuild'` plan — the always-correct floor — for an MV whose body matches
1061
+ * no bounded-delta arm, or throw with a non-shape diagnostic. This is the fall-through
1062
+ * builder {@link tryBuildBoundedDeltaArm} routes to on a `null` (no bounded-delta arm fits).
1063
+ * See {@link FullRebuildPlan} and `docs/mv-maintenance.md` § Full-rebuild floor /
1064
+ * § Primary key inference.
1065
+ *
1066
+ * Create-time rejections (none shape-based — the floor accepts general bodies):
1067
+ * - **bag** body with no provable unique key (`keysOf` over the optimized body root is
1068
+ * empty) — there is no row identity to materialize on. `keysOf` already gates its
1069
+ * all-columns fallback on `isSet`, so a non-empty result is a real key (a true column
1070
+ * key OR the all-columns key of a provable set) and an empty result is exactly a bag.
1071
+ * (A duplicate-producing body usually fails the set contract earlier, at create-fill.)
1072
+ * - **non-deterministic** body (any `random()`/`now()`/volatile UDF anywhere in the plan)
1073
+ * without `pragma nondeterministic_schema` — no maintenance could keep it equal to its
1074
+ * plain view (mirrors the per-arm determinism rejects and the DDL determinism gate);
1075
+ * - body with **no relational output** (degenerate);
1076
+ * - **size**: full-rebuild is the only sound strategy *and* the **largest** participating
1077
+ * source exceeds the `materialized_view_rebuild_row_threshold` option
1078
+ * ({@link isFullRebuildPathological}) — every DML write would re-scan that source.
1079
+ * `0` disables the size reject (accept any size).
1080
+ */
1081
+ export function buildFullRebuildPlan(ctx, mv, analyzed) {
1082
+ const db = ctx;
1083
+ // Optimize the whole body ONCE — read-side MV rewrite suppressed so it reads its
1084
+ // sources, not the backing it populates — then derive the body's key + determinism
1085
+ // from, and compile its scheduler from, the SAME optimized plan.
1086
+ const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => ctx.optimizer.optimize(analyzed, db));
1087
+ const root = rootRelationalNode(optimized);
1088
+ if (!root)
1089
+ throw cannotMaterialize(mv.name, 'its body produced no relational output');
1090
+ // Backing key = the body's provable unique key. A bag (no provable key — a key-dropping
1091
+ // projection, a `union all` of overlapping inputs, …) has no row identity to key a
1092
+ // materialization on, so it must be a set. An all-columns pseudo-key is admitted only
1093
+ // when the body is provably a set (`keysOf` gates it on `isSet`); a bag with an
1094
+ // all-columns "key" still resolves to empty here and rejects, else duplicates would
1095
+ // collide on the backing key.
1096
+ //
1097
+ // One carve-out: a keyless body whose source key survives through value-preserving
1098
+ // passthrough lineage (the parallel-migration collation-weakening shape) is keyed on
1099
+ // the COARSENED lineage key instead of rejected — the same `deriveCoarsenedBackingKey`
1100
+ // derivation `deriveBackingShape` keyed the backing with, over the same fully-optimized
1101
+ // body, so the two agree by construction. Colliding rows then last-write-win under the
1102
+ // floor's collation-keyed `replace-all` diff (docs/materialized-views.md § Coarsened
1103
+ // backing keys); the create emitter owns the key-coarsening warning.
1104
+ if (keysOf(root).length === 0 && deriveCoarsenedBackingKey(root) === undefined) {
1105
+ throw cannotMaterialize(mv.name, 'its body has no provable unique key — it is a bag (e.g. a key-dropping '
1106
+ + 'projection or a `union all` of overlapping inputs), so it must be a set');
1107
+ }
1108
+ // Whole-body determinism: a non-deterministic body can never be kept equal to its
1109
+ // plain view by any maintenance, so it is a hard reject unless the schema-determinism
1110
+ // gate is lifted. Mirrors the per-arm determinism rejects (and the DDL gate).
1111
+ if (!db.options.getBooleanOption('nondeterministic_schema')) {
1112
+ const nonDet = findNonDeterministic(analyzed);
1113
+ if (nonDet) {
1114
+ throw cannotMaterialize(mv.name, `its body is non-deterministic (${nonDet}); a materialized view body must be `
1115
+ + 'reproducible to stay equal to its plain view (set `pragma nondeterministic_schema` to override)');
1116
+ }
1117
+ }
1118
+ // Every source the body reads (set-op legs, every join source, …) so a write to any of
1119
+ // them triggers a rebuild. Collected from the (pre-physical) analyzed plan, where every
1120
+ // source is a bare `TableReferenceNode` — the optimized plan may have wrapped them in
1121
+ // physical access nodes.
1122
+ const tableRefs = [...collectTableRefs(analyzed).values()];
1123
+ const sourceBases = [...new Set(tableRefs.map(ref => `${ref.tableSchema.schemaName}.${ref.tableSchema.name}`.toLowerCase()))];
1124
+ if (sourceBases.length === 0)
1125
+ throw cannotMaterialize(mv.name, 'its body reads no source table');
1126
+ const backing = ctx._findTable(mv.name, mv.schemaName);
1127
+ if (!backing) {
1128
+ throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
1129
+ }
1130
+ // ── Cost gate + size reject ──
1131
+ // Full-rebuild is the floor: an EMPTY sound set resolves to it (`selectMaintenanceStrategy`).
1132
+ // Cost the rebuild against the LARGEST participating source — every write re-evaluates the
1133
+ // whole body, so the largest source it scans governs whether the per-write rebuild is
1134
+ // pathological (e.g. a tiny driving table joined to a huge lookup gates on the lookup).
1135
+ // Re-resolve each source's CURRENT schema (the analyzed plan node may carry a pre-`analyze`
1136
+ // snapshot whose `statistics` predates the latest counts) so the size gate reflects the
1137
+ // live source size at create time.
1138
+ const statsProvider = ctx.optimizer.getStats();
1139
+ let largestSchema = tableRefs[0].tableSchema;
1140
+ let largestRows = -1;
1141
+ for (const ref of tableRefs) {
1142
+ const live = liveSourceSchema(ctx, ref);
1143
+ const rows = statsProvider.tableRows(live) ?? DEFAULT_SOURCE_ROWS;
1144
+ if (rows > largestRows) {
1145
+ largestRows = rows;
1146
+ largestSchema = live;
1147
+ }
1148
+ }
1149
+ const sourceStats = estimateMaintenanceStats(ctx, largestSchema, backing.columns.length, /*hasPredicate*/ false);
1150
+ // Size reject: full-rebuild is the only sound strategy here, so a source past the
1151
+ // configurable threshold makes every write pathological. `0` disables the reject.
1152
+ const rebuildThreshold = db.options.getNumberOption('materialized_view_rebuild_row_threshold');
1153
+ if (isFullRebuildPathological(sourceStats, rebuildThreshold)) {
1154
+ const largestBase = `${largestSchema.schemaName}.${largestSchema.name}`.toLowerCase();
1155
+ throw cannotMaterialize(mv.name, `its only sound maintenance strategy is a full body rebuild, but its largest source '${largestBase}' has `
1156
+ + `~${sourceStats.tableRows} rows, over the materialized_view_rebuild_row_threshold (${rebuildThreshold}) — `
1157
+ + `every write would re-scan it. Raise or disable the threshold `
1158
+ + `(\`pragma materialized_view_rebuild_row_threshold = 0\`)`);
1159
+ }
1160
+ // Compile the whole optimized body once into a reusable scheduler (no key filter).
1161
+ const bodyScheduler = new Scheduler(emitPlanNode(optimized, new EmissionContext(db)));
1162
+ const chosenStrategy = selectMaintenanceStrategy([], Math.max(1, sourceStats.tableRows * 0.01), sourceStats);
1163
+ if (chosenStrategy !== 'full-rebuild') {
1164
+ throw new QuereusError(`Internal error: cost gate selected '${chosenStrategy}' for the full-rebuild floor of materialized view '${mv.name}'`, StatusCode.INTERNAL);
1165
+ }
1166
+ return {
1167
+ kind: 'full-rebuild',
1168
+ mv,
1169
+ sourceBase: sourceBases[0],
1170
+ backingSchema: mv.schemaName,
1171
+ backingTableName: mv.name,
1172
+ chosenStrategy,
1173
+ sourceStats,
1174
+ bodyScheduler,
1175
+ sourceBases,
1176
+ };
1177
+ }
1178
+ /**
1179
+ * Compile the key-filtered residual for a binding into a reusable {@link Scheduler}:
1180
+ * the analyzed body with a key-equality filter injected on `T`'s `TableReferenceNode`
1181
+ * (parameterized `${paramPrefix}0…`), then optimized + emitted. Mirrors the assertion
1182
+ * evaluator's residual compilation (`database-assertions.ts`) so the two cannot drift.
1183
+ * Returns `null` if `injectKeyFilter` could not target `T` (the arm builder then falls
1184
+ * through to the full-rebuild floor).
1185
+ */
1186
+ export function compileResidual(ctx, analyzed, relKey, bindColumns, paramPrefix) {
1187
+ const db = ctx;
1188
+ const rewritten = injectKeyFilter(analyzed, relKey, bindColumns, paramPrefix);
1189
+ if (rewritten === analyzed)
1190
+ return null; // could not parameterize the residual → floor
1191
+ // Suppress the read-side rewrite: the residual is the MV's own body (+ a key
1192
+ // filter) compiled to maintain its backing, so it must stay over the source.
1193
+ const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => ctx.optimizer.optimize(rewritten, db));
1194
+ const instruction = emitPlanNode(optimized, new EmissionContext(db));
1195
+ return new Scheduler(instruction);
1196
+ }
1197
+ /**
1198
+ * Compile the WHOLE optimized body (no key filter, read-side rewrite suppressed) into a
1199
+ * reusable {@link Scheduler} — the residual arms' per-statement degrade-to-rebuild
1200
+ * escape ({@link ResidualArmCommon.fullRebuildScheduler}). Mirrors the floor's own
1201
+ * `bodyScheduler` compilation in {@link buildFullRebuildPlan}: run to completion against
1202
+ * live mid-transaction source state, its rows feed a single `'replace-all'` keyed diff.
1203
+ */
1204
+ function compileWholeBodyScheduler(ctx, analyzed) {
1205
+ const db = ctx;
1206
+ const optimized = db.schemaManager.withSuppressedMaterializedViewRewrite(() => ctx.optimizer.optimize(analyzed, db));
1207
+ return new Scheduler(emitPlanNode(optimized, new EmissionContext(db)));
1208
+ }
1209
+ /**
1210
+ * Build a `'prefix-delete'` plan for a single-source lateral-TVF fan-out body
1211
+ * (`select T.pk…, …, f.* from T cross join lateral tvf(<args over T>) f`), or return
1212
+ * `null` on a shape mismatch (the caller falls through to the full-rebuild floor). The
1213
+ * backing PK is the composite product key `(T.pk ∪ tvf-key)` that `keysOf` advertises
1214
+ * through the lateral join; the base PK is its leading prefix. See {@link PrefixDeletePlan}
1215
+ * and `docs/incremental-maintenance.md` § prefix-delete.
1216
+ *
1217
+ * Soundness gates (a mismatch on any returns `null` → floor): exactly one lateral TVF and
1218
+ * one join (no nested/multi TVF, no aggregate over the fan-out); the TVF advertises a
1219
+ * per-call key; the base PK projected and forming the **leading prefix** of the backing PK
1220
+ * with a non-empty TVF-key tail (so each base row's fan-out rows are individually
1221
+ * addressable and a by-prefix delete selects exactly one base row's slice). An `order by`
1222
+ * over the fan-out that reorders the composite key so the base PK no longer leads is a
1223
+ * `null` fall-through (the floor maintains it wholesale). The body's WHERE, if any, is part
1224
+ * of the residual (so an out-of-scope base row fans out to zero rows), exactly as in the
1225
+ * aggregate arm. A **non-deterministic** TVF / argument is the one hard reject (its
1226
+ * arm-specific determinism diagnostic must survive).
1227
+ */
1228
+ export function buildLateralTvfPrefixDeletePlan(ctx, mv, analyzed, tableRef, sourceBase) {
1229
+ // Exactly one lateral TVF and one join. A second base table is already excluded by
1230
+ // the single-source check upstream; this falls to the floor for a second TVF / chained
1231
+ // lateral join (`t join lateral tvf1 join lateral tvf2`).
1232
+ if (countNodeType(analyzed, PlanNodeType.TableFunctionCall) !== 1)
1233
+ return null;
1234
+ if (countJoins(analyzed) !== 1)
1235
+ return null;
1236
+ // An aggregate over the fan-out is a different shape → floor (the TVF route is taken
1237
+ // before the aggregate route, so an `... group by` over a lateral TVF lands here).
1238
+ if (findAggregate(analyzed))
1239
+ return null;
1240
+ // Determinism: a residual must reproduce exactly what `select <body>` returns, so a
1241
+ // volatile TVF (or a volatile argument expression) is a HARD reject.
1242
+ const tvf = findTableFunctionCall(analyzed);
1243
+ if (!tvf) {
1244
+ // Unreachable — countNodeType(...) === 1 above guarantees one exists.
1245
+ throw new QuereusError(`Internal error: lateral TVF node not found for materialized view '${mv.name}'`, StatusCode.INTERNAL);
1246
+ }
1247
+ if (tvf.physical.deterministic === false) {
1248
+ 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)`);
1249
+ }
1250
+ for (const operand of tvf.operands) {
1251
+ const det = checkDeterministic(operand);
1252
+ if (!det.valid)
1253
+ throw cannotMaterialize(mv.name, `it passes a non-deterministic argument (${det.expression}) to the lateral table-valued function`);
1254
+ }
1255
+ // The lateral TVF must advertise a per-call key, so the composite product key is a
1256
+ // real column key `(base PK ∪ TVF key)` rather than the all-columns / `isSet`
1257
+ // fallback. Without one the fan-out rows are not individually addressable by a proper
1258
+ // key — the by-prefix delete + keyed upsert would be unsound — so fall to the floor.
1259
+ // `getType().keys` carries the validated advertisement (an out-of-range key
1260
+ // advertisement is dropped, leaving this empty), so it is the authoritative
1261
+ // "did the TVF advertise a usable key" signal.
1262
+ if (tvf.getType().keys.length === 0)
1263
+ return null;
1264
+ // Base T's PK source columns.
1265
+ const sourceSchema = tableRef.tableSchema;
1266
+ const sourcePkCols = sourceSchema.primaryKeyDefinition.map(d => d.index);
1267
+ if (sourcePkCols.length === 0)
1268
+ return null;
1269
+ // Backing table + its physical PK (the composite product key).
1270
+ const backing = ctx._findTable(mv.name, mv.schemaName);
1271
+ if (!backing) {
1272
+ throw new QuereusError(`Internal error: backing table '${mv.name}' for materialized view '${mv.name}' not found`, StatusCode.INTERNAL);
1273
+ }
1274
+ const backingPkDefinition = resolveBackingPkColumns(backing, ctx.getCollationResolver());
1275
+ // Map each output attribute to a base-T source column (or `undefined` for a TVF
1276
+ // output column). T's attributes pass through the join unchanged, so a base-PK
1277
+ // output column resolves to a T column while a TVF-key output column does not.
1278
+ const sourceAttrToCol = new Map();
1279
+ tableRef.getAttributes().forEach((a, i) => sourceAttrToCol.set(a.id, i));
1280
+ const producingByAttrId = collectProducingExprs(analyzed);
1281
+ const rootAttrs = relationalAttributes(analyzed);
1282
+ if (!rootAttrs)
1283
+ return null;
1284
+ // Prefix soundness: the LEADING `basePrefixLen` backing-PK columns must each
1285
+ // project (transitively) a distinct base-T PK column, their set must equal the base
1286
+ // PK, and there must be a non-empty TVF-key tail. So the base PK is the leading
1287
+ // prefix of the composite product key and the by-prefix delete selects exactly one
1288
+ // base row's fan-out. A composite key that did not form this way falls to the floor.
1289
+ const basePrefixLen = sourcePkCols.length;
1290
+ if (backingPkDefinition.length <= basePrefixLen)
1291
+ return null;
1292
+ const basePkSet = new Set(sourcePkCols);
1293
+ const leadingSourceCols = new Set();
1294
+ const backingPrefixSourceCols = [];
1295
+ for (let i = 0; i < basePrefixLen; i++) {
1296
+ const d = backingPkDefinition[i];
1297
+ const attr = rootAttrs[d.index];
1298
+ const sc = attr ? resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
1299
+ if (sc === undefined || !basePkSet.has(sc))
1300
+ return null; // base PK not the leading prefix → floor
1301
+ // Soundness precondition for the binary prefix scan (the property
1302
+ // `prefix-delete-noncase-collation-regression-test` locks in): the backing base-PK
1303
+ // column MUST inherit the source PK column's collation. The btree orders this prefix
1304
+ // by `d.collation`, but the keyed diff's existing-slice read (`scanEffective` with
1305
+ // the base prefix, in `applyPrefixDelete`) early-terminates the prefix scan on a
1306
+ // BINARY compare (scan-layer.ts) — sound ONLY because source-PK uniqueness under that
1307
+ // collation collapses each collation class to a single binary value, so a base row's
1308
+ // fan-out rows are binary-homogeneous and contiguous. A backing collation MORE
1309
+ // permissive than the source's would let collation-equal/binary-different base rows
1310
+ // interleave and break that. The backing column derives its collation from the body
1311
+ // relation's type (deriveBackingShape), so a mismatch is an internal derivation bug —
1312
+ // fail loud rather than register an unsound plan.
1313
+ const backingColl = normalizeCollation(d.collation);
1314
+ const sourceColl = normalizeCollation(sourceSchema.columns[sc]?.collation);
1315
+ if (backingColl !== sourceColl) {
1316
+ throw new QuereusError(`Internal error: materialized view '${mv.name}' backing base-PK column `
1317
+ + `'${backing.columns[d.index]?.name ?? d.index}' has collation '${backingColl}' but its source `
1318
+ + `primary-key column '${sourceSchema.columns[sc]?.name ?? sc}' has collation '${sourceColl}'; `
1319
+ + `the prefix-delete arm's binary prefix scan requires the backing base-PK column to inherit the `
1320
+ + `source PK collation (see scan-layer.ts early-termination)`, StatusCode.INTERNAL);
1321
+ }
1322
+ leadingSourceCols.add(sc);
1323
+ backingPrefixSourceCols.push(sc);
1324
+ }
1325
+ if (leadingSourceCols.size !== basePkSet.size)
1326
+ return null; // prefix does not cover the base PK → floor
1327
+ // The TVF-key tail must NOT re-use a base-PK column — else the fan-out rows would
1328
+ // not be distinguished and the "key" would be base-only (defensive: the product key
1329
+ // places the TVF key, a distinct relation's columns, in the tail). Otherwise → floor.
1330
+ for (let i = basePrefixLen; i < backingPkDefinition.length; i++) {
1331
+ const d = backingPkDefinition[i];
1332
+ const attr = rootAttrs[d.index];
1333
+ const sc = attr ? resolveTransitiveSourceCol(attr.id, sourceAttrToCol, producingByAttrId) : undefined;
1334
+ if (sc !== undefined && basePkSet.has(sc))
1335
+ return null;
1336
+ }
1337
+ // Compile + cache the base-PK-keyed residual once (the body with `T.pk = :pk0 AND …`
1338
+ // injected on T). Re-run per affected base key against the live transaction; it
1339
+ // re-runs the lateral join + TVF for that single base row, fanning out to N rows.
1340
+ const relKey = `${sourceBase}#${tableRef.id ?? 'unknown'}`;
1341
+ const residualScheduler = compileResidual(ctx, analyzed, relKey, sourcePkCols, 'pk');
1342
+ if (!residualScheduler)
1343
+ return null; // could not parameterize the residual → floor
1344
+ // ── Cost gate ──
1345
+ // The fan-out residual shares the residual-recompute cost shape (a key-filtered
1346
+ // re-execution of the body); the fan-out factor (rows per base key) is not known at
1347
+ // create, so we cost it as a residual and record the choice for substrate parity.
1348
+ // The synchronous reject-at-create / degrade-to-rebuild machinery stays dormant, as
1349
+ // it does for the other arms (a TVF whose fan-out is pathological is not detectable
1350
+ // without fan-out stats — deferred with the fanning-keyed-join follow-up).
1351
+ const soundStrategies = ['residual-recompute'];
1352
+ const hasPredicate = mv.derivation.selectAst.type === 'select' && mv.derivation.selectAst.where !== undefined;
1353
+ const sourceStats = estimateMaintenanceStats(ctx, sourceSchema, backing.columns.length, hasPredicate);
1354
+ const estimatedChangeCardinality = Math.max(1, sourceStats.tableRows * 0.01);
1355
+ const chosenStrategy = selectMaintenanceStrategy(soundStrategies, estimatedChangeCardinality, sourceStats);
1356
+ if (chosenStrategy !== 'residual-recompute') {
1357
+ throw new QuereusError(`Internal error: cost gate selected unwired strategy '${chosenStrategy}' for materialized view '${mv.name}'`, StatusCode.INTERNAL);
1358
+ }
1359
+ return {
1360
+ kind: 'prefix-delete',
1361
+ mv,
1362
+ sourceBase,
1363
+ backingSchema: mv.schemaName,
1364
+ backingTableName: mv.name,
1365
+ chosenStrategy,
1366
+ sourceStats,
1367
+ binding: { kind: 'row', keyColumns: [...sourcePkCols] },
1368
+ residualScheduler,
1369
+ bindParamPrefix: 'pk',
1370
+ bindColumns: sourcePkCols,
1371
+ backingPkDefinition,
1372
+ basePrefixLength: basePrefixLen,
1373
+ backingPrefixSourceCols,
1374
+ };
1375
+ }
1376
+ /**
1377
+ * Assemble {@link MaintenanceSourceStats} for the cost gate from the optimizer's
1378
+ * StatsProvider and tuning. `tableRows` / `distinctGroupsEstimate` come from the
1379
+ * provider (heuristic defaults when absent); `forwardBodyCost` is estimated from the
1380
+ * forward cost helpers (a scan + optional filter + projection of the source — the
1381
+ * covering-index body shape); `fallbackRatio` carries the detection kernel's
1382
+ * `deltaPerRowFallbackRatio` for the no-stats residual path.
1383
+ */
1384
+ /**
1385
+ * The CURRENT `TableSchema` of a source `TableReferenceNode`, re-resolved through the
1386
+ * schema manager. A plan node captures the schema as of plan-build; a later `analyze`
1387
+ * replaces the catalog entry with one carrying fresh `statistics`, so the stale captured
1388
+ * schema would report pre-`analyze` row counts. Re-resolving keeps the floor's size gate
1389
+ * on the live source size. Falls back to the node's captured schema if the name no longer
1390
+ * resolves (it always should — the body planned).
1391
+ */
1392
+ export function liveSourceSchema(ctx, ref) {
1393
+ const captured = ref.tableSchema;
1394
+ return ctx._findTable(captured.name, captured.schemaName) ?? captured;
1395
+ }
1396
+ export function estimateMaintenanceStats(ctx, sourceSchema, projectionCount, hasPredicate) {
1397
+ const optimizer = ctx.optimizer;
1398
+ const statsProvider = optimizer.getStats();
1399
+ const tableRows = statsProvider.tableRows(sourceSchema) ?? DEFAULT_SOURCE_ROWS;
1400
+ const forwardBodyCost = seqScanCost(tableRows)
1401
+ + (hasPredicate ? filterCost(tableRows) : 0)
1402
+ + projectCost(tableRows, projectionCount);
1403
+ const stats = {
1404
+ tableRows,
1405
+ forwardBodyCost,
1406
+ fallbackRatio: optimizer.tuning.deltaPerRowFallbackRatio,
1407
+ };
1408
+ // `distinctValues` is an optional, per-column StatsProvider method. For the
1409
+ // covering-index shape the source PK is the grouping key; a single-column PK
1410
+ // yields a usable distinct-groups estimate (which only feeds the never-chosen-here
1411
+ // residual cost). Multi-column PKs leave it unset → residual takes the no-stats path.
1412
+ const pkDef = sourceSchema.primaryKeyDefinition;
1413
+ if (pkDef.length === 1 && statsProvider.distinctValues) {
1414
+ const pkColName = sourceSchema.columns[pkDef[0].index]?.name;
1415
+ if (pkColName !== undefined) {
1416
+ const distinct = statsProvider.distinctValues(sourceSchema, pkColName);
1417
+ if (distinct !== undefined)
1418
+ stats.distinctGroupsEstimate = distinct;
1419
+ }
1420
+ }
1421
+ return stats;
1422
+ }
1423
+ //# sourceMappingURL=database-materialized-views-plan-builders.js.map