@quereus/quereus 3.3.0 → 4.0.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 (900) hide show
  1. package/README.md +7 -0
  2. package/dist/src/common/datatype.d.ts +12 -0
  3. package/dist/src/common/datatype.d.ts.map +1 -1
  4. package/dist/src/common/datatype.js.map +1 -1
  5. package/dist/src/common/types.d.ts +24 -0
  6. package/dist/src/common/types.d.ts.map +1 -1
  7. package/dist/src/common/types.js.map +1 -1
  8. package/dist/src/core/database-assertions.d.ts +37 -9
  9. package/dist/src/core/database-assertions.d.ts.map +1 -1
  10. package/dist/src/core/database-assertions.js +62 -110
  11. package/dist/src/core/database-assertions.js.map +1 -1
  12. package/dist/src/core/database-events.d.ts +163 -0
  13. package/dist/src/core/database-events.d.ts.map +1 -1
  14. package/dist/src/core/database-events.js +235 -21
  15. package/dist/src/core/database-events.js.map +1 -1
  16. package/dist/src/core/database-external-changes.d.ts +28 -0
  17. package/dist/src/core/database-external-changes.d.ts.map +1 -0
  18. package/dist/src/core/database-external-changes.js +242 -0
  19. package/dist/src/core/database-external-changes.js.map +1 -0
  20. package/dist/src/core/database-internal.d.ts +50 -1
  21. package/dist/src/core/database-internal.d.ts.map +1 -1
  22. package/dist/src/core/database-materialized-views.d.ts +1253 -0
  23. package/dist/src/core/database-materialized-views.d.ts.map +1 -0
  24. package/dist/src/core/database-materialized-views.js +3064 -0
  25. package/dist/src/core/database-materialized-views.js.map +1 -0
  26. package/dist/src/core/database-options.d.ts +4 -0
  27. package/dist/src/core/database-options.d.ts.map +1 -1
  28. package/dist/src/core/database-options.js +10 -0
  29. package/dist/src/core/database-options.js.map +1 -1
  30. package/dist/src/core/database-transaction.d.ts +19 -3
  31. package/dist/src/core/database-transaction.d.ts.map +1 -1
  32. package/dist/src/core/database-transaction.js +30 -3
  33. package/dist/src/core/database-transaction.js.map +1 -1
  34. package/dist/src/core/database-watchers.d.ts +19 -0
  35. package/dist/src/core/database-watchers.d.ts.map +1 -1
  36. package/dist/src/core/database-watchers.js +63 -3
  37. package/dist/src/core/database-watchers.js.map +1 -1
  38. package/dist/src/core/database.d.ts +203 -11
  39. package/dist/src/core/database.d.ts.map +1 -1
  40. package/dist/src/core/database.js +493 -29
  41. package/dist/src/core/database.js.map +1 -1
  42. package/dist/src/core/derived-row-validator.d.ts +137 -0
  43. package/dist/src/core/derived-row-validator.d.ts.map +1 -0
  44. package/dist/src/core/derived-row-validator.js +314 -0
  45. package/dist/src/core/derived-row-validator.js.map +1 -0
  46. package/dist/src/core/statement.d.ts.map +1 -1
  47. package/dist/src/core/statement.js +30 -9
  48. package/dist/src/core/statement.js.map +1 -1
  49. package/dist/src/emit/ast-stringify.d.ts +135 -1
  50. package/dist/src/emit/ast-stringify.d.ts.map +1 -1
  51. package/dist/src/emit/ast-stringify.js +793 -118
  52. package/dist/src/emit/ast-stringify.js.map +1 -1
  53. package/dist/src/func/builtins/aggregate.d.ts.map +1 -1
  54. package/dist/src/func/builtins/aggregate.js +11 -10
  55. package/dist/src/func/builtins/aggregate.js.map +1 -1
  56. package/dist/src/func/builtins/builtin-window-functions.d.ts.map +1 -1
  57. package/dist/src/func/builtins/builtin-window-functions.js +32 -0
  58. package/dist/src/func/builtins/builtin-window-functions.js.map +1 -1
  59. package/dist/src/func/builtins/explain.d.ts +3 -0
  60. package/dist/src/func/builtins/explain.d.ts.map +1 -1
  61. package/dist/src/func/builtins/explain.js +229 -0
  62. package/dist/src/func/builtins/explain.js.map +1 -1
  63. package/dist/src/func/builtins/index.d.ts.map +1 -1
  64. package/dist/src/func/builtins/index.js +10 -2
  65. package/dist/src/func/builtins/index.js.map +1 -1
  66. package/dist/src/func/builtins/json.d.ts.map +1 -1
  67. package/dist/src/func/builtins/json.js +3 -2
  68. package/dist/src/func/builtins/json.js.map +1 -1
  69. package/dist/src/func/builtins/mutation.d.ts +2 -0
  70. package/dist/src/func/builtins/mutation.d.ts.map +1 -0
  71. package/dist/src/func/builtins/mutation.js +53 -0
  72. package/dist/src/func/builtins/mutation.js.map +1 -0
  73. package/dist/src/func/builtins/schema.d.ts +2 -0
  74. package/dist/src/func/builtins/schema.d.ts.map +1 -1
  75. package/dist/src/func/builtins/schema.js +713 -26
  76. package/dist/src/func/builtins/schema.js.map +1 -1
  77. package/dist/src/func/builtins/string.js +1 -1
  78. package/dist/src/func/builtins/string.js.map +1 -1
  79. package/dist/src/func/registration.d.ts +9 -0
  80. package/dist/src/func/registration.d.ts.map +1 -1
  81. package/dist/src/func/registration.js +4 -0
  82. package/dist/src/func/registration.js.map +1 -1
  83. package/dist/src/index.d.ts +25 -6
  84. package/dist/src/index.d.ts.map +1 -1
  85. package/dist/src/index.js +27 -3
  86. package/dist/src/index.js.map +1 -1
  87. package/dist/src/parser/ast.d.ts +353 -21
  88. package/dist/src/parser/ast.d.ts.map +1 -1
  89. package/dist/src/parser/index.d.ts +14 -1
  90. package/dist/src/parser/index.d.ts.map +1 -1
  91. package/dist/src/parser/index.js +19 -0
  92. package/dist/src/parser/index.js.map +1 -1
  93. package/dist/src/parser/lexer.d.ts +9 -0
  94. package/dist/src/parser/lexer.d.ts.map +1 -1
  95. package/dist/src/parser/lexer.js +9 -0
  96. package/dist/src/parser/lexer.js.map +1 -1
  97. package/dist/src/parser/parser.d.ts +276 -7
  98. package/dist/src/parser/parser.d.ts.map +1 -1
  99. package/dist/src/parser/parser.js +1387 -469
  100. package/dist/src/parser/parser.js.map +1 -1
  101. package/dist/src/parser/visitor.d.ts.map +1 -1
  102. package/dist/src/parser/visitor.js +12 -8
  103. package/dist/src/parser/visitor.js.map +1 -1
  104. package/dist/src/planner/analysis/assertion-classifier.d.ts.map +1 -1
  105. package/dist/src/planner/analysis/assertion-classifier.js +4 -0
  106. package/dist/src/planner/analysis/assertion-classifier.js.map +1 -1
  107. package/dist/src/planner/analysis/assertion-hoist-cache.d.ts.map +1 -1
  108. package/dist/src/planner/analysis/assertion-hoist-cache.js +8 -4
  109. package/dist/src/planner/analysis/assertion-hoist-cache.js.map +1 -1
  110. package/dist/src/planner/analysis/authored-inverse.d.ts +22 -0
  111. package/dist/src/planner/analysis/authored-inverse.d.ts.map +1 -0
  112. package/dist/src/planner/analysis/authored-inverse.js +267 -0
  113. package/dist/src/planner/analysis/authored-inverse.js.map +1 -0
  114. package/dist/src/planner/analysis/change-scope.d.ts +34 -4
  115. package/dist/src/planner/analysis/change-scope.d.ts.map +1 -1
  116. package/dist/src/planner/analysis/change-scope.js +108 -7
  117. package/dist/src/planner/analysis/change-scope.js.map +1 -1
  118. package/dist/src/planner/analysis/check-extraction.d.ts +36 -2
  119. package/dist/src/planner/analysis/check-extraction.d.ts.map +1 -1
  120. package/dist/src/planner/analysis/check-extraction.js +174 -46
  121. package/dist/src/planner/analysis/check-extraction.js.map +1 -1
  122. package/dist/src/planner/analysis/coarsened-key.d.ts +109 -0
  123. package/dist/src/planner/analysis/coarsened-key.d.ts.map +1 -0
  124. package/dist/src/planner/analysis/coarsened-key.js +228 -0
  125. package/dist/src/planner/analysis/coarsened-key.js.map +1 -0
  126. package/dist/src/planner/analysis/comparison-collation.d.ts +216 -0
  127. package/dist/src/planner/analysis/comparison-collation.d.ts.map +1 -0
  128. package/dist/src/planner/analysis/comparison-collation.js +341 -0
  129. package/dist/src/planner/analysis/comparison-collation.js.map +1 -0
  130. package/dist/src/planner/analysis/constraint-extractor.d.ts +3 -1
  131. package/dist/src/planner/analysis/constraint-extractor.d.ts.map +1 -1
  132. package/dist/src/planner/analysis/constraint-extractor.js +192 -9
  133. package/dist/src/planner/analysis/constraint-extractor.js.map +1 -1
  134. package/dist/src/planner/analysis/coverage-prover.d.ts +321 -0
  135. package/dist/src/planner/analysis/coverage-prover.d.ts.map +1 -0
  136. package/dist/src/planner/analysis/coverage-prover.js +1038 -0
  137. package/dist/src/planner/analysis/coverage-prover.js.map +1 -0
  138. package/dist/src/planner/analysis/key-filter.d.ts +22 -0
  139. package/dist/src/planner/analysis/key-filter.d.ts.map +1 -0
  140. package/dist/src/planner/analysis/key-filter.js +105 -0
  141. package/dist/src/planner/analysis/key-filter.js.map +1 -0
  142. package/dist/src/planner/analysis/partial-unique-extraction.d.ts +36 -1
  143. package/dist/src/planner/analysis/partial-unique-extraction.d.ts.map +1 -1
  144. package/dist/src/planner/analysis/partial-unique-extraction.js +148 -22
  145. package/dist/src/planner/analysis/partial-unique-extraction.js.map +1 -1
  146. package/dist/src/planner/analysis/predicate-normalizer.d.ts.map +1 -1
  147. package/dist/src/planner/analysis/predicate-normalizer.js +30 -1
  148. package/dist/src/planner/analysis/predicate-normalizer.js.map +1 -1
  149. package/dist/src/planner/analysis/predicate-shape.d.ts +36 -1
  150. package/dist/src/planner/analysis/predicate-shape.d.ts.map +1 -1
  151. package/dist/src/planner/analysis/predicate-shape.js +51 -13
  152. package/dist/src/planner/analysis/predicate-shape.js.map +1 -1
  153. package/dist/src/planner/analysis/query-rewrite-matcher.d.ts +314 -0
  154. package/dist/src/planner/analysis/query-rewrite-matcher.d.ts.map +1 -0
  155. package/dist/src/planner/analysis/query-rewrite-matcher.js +1081 -0
  156. package/dist/src/planner/analysis/query-rewrite-matcher.js.map +1 -0
  157. package/dist/src/planner/analysis/scalar-invertibility.d.ts +92 -0
  158. package/dist/src/planner/analysis/scalar-invertibility.d.ts.map +1 -0
  159. package/dist/src/planner/analysis/scalar-invertibility.js +129 -0
  160. package/dist/src/planner/analysis/scalar-invertibility.js.map +1 -0
  161. package/dist/src/planner/analysis/update-lineage.d.ts +196 -0
  162. package/dist/src/planner/analysis/update-lineage.d.ts.map +1 -0
  163. package/dist/src/planner/analysis/update-lineage.js +322 -0
  164. package/dist/src/planner/analysis/update-lineage.js.map +1 -0
  165. package/dist/src/planner/analysis/view-complement.d.ts +42 -0
  166. package/dist/src/planner/analysis/view-complement.d.ts.map +1 -0
  167. package/dist/src/planner/analysis/view-complement.js +54 -0
  168. package/dist/src/planner/analysis/view-complement.js.map +1 -0
  169. package/dist/src/planner/building/alter-table.d.ts +1 -1
  170. package/dist/src/planner/building/alter-table.d.ts.map +1 -1
  171. package/dist/src/planner/building/alter-table.js +211 -2
  172. package/dist/src/planner/building/alter-table.js.map +1 -1
  173. package/dist/src/planner/building/block.d.ts.map +1 -1
  174. package/dist/src/planner/building/block.js +18 -1
  175. package/dist/src/planner/building/block.js.map +1 -1
  176. package/dist/src/planner/building/constraint-builder.d.ts +33 -5
  177. package/dist/src/planner/building/constraint-builder.d.ts.map +1 -1
  178. package/dist/src/planner/building/constraint-builder.js +63 -28
  179. package/dist/src/planner/building/constraint-builder.js.map +1 -1
  180. package/dist/src/planner/building/create-view.d.ts +9 -0
  181. package/dist/src/planner/building/create-view.d.ts.map +1 -1
  182. package/dist/src/planner/building/create-view.js +41 -12
  183. package/dist/src/planner/building/create-view.js.map +1 -1
  184. package/dist/src/planner/building/ddl.d.ts.map +1 -1
  185. package/dist/src/planner/building/ddl.js +94 -0
  186. package/dist/src/planner/building/ddl.js.map +1 -1
  187. package/dist/src/planner/building/declare-schema.d.ts +1 -0
  188. package/dist/src/planner/building/declare-schema.d.ts.map +1 -1
  189. package/dist/src/planner/building/declare-schema.js +4 -1
  190. package/dist/src/planner/building/declare-schema.js.map +1 -1
  191. package/dist/src/planner/building/default-scope.d.ts +26 -0
  192. package/dist/src/planner/building/default-scope.d.ts.map +1 -0
  193. package/dist/src/planner/building/default-scope.js +41 -0
  194. package/dist/src/planner/building/default-scope.js.map +1 -0
  195. package/dist/src/planner/building/delete.d.ts +19 -1
  196. package/dist/src/planner/building/delete.d.ts.map +1 -1
  197. package/dist/src/planner/building/delete.js +109 -30
  198. package/dist/src/planner/building/delete.js.map +1 -1
  199. package/dist/src/planner/building/dml-target.d.ts +118 -0
  200. package/dist/src/planner/building/dml-target.d.ts.map +1 -0
  201. package/dist/src/planner/building/dml-target.js +282 -0
  202. package/dist/src/planner/building/dml-target.js.map +1 -0
  203. package/dist/src/planner/building/drop-index.d.ts.map +1 -1
  204. package/dist/src/planner/building/drop-index.js +4 -1
  205. package/dist/src/planner/building/drop-index.js.map +1 -1
  206. package/dist/src/planner/building/drop-view.d.ts.map +1 -1
  207. package/dist/src/planner/building/drop-view.js +4 -2
  208. package/dist/src/planner/building/drop-view.js.map +1 -1
  209. package/dist/src/planner/building/expression.d.ts.map +1 -1
  210. package/dist/src/planner/building/expression.js +60 -21
  211. package/dist/src/planner/building/expression.js.map +1 -1
  212. package/dist/src/planner/building/foreign-key-builder.d.ts +30 -0
  213. package/dist/src/planner/building/foreign-key-builder.d.ts.map +1 -1
  214. package/dist/src/planner/building/foreign-key-builder.js +160 -129
  215. package/dist/src/planner/building/foreign-key-builder.js.map +1 -1
  216. package/dist/src/planner/building/insert.d.ts +45 -2
  217. package/dist/src/planner/building/insert.d.ts.map +1 -1
  218. package/dist/src/planner/building/insert.js +257 -88
  219. package/dist/src/planner/building/insert.js.map +1 -1
  220. package/dist/src/planner/building/lens-auxiliary-access.d.ts +22 -0
  221. package/dist/src/planner/building/lens-auxiliary-access.d.ts.map +1 -0
  222. package/dist/src/planner/building/lens-auxiliary-access.js +132 -0
  223. package/dist/src/planner/building/lens-auxiliary-access.js.map +1 -0
  224. package/dist/src/planner/building/materialized-view.d.ts +16 -0
  225. package/dist/src/planner/building/materialized-view.d.ts.map +1 -0
  226. package/dist/src/planner/building/materialized-view.js +57 -0
  227. package/dist/src/planner/building/materialized-view.js.map +1 -0
  228. package/dist/src/planner/building/returning-star.d.ts +32 -0
  229. package/dist/src/planner/building/returning-star.d.ts.map +1 -0
  230. package/dist/src/planner/building/returning-star.js +45 -0
  231. package/dist/src/planner/building/returning-star.js.map +1 -0
  232. package/dist/src/planner/building/select-aggregates.d.ts.map +1 -1
  233. package/dist/src/planner/building/select-aggregates.js +47 -0
  234. package/dist/src/planner/building/select-aggregates.js.map +1 -1
  235. package/dist/src/planner/building/select-compound.d.ts.map +1 -1
  236. package/dist/src/planner/building/select-compound.js +84 -11
  237. package/dist/src/planner/building/select-compound.js.map +1 -1
  238. package/dist/src/planner/building/select-context.d.ts +10 -2
  239. package/dist/src/planner/building/select-context.d.ts.map +1 -1
  240. package/dist/src/planner/building/select-context.js +7 -1
  241. package/dist/src/planner/building/select-context.js.map +1 -1
  242. package/dist/src/planner/building/select-modifiers.js +6 -0
  243. package/dist/src/planner/building/select-modifiers.js.map +1 -1
  244. package/dist/src/planner/building/select-ordinal.d.ts +18 -0
  245. package/dist/src/planner/building/select-ordinal.d.ts.map +1 -1
  246. package/dist/src/planner/building/select-ordinal.js +30 -0
  247. package/dist/src/planner/building/select-ordinal.js.map +1 -1
  248. package/dist/src/planner/building/select-projections.d.ts +8 -2
  249. package/dist/src/planner/building/select-projections.d.ts.map +1 -1
  250. package/dist/src/planner/building/select-projections.js +26 -4
  251. package/dist/src/planner/building/select-projections.js.map +1 -1
  252. package/dist/src/planner/building/select-window.d.ts.map +1 -1
  253. package/dist/src/planner/building/select-window.js +8 -5
  254. package/dist/src/planner/building/select-window.js.map +1 -1
  255. package/dist/src/planner/building/select.d.ts.map +1 -1
  256. package/dist/src/planner/building/select.js +164 -59
  257. package/dist/src/planner/building/select.js.map +1 -1
  258. package/dist/src/planner/building/set-object-tags.d.ts +7 -0
  259. package/dist/src/planner/building/set-object-tags.d.ts.map +1 -0
  260. package/dist/src/planner/building/set-object-tags.js +38 -0
  261. package/dist/src/planner/building/set-object-tags.js.map +1 -0
  262. package/dist/src/planner/building/tag-diagnostics.d.ts +27 -0
  263. package/dist/src/planner/building/tag-diagnostics.d.ts.map +1 -0
  264. package/dist/src/planner/building/tag-diagnostics.js +37 -0
  265. package/dist/src/planner/building/tag-diagnostics.js.map +1 -0
  266. package/dist/src/planner/building/update.d.ts +18 -1
  267. package/dist/src/planner/building/update.d.ts.map +1 -1
  268. package/dist/src/planner/building/update.js +134 -58
  269. package/dist/src/planner/building/update.js.map +1 -1
  270. package/dist/src/planner/building/view-mutation-builder.d.ts +15 -0
  271. package/dist/src/planner/building/view-mutation-builder.d.ts.map +1 -0
  272. package/dist/src/planner/building/view-mutation-builder.js +1158 -0
  273. package/dist/src/planner/building/view-mutation-builder.js.map +1 -0
  274. package/dist/src/planner/building/with.d.ts +11 -0
  275. package/dist/src/planner/building/with.d.ts.map +1 -1
  276. package/dist/src/planner/building/with.js +48 -10
  277. package/dist/src/planner/building/with.js.map +1 -1
  278. package/dist/src/planner/cost/index.d.ts +83 -0
  279. package/dist/src/planner/cost/index.d.ts.map +1 -1
  280. package/dist/src/planner/cost/index.js +114 -0
  281. package/dist/src/planner/cost/index.js.map +1 -1
  282. package/dist/src/planner/framework/characteristics.d.ts +38 -4
  283. package/dist/src/planner/framework/characteristics.d.ts.map +1 -1
  284. package/dist/src/planner/framework/characteristics.js +50 -6
  285. package/dist/src/planner/framework/characteristics.js.map +1 -1
  286. package/dist/src/planner/framework/pass.d.ts.map +1 -1
  287. package/dist/src/planner/framework/pass.js +2 -1
  288. package/dist/src/planner/framework/pass.js.map +1 -1
  289. package/dist/src/planner/framework/registry.d.ts +39 -1
  290. package/dist/src/planner/framework/registry.d.ts.map +1 -1
  291. package/dist/src/planner/framework/registry.js +18 -2
  292. package/dist/src/planner/framework/registry.js.map +1 -1
  293. package/dist/src/planner/mutation/backward-body.d.ts +131 -0
  294. package/dist/src/planner/mutation/backward-body.d.ts.map +1 -0
  295. package/dist/src/planner/mutation/backward-body.js +135 -0
  296. package/dist/src/planner/mutation/backward-body.js.map +1 -0
  297. package/dist/src/planner/mutation/cte-flatten.d.ts +17 -0
  298. package/dist/src/planner/mutation/cte-flatten.d.ts.map +1 -0
  299. package/dist/src/planner/mutation/cte-flatten.js +364 -0
  300. package/dist/src/planner/mutation/cte-flatten.js.map +1 -0
  301. package/dist/src/planner/mutation/decomposition.d.ts +273 -0
  302. package/dist/src/planner/mutation/decomposition.d.ts.map +1 -0
  303. package/dist/src/planner/mutation/decomposition.js +1719 -0
  304. package/dist/src/planner/mutation/decomposition.js.map +1 -0
  305. package/dist/src/planner/mutation/lens-enforcement.d.ts +165 -0
  306. package/dist/src/planner/mutation/lens-enforcement.d.ts.map +1 -0
  307. package/dist/src/planner/mutation/lens-enforcement.js +745 -0
  308. package/dist/src/planner/mutation/lens-enforcement.js.map +1 -0
  309. package/dist/src/planner/mutation/multi-source.d.ts +568 -0
  310. package/dist/src/planner/mutation/multi-source.d.ts.map +1 -0
  311. package/dist/src/planner/mutation/multi-source.js +2915 -0
  312. package/dist/src/planner/mutation/multi-source.js.map +1 -0
  313. package/dist/src/planner/mutation/mutation-diagnostic.d.ts +37 -0
  314. package/dist/src/planner/mutation/mutation-diagnostic.d.ts.map +1 -0
  315. package/dist/src/planner/mutation/mutation-diagnostic.js +24 -0
  316. package/dist/src/planner/mutation/mutation-diagnostic.js.map +1 -0
  317. package/dist/src/planner/mutation/mutation-tags.d.ts +33 -0
  318. package/dist/src/planner/mutation/mutation-tags.d.ts.map +1 -0
  319. package/dist/src/planner/mutation/mutation-tags.js +31 -0
  320. package/dist/src/planner/mutation/mutation-tags.js.map +1 -0
  321. package/dist/src/planner/mutation/propagate.d.ts +97 -0
  322. package/dist/src/planner/mutation/propagate.d.ts.map +1 -0
  323. package/dist/src/planner/mutation/propagate.js +220 -0
  324. package/dist/src/planner/mutation/propagate.js.map +1 -0
  325. package/dist/src/planner/mutation/scope-transform.d.ts +181 -0
  326. package/dist/src/planner/mutation/scope-transform.d.ts.map +1 -0
  327. package/dist/src/planner/mutation/scope-transform.js +574 -0
  328. package/dist/src/planner/mutation/scope-transform.js.map +1 -0
  329. package/dist/src/planner/mutation/set-op.d.ts +242 -0
  330. package/dist/src/planner/mutation/set-op.d.ts.map +1 -0
  331. package/dist/src/planner/mutation/set-op.js +1687 -0
  332. package/dist/src/planner/mutation/set-op.js.map +1 -0
  333. package/dist/src/planner/mutation/single-source.d.ts +261 -0
  334. package/dist/src/planner/mutation/single-source.d.ts.map +1 -0
  335. package/dist/src/planner/mutation/single-source.js +1096 -0
  336. package/dist/src/planner/mutation/single-source.js.map +1 -0
  337. package/dist/src/planner/nodes/aggregate-node.js +3 -3
  338. package/dist/src/planner/nodes/aggregate-node.js.map +1 -1
  339. package/dist/src/planner/nodes/alias-node.d.ts.map +1 -1
  340. package/dist/src/planner/nodes/alias-node.js +5 -1
  341. package/dist/src/planner/nodes/alias-node.js.map +1 -1
  342. package/dist/src/planner/nodes/alter-table-node.d.ts +124 -1
  343. package/dist/src/planner/nodes/alter-table-node.d.ts.map +1 -1
  344. package/dist/src/planner/nodes/alter-table-node.js +27 -0
  345. package/dist/src/planner/nodes/alter-table-node.js.map +1 -1
  346. package/dist/src/planner/nodes/analyze-node.d.ts +2 -1
  347. package/dist/src/planner/nodes/analyze-node.d.ts.map +1 -1
  348. package/dist/src/planner/nodes/analyze-node.js +18 -1
  349. package/dist/src/planner/nodes/analyze-node.js.map +1 -1
  350. package/dist/src/planner/nodes/asserted-keys-node.d.ts +43 -0
  351. package/dist/src/planner/nodes/asserted-keys-node.d.ts.map +1 -0
  352. package/dist/src/planner/nodes/asserted-keys-node.js +99 -0
  353. package/dist/src/planner/nodes/asserted-keys-node.js.map +1 -0
  354. package/dist/src/planner/nodes/async-gather-node.d.ts.map +1 -1
  355. package/dist/src/planner/nodes/async-gather-node.js +33 -8
  356. package/dist/src/planner/nodes/async-gather-node.js.map +1 -1
  357. package/dist/src/planner/nodes/bloom-join-node.d.ts.map +1 -1
  358. package/dist/src/planner/nodes/bloom-join-node.js +2 -1
  359. package/dist/src/planner/nodes/bloom-join-node.js.map +1 -1
  360. package/dist/src/planner/nodes/create-view-node.d.ts +7 -2
  361. package/dist/src/planner/nodes/create-view-node.d.ts.map +1 -1
  362. package/dist/src/planner/nodes/create-view-node.js +4 -1
  363. package/dist/src/planner/nodes/create-view-node.js.map +1 -1
  364. package/dist/src/planner/nodes/declarative-schema.d.ts +13 -1
  365. package/dist/src/planner/nodes/declarative-schema.d.ts.map +1 -1
  366. package/dist/src/planner/nodes/declarative-schema.js +32 -0
  367. package/dist/src/planner/nodes/declarative-schema.js.map +1 -1
  368. package/dist/src/planner/nodes/distinct-node.d.ts.map +1 -1
  369. package/dist/src/planner/nodes/distinct-node.js +2 -0
  370. package/dist/src/planner/nodes/distinct-node.js.map +1 -1
  371. package/dist/src/planner/nodes/dml-executor-node.d.ts +29 -1
  372. package/dist/src/planner/nodes/dml-executor-node.d.ts.map +1 -1
  373. package/dist/src/planner/nodes/dml-executor-node.js +27 -3
  374. package/dist/src/planner/nodes/dml-executor-node.js.map +1 -1
  375. package/dist/src/planner/nodes/eager-prefetch-node.d.ts.map +1 -1
  376. package/dist/src/planner/nodes/eager-prefetch-node.js +2 -0
  377. package/dist/src/planner/nodes/eager-prefetch-node.js.map +1 -1
  378. package/dist/src/planner/nodes/envelope-scan-node.d.ts +42 -0
  379. package/dist/src/planner/nodes/envelope-scan-node.d.ts.map +1 -0
  380. package/dist/src/planner/nodes/envelope-scan-node.js +62 -0
  381. package/dist/src/planner/nodes/envelope-scan-node.js.map +1 -0
  382. package/dist/src/planner/nodes/fanout-lookup-join-node.d.ts.map +1 -1
  383. package/dist/src/planner/nodes/fanout-lookup-join-node.js +11 -1
  384. package/dist/src/planner/nodes/fanout-lookup-join-node.js.map +1 -1
  385. package/dist/src/planner/nodes/filter.d.ts.map +1 -1
  386. package/dist/src/planner/nodes/filter.js +63 -13
  387. package/dist/src/planner/nodes/filter.js.map +1 -1
  388. package/dist/src/planner/nodes/join-node.d.ts +41 -1
  389. package/dist/src/planner/nodes/join-node.d.ts.map +1 -1
  390. package/dist/src/planner/nodes/join-node.js +78 -8
  391. package/dist/src/planner/nodes/join-node.js.map +1 -1
  392. package/dist/src/planner/nodes/join-utils.d.ts +33 -6
  393. package/dist/src/planner/nodes/join-utils.d.ts.map +1 -1
  394. package/dist/src/planner/nodes/join-utils.js +124 -9
  395. package/dist/src/planner/nodes/join-utils.js.map +1 -1
  396. package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts +104 -0
  397. package/dist/src/planner/nodes/lens-auxiliary-access-node.d.ts.map +1 -0
  398. package/dist/src/planner/nodes/lens-auxiliary-access-node.js +91 -0
  399. package/dist/src/planner/nodes/lens-auxiliary-access-node.js.map +1 -0
  400. package/dist/src/planner/nodes/limit-offset.d.ts.map +1 -1
  401. package/dist/src/planner/nodes/limit-offset.js +4 -5
  402. package/dist/src/planner/nodes/limit-offset.js.map +1 -1
  403. package/dist/src/planner/nodes/materialized-view-nodes.d.ts +69 -0
  404. package/dist/src/planner/nodes/materialized-view-nodes.d.ts.map +1 -0
  405. package/dist/src/planner/nodes/materialized-view-nodes.js +111 -0
  406. package/dist/src/planner/nodes/materialized-view-nodes.js.map +1 -0
  407. package/dist/src/planner/nodes/merge-join-node.d.ts.map +1 -1
  408. package/dist/src/planner/nodes/merge-join-node.js +2 -1
  409. package/dist/src/planner/nodes/merge-join-node.js.map +1 -1
  410. package/dist/src/planner/nodes/ordinal-slice-node.d.ts.map +1 -1
  411. package/dist/src/planner/nodes/ordinal-slice-node.js +2 -0
  412. package/dist/src/planner/nodes/ordinal-slice-node.js.map +1 -1
  413. package/dist/src/planner/nodes/plan-node-type.d.ts +9 -0
  414. package/dist/src/planner/nodes/plan-node-type.d.ts.map +1 -1
  415. package/dist/src/planner/nodes/plan-node-type.js +9 -0
  416. package/dist/src/planner/nodes/plan-node-type.js.map +1 -1
  417. package/dist/src/planner/nodes/plan-node.d.ts +265 -5
  418. package/dist/src/planner/nodes/plan-node.d.ts.map +1 -1
  419. package/dist/src/planner/nodes/plan-node.js.map +1 -1
  420. package/dist/src/planner/nodes/pragma.d.ts +2 -1
  421. package/dist/src/planner/nodes/pragma.d.ts.map +1 -1
  422. package/dist/src/planner/nodes/pragma.js +12 -0
  423. package/dist/src/planner/nodes/pragma.js.map +1 -1
  424. package/dist/src/planner/nodes/project-node.d.ts +14 -1
  425. package/dist/src/planner/nodes/project-node.d.ts.map +1 -1
  426. package/dist/src/planner/nodes/project-node.js +85 -11
  427. package/dist/src/planner/nodes/project-node.js.map +1 -1
  428. package/dist/src/planner/nodes/reference.d.ts.map +1 -1
  429. package/dist/src/planner/nodes/reference.js +62 -27
  430. package/dist/src/planner/nodes/reference.js.map +1 -1
  431. package/dist/src/planner/nodes/retrieve-node.d.ts.map +1 -1
  432. package/dist/src/planner/nodes/retrieve-node.js +7 -0
  433. package/dist/src/planner/nodes/retrieve-node.js.map +1 -1
  434. package/dist/src/planner/nodes/returning-node.d.ts.map +1 -1
  435. package/dist/src/planner/nodes/returning-node.js +10 -3
  436. package/dist/src/planner/nodes/returning-node.js.map +1 -1
  437. package/dist/src/planner/nodes/scalar.d.ts +20 -0
  438. package/dist/src/planner/nodes/scalar.d.ts.map +1 -1
  439. package/dist/src/planner/nodes/scalar.js +71 -14
  440. package/dist/src/planner/nodes/scalar.js.map +1 -1
  441. package/dist/src/planner/nodes/set-object-tags-node.d.ts +39 -0
  442. package/dist/src/planner/nodes/set-object-tags-node.d.ts.map +1 -0
  443. package/dist/src/planner/nodes/set-object-tags-node.js +41 -0
  444. package/dist/src/planner/nodes/set-object-tags-node.js.map +1 -0
  445. package/dist/src/planner/nodes/set-operation-node.d.ts +123 -1
  446. package/dist/src/planner/nodes/set-operation-node.d.ts.map +1 -1
  447. package/dist/src/planner/nodes/set-operation-node.js +291 -18
  448. package/dist/src/planner/nodes/set-operation-node.js.map +1 -1
  449. package/dist/src/planner/nodes/single-row.d.ts.map +1 -1
  450. package/dist/src/planner/nodes/single-row.js +3 -0
  451. package/dist/src/planner/nodes/single-row.js.map +1 -1
  452. package/dist/src/planner/nodes/sort.d.ts.map +1 -1
  453. package/dist/src/planner/nodes/sort.js +7 -6
  454. package/dist/src/planner/nodes/sort.js.map +1 -1
  455. package/dist/src/planner/nodes/subquery.d.ts +2 -0
  456. package/dist/src/planner/nodes/subquery.d.ts.map +1 -1
  457. package/dist/src/planner/nodes/subquery.js +18 -2
  458. package/dist/src/planner/nodes/subquery.js.map +1 -1
  459. package/dist/src/planner/nodes/table-access-nodes.d.ts.map +1 -1
  460. package/dist/src/planner/nodes/table-access-nodes.js +23 -3
  461. package/dist/src/planner/nodes/table-access-nodes.js.map +1 -1
  462. package/dist/src/planner/nodes/table-function-call.js +6 -0
  463. package/dist/src/planner/nodes/table-function-call.js.map +1 -1
  464. package/dist/src/planner/nodes/values-node.d.ts +1 -0
  465. package/dist/src/planner/nodes/values-node.d.ts.map +1 -1
  466. package/dist/src/planner/nodes/values-node.js +16 -6
  467. package/dist/src/planner/nodes/values-node.js.map +1 -1
  468. package/dist/src/planner/nodes/view-mutation-node.d.ts +259 -0
  469. package/dist/src/planner/nodes/view-mutation-node.d.ts.map +1 -0
  470. package/dist/src/planner/nodes/view-mutation-node.js +273 -0
  471. package/dist/src/planner/nodes/view-mutation-node.js.map +1 -0
  472. package/dist/src/planner/nodes/window-function.d.ts +17 -1
  473. package/dist/src/planner/nodes/window-function.d.ts.map +1 -1
  474. package/dist/src/planner/nodes/window-function.js +15 -1
  475. package/dist/src/planner/nodes/window-function.js.map +1 -1
  476. package/dist/src/planner/nodes/window-node.js +2 -2
  477. package/dist/src/planner/nodes/window-node.js.map +1 -1
  478. package/dist/src/planner/optimizer.d.ts.map +1 -1
  479. package/dist/src/planner/optimizer.js +372 -39
  480. package/dist/src/planner/optimizer.js.map +1 -1
  481. package/dist/src/planner/planning-context.d.ts +1 -1
  482. package/dist/src/planner/planning-context.d.ts.map +1 -1
  483. package/dist/src/planner/rules/access/lens-access-form-matcher.d.ts +70 -0
  484. package/dist/src/planner/rules/access/lens-access-form-matcher.d.ts.map +1 -0
  485. package/dist/src/planner/rules/access/lens-access-form-matcher.js +156 -0
  486. package/dist/src/planner/rules/access/lens-access-form-matcher.js.map +1 -0
  487. package/dist/src/planner/rules/access/rule-lens-auxiliary-access.d.ts +31 -0
  488. package/dist/src/planner/rules/access/rule-lens-auxiliary-access.d.ts.map +1 -0
  489. package/dist/src/planner/rules/access/rule-lens-auxiliary-access.js +176 -0
  490. package/dist/src/planner/rules/access/rule-lens-auxiliary-access.js.map +1 -0
  491. package/dist/src/planner/rules/access/rule-select-access-path.d.ts.map +1 -1
  492. package/dist/src/planner/rules/access/rule-select-access-path.js +435 -37
  493. package/dist/src/planner/rules/access/rule-select-access-path.js.map +1 -1
  494. package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.d.ts.map +1 -1
  495. package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.js +9 -0
  496. package/dist/src/planner/rules/aggregate/rule-groupby-fd-simplification.js.map +1 -1
  497. package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts +39 -0
  498. package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.d.ts.map +1 -0
  499. package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js +616 -0
  500. package/dist/src/planner/rules/cache/rule-materialized-view-rewrite.js.map +1 -0
  501. package/dist/src/planner/rules/cache/rule-scalar-cse.d.ts.map +1 -1
  502. package/dist/src/planner/rules/cache/rule-scalar-cse.js +8 -1
  503. package/dist/src/planner/rules/cache/rule-scalar-cse.js.map +1 -1
  504. package/dist/src/planner/rules/join/equi-pair-extractor.d.ts +36 -0
  505. package/dist/src/planner/rules/join/equi-pair-extractor.d.ts.map +1 -1
  506. package/dist/src/planner/rules/join/equi-pair-extractor.js +38 -1
  507. package/dist/src/planner/rules/join/equi-pair-extractor.js.map +1 -1
  508. package/dist/src/planner/rules/join/rule-fanout-batched-outer.d.ts.map +1 -1
  509. package/dist/src/planner/rules/join/rule-fanout-batched-outer.js +10 -0
  510. package/dist/src/planner/rules/join/rule-fanout-batched-outer.js.map +1 -1
  511. package/dist/src/planner/rules/join/rule-fanout-lookup-join.d.ts.map +1 -1
  512. package/dist/src/planner/rules/join/rule-fanout-lookup-join.js +19 -1
  513. package/dist/src/planner/rules/join/rule-fanout-lookup-join.js.map +1 -1
  514. package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.d.ts +130 -0
  515. package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.d.ts.map +1 -0
  516. package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js +206 -0
  517. package/dist/src/planner/rules/join/rule-inner-join-existence-recovery.js.map +1 -0
  518. package/dist/src/planner/rules/join/rule-join-elimination.d.ts +67 -14
  519. package/dist/src/planner/rules/join/rule-join-elimination.d.ts.map +1 -1
  520. package/dist/src/planner/rules/join/rule-join-elimination.js +81 -25
  521. package/dist/src/planner/rules/join/rule-join-elimination.js.map +1 -1
  522. package/dist/src/planner/rules/join/rule-join-existence-pruning.d.ts +84 -0
  523. package/dist/src/planner/rules/join/rule-join-existence-pruning.d.ts.map +1 -0
  524. package/dist/src/planner/rules/join/rule-join-existence-pruning.js +138 -0
  525. package/dist/src/planner/rules/join/rule-join-existence-pruning.js.map +1 -0
  526. package/dist/src/planner/rules/join/rule-join-greedy-commute.d.ts.map +1 -1
  527. package/dist/src/planner/rules/join/rule-join-greedy-commute.js +9 -1
  528. package/dist/src/planner/rules/join/rule-join-greedy-commute.js.map +1 -1
  529. package/dist/src/planner/rules/join/rule-join-physical-selection.d.ts.map +1 -1
  530. package/dist/src/planner/rules/join/rule-join-physical-selection.js +12 -1
  531. package/dist/src/planner/rules/join/rule-join-physical-selection.js.map +1 -1
  532. package/dist/src/planner/rules/join/rule-lateral-top1-asof.d.ts.map +1 -1
  533. package/dist/src/planner/rules/join/rule-lateral-top1-asof.js +4 -0
  534. package/dist/src/planner/rules/join/rule-lateral-top1-asof.js.map +1 -1
  535. package/dist/src/planner/rules/join/rule-monotonic-merge-join.d.ts.map +1 -1
  536. package/dist/src/planner/rules/join/rule-monotonic-merge-join.js +4 -0
  537. package/dist/src/planner/rules/join/rule-monotonic-merge-join.js.map +1 -1
  538. package/dist/src/planner/rules/join/rule-quickpick-enumeration.d.ts.map +1 -1
  539. package/dist/src/planner/rules/join/rule-quickpick-enumeration.js +10 -0
  540. package/dist/src/planner/rules/join/rule-quickpick-enumeration.js.map +1 -1
  541. package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.d.ts +286 -0
  542. package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.d.ts.map +1 -0
  543. package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.js +548 -0
  544. package/dist/src/planner/rules/join/rule-semijoin-existence-recovery.js.map +1 -0
  545. package/dist/src/planner/rules/parallel/rule-async-gather-union-all.d.ts.map +1 -1
  546. package/dist/src/planner/rules/parallel/rule-async-gather-union-all.js +9 -1
  547. package/dist/src/planner/rules/parallel/rule-async-gather-union-all.js.map +1 -1
  548. package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.d.ts.map +1 -1
  549. package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js +7 -0
  550. package/dist/src/planner/rules/parallel/rule-async-gather-zip-by-key.js.map +1 -1
  551. package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.d.ts.map +1 -1
  552. package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.js +10 -1
  553. package/dist/src/planner/rules/parallel/rule-eager-prefetch-probe.js.map +1 -1
  554. package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.d.ts.map +1 -1
  555. package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.js +9 -0
  556. package/dist/src/planner/rules/predicate/rule-aggregate-predicate-pushdown.js.map +1 -1
  557. package/dist/src/planner/rules/predicate/rule-empty-relation-folding.d.ts.map +1 -1
  558. package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js +18 -0
  559. package/dist/src/planner/rules/predicate/rule-empty-relation-folding.js.map +1 -1
  560. package/dist/src/planner/rules/predicate/rule-filter-contradiction.d.ts.map +1 -1
  561. package/dist/src/planner/rules/predicate/rule-filter-contradiction.js +7 -0
  562. package/dist/src/planner/rules/predicate/rule-filter-contradiction.js.map +1 -1
  563. package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.d.ts.map +1 -1
  564. package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.js +9 -0
  565. package/dist/src/planner/rules/predicate/rule-predicate-inference-equivalence.js.map +1 -1
  566. package/dist/src/planner/rules/predicate/rule-predicate-pushdown.js +13 -3
  567. package/dist/src/planner/rules/predicate/rule-predicate-pushdown.js.map +1 -1
  568. package/dist/src/planner/rules/retrieve/rule-projection-pruning.d.ts.map +1 -1
  569. package/dist/src/planner/rules/retrieve/rule-projection-pruning.js +14 -0
  570. package/dist/src/planner/rules/retrieve/rule-projection-pruning.js.map +1 -1
  571. package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.d.ts +1 -1
  572. package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.js +4 -4
  573. package/dist/src/planner/rules/sort/rule-orderby-fd-pruning.js.map +1 -1
  574. package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.d.ts.map +1 -1
  575. package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js +8 -0
  576. package/dist/src/planner/rules/subquery/rule-anti-join-fk-empty.js.map +1 -1
  577. package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.d.ts.map +1 -1
  578. package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.js +7 -0
  579. package/dist/src/planner/rules/subquery/rule-semi-join-fk-trivial.js.map +1 -1
  580. package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.d.ts.map +1 -1
  581. package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js +12 -0
  582. package/dist/src/planner/rules/subquery/rule-subquery-decorrelation.js.map +1 -1
  583. package/dist/src/planner/type-utils.d.ts +14 -0
  584. package/dist/src/planner/type-utils.d.ts.map +1 -1
  585. package/dist/src/planner/type-utils.js +66 -21
  586. package/dist/src/planner/type-utils.js.map +1 -1
  587. package/dist/src/planner/util/fd-utils.d.ts +177 -43
  588. package/dist/src/planner/util/fd-utils.d.ts.map +1 -1
  589. package/dist/src/planner/util/fd-utils.js +396 -101
  590. package/dist/src/planner/util/fd-utils.js.map +1 -1
  591. package/dist/src/planner/util/ind-utils.d.ts +27 -1
  592. package/dist/src/planner/util/ind-utils.d.ts.map +1 -1
  593. package/dist/src/planner/util/ind-utils.js +80 -6
  594. package/dist/src/planner/util/ind-utils.js.map +1 -1
  595. package/dist/src/planner/util/key-utils.d.ts.map +1 -1
  596. package/dist/src/planner/util/key-utils.js +81 -12
  597. package/dist/src/planner/util/key-utils.js.map +1 -1
  598. package/dist/src/planner/util/set-op-wrapper.d.ts +37 -0
  599. package/dist/src/planner/util/set-op-wrapper.d.ts.map +1 -0
  600. package/dist/src/planner/util/set-op-wrapper.js +82 -0
  601. package/dist/src/planner/util/set-op-wrapper.js.map +1 -0
  602. package/dist/src/planner/validation/plan-validator.d.ts.map +1 -1
  603. package/dist/src/planner/validation/plan-validator.js +1 -0
  604. package/dist/src/planner/validation/plan-validator.js.map +1 -1
  605. package/dist/src/runtime/context-helpers.d.ts +13 -1
  606. package/dist/src/runtime/context-helpers.d.ts.map +1 -1
  607. package/dist/src/runtime/context-helpers.js +7 -1
  608. package/dist/src/runtime/context-helpers.js.map +1 -1
  609. package/dist/src/runtime/delta-executor.d.ts +30 -1
  610. package/dist/src/runtime/delta-executor.d.ts.map +1 -1
  611. package/dist/src/runtime/delta-executor.js +29 -4
  612. package/dist/src/runtime/delta-executor.js.map +1 -1
  613. package/dist/src/runtime/emit/add-constraint.d.ts.map +1 -1
  614. package/dist/src/runtime/emit/add-constraint.js +38 -5
  615. package/dist/src/runtime/emit/add-constraint.js.map +1 -1
  616. package/dist/src/runtime/emit/aggregate.d.ts.map +1 -1
  617. package/dist/src/runtime/emit/aggregate.js +10 -8
  618. package/dist/src/runtime/emit/aggregate.js.map +1 -1
  619. package/dist/src/runtime/emit/alter-table.d.ts +1 -1
  620. package/dist/src/runtime/emit/alter-table.d.ts.map +1 -1
  621. package/dist/src/runtime/emit/alter-table.js +664 -108
  622. package/dist/src/runtime/emit/alter-table.js.map +1 -1
  623. package/dist/src/runtime/emit/analyze.d.ts.map +1 -1
  624. package/dist/src/runtime/emit/analyze.js +2 -1
  625. package/dist/src/runtime/emit/analyze.js.map +1 -1
  626. package/dist/src/runtime/emit/asof-scan.d.ts.map +1 -1
  627. package/dist/src/runtime/emit/asof-scan.js +18 -5
  628. package/dist/src/runtime/emit/asof-scan.js.map +1 -1
  629. package/dist/src/runtime/emit/asserted-keys.d.ts +13 -0
  630. package/dist/src/runtime/emit/asserted-keys.d.ts.map +1 -0
  631. package/dist/src/runtime/emit/asserted-keys.js +13 -0
  632. package/dist/src/runtime/emit/asserted-keys.js.map +1 -0
  633. package/dist/src/runtime/emit/between.d.ts.map +1 -1
  634. package/dist/src/runtime/emit/between.js +24 -19
  635. package/dist/src/runtime/emit/between.js.map +1 -1
  636. package/dist/src/runtime/emit/binary.d.ts.map +1 -1
  637. package/dist/src/runtime/emit/binary.js +5 -9
  638. package/dist/src/runtime/emit/binary.js.map +1 -1
  639. package/dist/src/runtime/emit/block.d.ts.map +1 -1
  640. package/dist/src/runtime/emit/block.js +11 -2
  641. package/dist/src/runtime/emit/block.js.map +1 -1
  642. package/dist/src/runtime/emit/bloom-join.d.ts.map +1 -1
  643. package/dist/src/runtime/emit/bloom-join.js +8 -2
  644. package/dist/src/runtime/emit/bloom-join.js.map +1 -1
  645. package/dist/src/runtime/emit/constraint-check.js +15 -0
  646. package/dist/src/runtime/emit/constraint-check.js.map +1 -1
  647. package/dist/src/runtime/emit/create-table.d.ts.map +1 -1
  648. package/dist/src/runtime/emit/create-table.js +8 -0
  649. package/dist/src/runtime/emit/create-table.js.map +1 -1
  650. package/dist/src/runtime/emit/create-view.d.ts.map +1 -1
  651. package/dist/src/runtime/emit/create-view.js +16 -1
  652. package/dist/src/runtime/emit/create-view.js.map +1 -1
  653. package/dist/src/runtime/emit/dml-executor.d.ts +27 -0
  654. package/dist/src/runtime/emit/dml-executor.d.ts.map +1 -1
  655. package/dist/src/runtime/emit/dml-executor.js +413 -193
  656. package/dist/src/runtime/emit/dml-executor.js.map +1 -1
  657. package/dist/src/runtime/emit/drop-table.d.ts.map +1 -1
  658. package/dist/src/runtime/emit/drop-table.js +10 -0
  659. package/dist/src/runtime/emit/drop-table.js.map +1 -1
  660. package/dist/src/runtime/emit/drop-view.d.ts.map +1 -1
  661. package/dist/src/runtime/emit/drop-view.js +17 -0
  662. package/dist/src/runtime/emit/drop-view.js.map +1 -1
  663. package/dist/src/runtime/emit/envelope-scan.d.ts +13 -0
  664. package/dist/src/runtime/emit/envelope-scan.d.ts.map +1 -0
  665. package/dist/src/runtime/emit/envelope-scan.js +22 -0
  666. package/dist/src/runtime/emit/envelope-scan.js.map +1 -0
  667. package/dist/src/runtime/emit/join.d.ts +10 -2
  668. package/dist/src/runtime/emit/join.d.ts.map +1 -1
  669. package/dist/src/runtime/emit/join.js +128 -38
  670. package/dist/src/runtime/emit/join.js.map +1 -1
  671. package/dist/src/runtime/emit/lens-auxiliary-access.d.ts +16 -0
  672. package/dist/src/runtime/emit/lens-auxiliary-access.d.ts.map +1 -0
  673. package/dist/src/runtime/emit/lens-auxiliary-access.js +16 -0
  674. package/dist/src/runtime/emit/lens-auxiliary-access.js.map +1 -0
  675. package/dist/src/runtime/emit/materialized-view-helpers.d.ts +640 -0
  676. package/dist/src/runtime/emit/materialized-view-helpers.d.ts.map +1 -0
  677. package/dist/src/runtime/emit/materialized-view-helpers.js +2576 -0
  678. package/dist/src/runtime/emit/materialized-view-helpers.js.map +1 -0
  679. package/dist/src/runtime/emit/materialized-view.d.ts +31 -0
  680. package/dist/src/runtime/emit/materialized-view.d.ts.map +1 -0
  681. package/dist/src/runtime/emit/materialized-view.js +187 -0
  682. package/dist/src/runtime/emit/materialized-view.js.map +1 -0
  683. package/dist/src/runtime/emit/merge-join.d.ts.map +1 -1
  684. package/dist/src/runtime/emit/merge-join.js +15 -3
  685. package/dist/src/runtime/emit/merge-join.js.map +1 -1
  686. package/dist/src/runtime/emit/project.d.ts.map +1 -1
  687. package/dist/src/runtime/emit/project.js +10 -5
  688. package/dist/src/runtime/emit/project.js.map +1 -1
  689. package/dist/src/runtime/emit/schema-declarative.d.ts +1 -0
  690. package/dist/src/runtime/emit/schema-declarative.d.ts.map +1 -1
  691. package/dist/src/runtime/emit/schema-declarative.js +101 -5
  692. package/dist/src/runtime/emit/schema-declarative.js.map +1 -1
  693. package/dist/src/runtime/emit/set-object-tags.d.ts +16 -0
  694. package/dist/src/runtime/emit/set-object-tags.d.ts.map +1 -0
  695. package/dist/src/runtime/emit/set-object-tags.js +57 -0
  696. package/dist/src/runtime/emit/set-object-tags.js.map +1 -0
  697. package/dist/src/runtime/emit/set-operation.d.ts.map +1 -1
  698. package/dist/src/runtime/emit/set-operation.js +140 -24
  699. package/dist/src/runtime/emit/set-operation.js.map +1 -1
  700. package/dist/src/runtime/emit/subquery.d.ts.map +1 -1
  701. package/dist/src/runtime/emit/subquery.js +110 -5
  702. package/dist/src/runtime/emit/subquery.js.map +1 -1
  703. package/dist/src/runtime/emit/unary.d.ts.map +1 -1
  704. package/dist/src/runtime/emit/unary.js +34 -6
  705. package/dist/src/runtime/emit/unary.js.map +1 -1
  706. package/dist/src/runtime/emit/view-mutation.d.ts +70 -0
  707. package/dist/src/runtime/emit/view-mutation.d.ts.map +1 -0
  708. package/dist/src/runtime/emit/view-mutation.js +299 -0
  709. package/dist/src/runtime/emit/view-mutation.js.map +1 -0
  710. package/dist/src/runtime/emit/window.js +29 -5
  711. package/dist/src/runtime/emit/window.js.map +1 -1
  712. package/dist/src/runtime/foreign-key-actions.d.ts +66 -3
  713. package/dist/src/runtime/foreign-key-actions.d.ts.map +1 -1
  714. package/dist/src/runtime/foreign-key-actions.js +580 -172
  715. package/dist/src/runtime/foreign-key-actions.js.map +1 -1
  716. package/dist/src/runtime/parallel-driver.d.ts +4 -1
  717. package/dist/src/runtime/parallel-driver.d.ts.map +1 -1
  718. package/dist/src/runtime/parallel-driver.js +5 -1
  719. package/dist/src/runtime/parallel-driver.js.map +1 -1
  720. package/dist/src/runtime/register.d.ts.map +1 -1
  721. package/dist/src/runtime/register.js +17 -1
  722. package/dist/src/runtime/register.js.map +1 -1
  723. package/dist/src/runtime/types.d.ts +10 -0
  724. package/dist/src/runtime/types.d.ts.map +1 -1
  725. package/dist/src/runtime/types.js.map +1 -1
  726. package/dist/src/schema/basis-backfill.d.ts +63 -0
  727. package/dist/src/schema/basis-backfill.d.ts.map +1 -0
  728. package/dist/src/schema/basis-backfill.js +161 -0
  729. package/dist/src/schema/basis-backfill.js.map +1 -0
  730. package/dist/src/schema/catalog.d.ts +115 -1
  731. package/dist/src/schema/catalog.d.ts.map +1 -1
  732. package/dist/src/schema/catalog.js +249 -22
  733. package/dist/src/schema/catalog.js.map +1 -1
  734. package/dist/src/schema/change-events.d.ts +42 -1
  735. package/dist/src/schema/change-events.d.ts.map +1 -1
  736. package/dist/src/schema/change-events.js.map +1 -1
  737. package/dist/src/schema/column.d.ts +16 -0
  738. package/dist/src/schema/column.d.ts.map +1 -1
  739. package/dist/src/schema/column.js.map +1 -1
  740. package/dist/src/schema/constraint-builder.d.ts +182 -0
  741. package/dist/src/schema/constraint-builder.d.ts.map +1 -0
  742. package/dist/src/schema/constraint-builder.js +424 -0
  743. package/dist/src/schema/constraint-builder.js.map +1 -0
  744. package/dist/src/schema/ddl-generator.d.ts +86 -1
  745. package/dist/src/schema/ddl-generator.d.ts.map +1 -1
  746. package/dist/src/schema/ddl-generator.js +316 -20
  747. package/dist/src/schema/ddl-generator.js.map +1 -1
  748. package/dist/src/schema/declared-schema-manager.d.ts +51 -0
  749. package/dist/src/schema/declared-schema-manager.d.ts.map +1 -1
  750. package/dist/src/schema/declared-schema-manager.js +61 -0
  751. package/dist/src/schema/declared-schema-manager.js.map +1 -1
  752. package/dist/src/schema/derivation.d.ts +106 -0
  753. package/dist/src/schema/derivation.d.ts.map +1 -0
  754. package/dist/src/schema/derivation.js +25 -0
  755. package/dist/src/schema/derivation.js.map +1 -0
  756. package/dist/src/schema/function.d.ts +13 -0
  757. package/dist/src/schema/function.d.ts.map +1 -1
  758. package/dist/src/schema/function.js.map +1 -1
  759. package/dist/src/schema/lens-ack.d.ts +90 -0
  760. package/dist/src/schema/lens-ack.d.ts.map +1 -0
  761. package/dist/src/schema/lens-ack.js +361 -0
  762. package/dist/src/schema/lens-ack.js.map +1 -0
  763. package/dist/src/schema/lens-compiler.d.ts +62 -0
  764. package/dist/src/schema/lens-compiler.d.ts.map +1 -0
  765. package/dist/src/schema/lens-compiler.js +1594 -0
  766. package/dist/src/schema/lens-compiler.js.map +1 -0
  767. package/dist/src/schema/lens-fk-discovery.d.ts +175 -0
  768. package/dist/src/schema/lens-fk-discovery.d.ts.map +1 -0
  769. package/dist/src/schema/lens-fk-discovery.js +336 -0
  770. package/dist/src/schema/lens-fk-discovery.js.map +1 -0
  771. package/dist/src/schema/lens-prover.d.ts +336 -0
  772. package/dist/src/schema/lens-prover.d.ts.map +1 -0
  773. package/dist/src/schema/lens-prover.js +1988 -0
  774. package/dist/src/schema/lens-prover.js.map +1 -0
  775. package/dist/src/schema/lens.d.ts +254 -0
  776. package/dist/src/schema/lens.d.ts.map +1 -0
  777. package/dist/src/schema/lens.js +21 -0
  778. package/dist/src/schema/lens.js.map +1 -0
  779. package/dist/src/schema/manager.d.ts +676 -18
  780. package/dist/src/schema/manager.d.ts.map +1 -1
  781. package/dist/src/schema/manager.js +1573 -238
  782. package/dist/src/schema/manager.js.map +1 -1
  783. package/dist/src/schema/mapping-advertisement-tags.d.ts +39 -0
  784. package/dist/src/schema/mapping-advertisement-tags.d.ts.map +1 -0
  785. package/dist/src/schema/mapping-advertisement-tags.js +216 -0
  786. package/dist/src/schema/mapping-advertisement-tags.js.map +1 -0
  787. package/dist/src/schema/rename-rewriter.d.ts +45 -4
  788. package/dist/src/schema/rename-rewriter.d.ts.map +1 -1
  789. package/dist/src/schema/rename-rewriter.js +412 -19
  790. package/dist/src/schema/rename-rewriter.js.map +1 -1
  791. package/dist/src/schema/reserved-tags-policy.d.ts +32 -0
  792. package/dist/src/schema/reserved-tags-policy.d.ts.map +1 -0
  793. package/dist/src/schema/reserved-tags-policy.js +34 -0
  794. package/dist/src/schema/reserved-tags-policy.js.map +1 -0
  795. package/dist/src/schema/reserved-tags.d.ts +170 -0
  796. package/dist/src/schema/reserved-tags.d.ts.map +1 -0
  797. package/dist/src/schema/reserved-tags.js +507 -0
  798. package/dist/src/schema/reserved-tags.js.map +1 -0
  799. package/dist/src/schema/schema-differ.d.ts +158 -2
  800. package/dist/src/schema/schema-differ.d.ts.map +1 -1
  801. package/dist/src/schema/schema-differ.js +1460 -78
  802. package/dist/src/schema/schema-differ.js.map +1 -1
  803. package/dist/src/schema/schema-hasher.d.ts +8 -3
  804. package/dist/src/schema/schema-hasher.d.ts.map +1 -1
  805. package/dist/src/schema/schema-hasher.js +22 -2
  806. package/dist/src/schema/schema-hasher.js.map +1 -1
  807. package/dist/src/schema/schema.d.ts +25 -1
  808. package/dist/src/schema/schema.d.ts.map +1 -1
  809. package/dist/src/schema/schema.js +36 -2
  810. package/dist/src/schema/schema.js.map +1 -1
  811. package/dist/src/schema/table.d.ts +259 -10
  812. package/dist/src/schema/table.d.ts.map +1 -1
  813. package/dist/src/schema/table.js +309 -26
  814. package/dist/src/schema/table.js.map +1 -1
  815. package/dist/src/schema/unique-enforcement.d.ts +78 -0
  816. package/dist/src/schema/unique-enforcement.d.ts.map +1 -0
  817. package/dist/src/schema/unique-enforcement.js +93 -0
  818. package/dist/src/schema/unique-enforcement.js.map +1 -0
  819. package/dist/src/schema/view.d.ts +83 -2
  820. package/dist/src/schema/view.d.ts.map +1 -1
  821. package/dist/src/schema/view.js +67 -1
  822. package/dist/src/schema/view.js.map +1 -1
  823. package/dist/src/schema/window-function.d.ts +9 -1
  824. package/dist/src/schema/window-function.d.ts.map +1 -1
  825. package/dist/src/schema/window-function.js.map +1 -1
  826. package/dist/src/util/comparison.d.ts +24 -0
  827. package/dist/src/util/comparison.d.ts.map +1 -1
  828. package/dist/src/util/comparison.js +34 -0
  829. package/dist/src/util/comparison.js.map +1 -1
  830. package/dist/src/util/mutation-statement.d.ts.map +1 -1
  831. package/dist/src/util/mutation-statement.js +4 -1
  832. package/dist/src/util/mutation-statement.js.map +1 -1
  833. package/dist/src/util/serialization.d.ts +9 -0
  834. package/dist/src/util/serialization.d.ts.map +1 -1
  835. package/dist/src/util/serialization.js +26 -0
  836. package/dist/src/util/serialization.js.map +1 -1
  837. package/dist/src/vtab/backing-host.d.ts +286 -0
  838. package/dist/src/vtab/backing-host.d.ts.map +1 -0
  839. package/dist/src/vtab/backing-host.js +118 -0
  840. package/dist/src/vtab/backing-host.js.map +1 -0
  841. package/dist/src/vtab/best-access-plan.d.ts +21 -0
  842. package/dist/src/vtab/best-access-plan.d.ts.map +1 -1
  843. package/dist/src/vtab/best-access-plan.js.map +1 -1
  844. package/dist/src/vtab/capabilities.d.ts +5 -5
  845. package/dist/src/vtab/capabilities.d.ts.map +1 -1
  846. package/dist/src/vtab/mapping-advertisement.d.ts +163 -0
  847. package/dist/src/vtab/mapping-advertisement.d.ts.map +1 -0
  848. package/dist/src/vtab/mapping-advertisement.js +2 -0
  849. package/dist/src/vtab/mapping-advertisement.js.map +1 -0
  850. package/dist/src/vtab/memory/index.d.ts +64 -4
  851. package/dist/src/vtab/memory/index.d.ts.map +1 -1
  852. package/dist/src/vtab/memory/index.js +119 -12
  853. package/dist/src/vtab/memory/index.js.map +1 -1
  854. package/dist/src/vtab/memory/layer/base.d.ts +38 -1
  855. package/dist/src/vtab/memory/layer/base.d.ts.map +1 -1
  856. package/dist/src/vtab/memory/layer/base.js +112 -24
  857. package/dist/src/vtab/memory/layer/base.js.map +1 -1
  858. package/dist/src/vtab/memory/layer/manager.d.ts +291 -4
  859. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  860. package/dist/src/vtab/memory/layer/manager.js +1050 -91
  861. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  862. package/dist/src/vtab/memory/layer/plan-filter.d.ts.map +1 -1
  863. package/dist/src/vtab/memory/layer/plan-filter.js +35 -6
  864. package/dist/src/vtab/memory/layer/plan-filter.js.map +1 -1
  865. package/dist/src/vtab/memory/layer/scan-layer.d.ts.map +1 -1
  866. package/dist/src/vtab/memory/layer/scan-layer.js +66 -14
  867. package/dist/src/vtab/memory/layer/scan-layer.js.map +1 -1
  868. package/dist/src/vtab/memory/layer/scan-plan.d.ts +14 -0
  869. package/dist/src/vtab/memory/layer/scan-plan.d.ts.map +1 -1
  870. package/dist/src/vtab/memory/layer/scan-plan.js +27 -4
  871. package/dist/src/vtab/memory/layer/scan-plan.js.map +1 -1
  872. package/dist/src/vtab/memory/layer/transaction.d.ts.map +1 -1
  873. package/dist/src/vtab/memory/layer/transaction.js +5 -1
  874. package/dist/src/vtab/memory/layer/transaction.js.map +1 -1
  875. package/dist/src/vtab/memory/module.d.ts +17 -0
  876. package/dist/src/vtab/memory/module.d.ts.map +1 -1
  877. package/dist/src/vtab/memory/module.js +82 -3
  878. package/dist/src/vtab/memory/module.js.map +1 -1
  879. package/dist/src/vtab/memory/table.d.ts.map +1 -1
  880. package/dist/src/vtab/memory/table.js +15 -5
  881. package/dist/src/vtab/memory/table.js.map +1 -1
  882. package/dist/src/vtab/memory/types.d.ts +20 -2
  883. package/dist/src/vtab/memory/types.d.ts.map +1 -1
  884. package/dist/src/vtab/memory/utils/predicate.d.ts.map +1 -1
  885. package/dist/src/vtab/memory/utils/predicate.js +46 -24
  886. package/dist/src/vtab/memory/utils/predicate.js.map +1 -1
  887. package/dist/src/vtab/memory/utils/primary-key-encode.d.ts +31 -0
  888. package/dist/src/vtab/memory/utils/primary-key-encode.d.ts.map +1 -0
  889. package/dist/src/vtab/memory/utils/primary-key-encode.js +101 -0
  890. package/dist/src/vtab/memory/utils/primary-key-encode.js.map +1 -0
  891. package/dist/src/vtab/memory/utils/primary-key.d.ts +8 -0
  892. package/dist/src/vtab/memory/utils/primary-key.d.ts.map +1 -1
  893. package/dist/src/vtab/memory/utils/primary-key.js +12 -5
  894. package/dist/src/vtab/memory/utils/primary-key.js.map +1 -1
  895. package/dist/src/vtab/module.d.ts +203 -4
  896. package/dist/src/vtab/module.d.ts.map +1 -1
  897. package/dist/src/vtab/table.d.ts +9 -0
  898. package/dist/src/vtab/table.d.ts.map +1 -1
  899. package/dist/src/vtab/table.js.map +1 -1
  900. package/package.json +6 -5
@@ -0,0 +1,2576 @@
1
+ import { QuereusError } from '../../common/errors.js';
2
+ import { StatusCode } from '../../common/types.js';
3
+ import { astToString, expressionToString, viewDefinitionToCanonicalString } from '../../emit/ast-stringify.js';
4
+ import { TableReferenceNode, ColumnReferenceNode } from '../../planner/nodes/reference.js';
5
+ import { Parser } from '../../parser/parser.js';
6
+ import { keysOf } from '../../planner/util/fd-utils.js';
7
+ import { proveCoverage } from '../../planner/analysis/coverage-prover.js';
8
+ import { deriveCoarsenedBackingKey } from '../../planner/analysis/coarsened-key.js';
9
+ import { buildColumnIndexMap, requireVtabModule, RowOpFlag } from '../../schema/table.js';
10
+ import { validateChecksOverExistingRows, validateForeignKeyOverExistingRows, maintainedTableCheckViolationError, maintainedTableFkViolationError, formatKeyValue, } from '../../schema/constraint-builder.js';
11
+ import { computeBodyHash } from '../../schema/view.js';
12
+ import { isMaintainedTable } from '../../schema/derivation.js';
13
+ import { renameTableInAst, renameColumnInAst } from '../../schema/rename-rewriter.js';
14
+ import { createLogger } from '../../common/logger.js';
15
+ import { compareSqlValues } from '../../util/comparison.js';
16
+ const log = createLogger('runtime:emit:materialized-view');
17
+ const warnLog = log.extend('warn');
18
+ // Canonical body-hash lives next to the MV schema definition so the declarative
19
+ // differ can share it without depending on the runtime layer. Re-exported here
20
+ // for the create/refresh emitters that already import from this module.
21
+ export { computeBodyHash };
22
+ /**
23
+ * Purpose-built diagnostic for a bag (duplicate-producing) materialized-view
24
+ * body. A v1 materialized view is a *keyed* derived relation: its body must
25
+ * produce a **set** (no duplicate rows under the backing-table key). This
26
+ * replaces the raw `UNIQUE constraint failed: <backing table> PK` message —
27
+ * which named a hidden implementation detail — with one that names the MV and
28
+ * explains the contract. Raised at create (loud, immediate) or at the next
29
+ * refresh if a duplicate-free body later becomes duplicate-producing.
30
+ */
31
+ export function materializedViewNotASetError(schemaName, viewName) {
32
+ return new QuereusError(`materialized view '${schemaName}.${viewName}' body produces duplicate rows, `
33
+ + `but a materialized view must be a set: its body needs a unique key. `
34
+ + `Project the source's primary-key column(s) so every row is unique; for a `
35
+ + `non-keyed result use a plain \`create view\` (live re-evaluation) or `
36
+ + `\`create table ... as <body>\` (a one-off snapshot).`, StatusCode.CONSTRAINT);
37
+ }
38
+ /**
39
+ * Builds + optimizes the materialized-view body and derives the backing table's
40
+ * column list, primary key, body ordering, and source-table dependencies.
41
+ *
42
+ * Columns and types come straight from the optimized relation's
43
+ * {@link RelationalPlanNode.getType}; the PK is the first usable key from
44
+ * `keysOf` (all-columns fallback when none — such an MV is incremental-ineligible
45
+ * until Phase 2). Re-planning here is cheap relative to materialization and keeps
46
+ * the create/refresh emitters free of optimizer plumbing.
47
+ */
48
+ export function deriveBackingShape(db, bodySql, explicitColumns) {
49
+ // Suppress the read-side rewrite: we are computing the MV body to derive/populate
50
+ // its OWN backing, so it must not be rewritten to read that backing.
51
+ return db.schemaManager.withSuppressedMaterializedViewRewrite(() => deriveBackingShapeUnguarded(db, bodySql, explicitColumns));
52
+ }
53
+ function deriveBackingShapeUnguarded(db, bodySql, explicitColumns) {
54
+ const plan = db.getPlan(bodySql);
55
+ const root = plan.getRelations()[0];
56
+ if (!root) {
57
+ throw new QuereusError('materialized view body produced no relation', StatusCode.INTERNAL);
58
+ }
59
+ const relType = root.getType();
60
+ const bodyColumns = relType.columns;
61
+ const names = explicitColumns && explicitColumns.length > 0
62
+ ? explicitColumns
63
+ : bodyColumns.map((c, i) => c.name || `col${i}`);
64
+ const columns = bodyColumns.map((c, i) => {
65
+ const col = {
66
+ name: names[i] ?? `col${i}`,
67
+ logicalType: c.type.logicalType,
68
+ notNull: c.type.nullable === false,
69
+ primaryKey: false,
70
+ pkOrder: 0,
71
+ defaultValue: null,
72
+ collation: c.type.collationName ?? 'BINARY',
73
+ generated: false,
74
+ };
75
+ // Thread the output collation's PROVENANCE into backing-column explicitness:
76
+ // a deliberately-collated output column (an explicit `COLLATE`, or a column
77
+ // whose declared collation flows through unchanged) publishes an EXPLICIT
78
+ // backing collation, so the store module's PK-collation reconcile keeps the
79
+ // backing text PK under the published collation instead of re-keying it under
80
+ // the store default (NOCASE). A 'default'/absent source stays implicit (field
81
+ // left unset — matching ColumnSchema's "absent ⇒ implicit" contract), so a
82
+ // genuinely-implicit MV column preserves the historical store-default keying.
83
+ if (c.type.collationSource === 'explicit' || c.type.collationSource === 'declared') {
84
+ col.collationExplicit = true;
85
+ }
86
+ return col;
87
+ });
88
+ // First usable key from the unified surface. A keyless body is then offered the
89
+ // coarsened lineage key (the parallel-migration shape — see coarsened-key.ts):
90
+ // the projected source key, keyed under the OUTPUT collations, so create-fill
91
+ // rejects collisions loudly and steady-state maintenance merges them LWW. The
92
+ // all-columns fallback remains for bodies with neither (rejected at
93
+ // registration as a bag, exactly as before).
94
+ const keys = keysOf(root);
95
+ let pkIndices;
96
+ let coarsenedKey;
97
+ if (keys.length > 0) {
98
+ pkIndices = [...keys[0]];
99
+ }
100
+ else {
101
+ const lineageKey = deriveCoarsenedBackingKey(root);
102
+ if (lineageKey) {
103
+ pkIndices = [...lineageKey.keyIndices];
104
+ // Only a genuinely COARSENING key carries the warning payload; an
105
+ // equal/refining lineage key is a true unique key accepted silently.
106
+ if (lineageKey.coarsens)
107
+ coarsenedKey = buildCoarsenedKeyInfo(lineageKey, columns);
108
+ }
109
+ else {
110
+ pkIndices = columns.map((_c, i) => i);
111
+ }
112
+ }
113
+ const primaryKey = pkIndices.map(idx => ({ index: idx, desc: false }));
114
+ // A COARSENING key must be the backing's physical key EXACTLY: the loud
115
+ // create-fill and the LWW merge both rest on the backing btree equating
116
+ // colliding source keys, and the ordering-seeded physical PK
117
+ // (computeBackingPrimaryKey leads with the body's `order by` columns) would
118
+ // widen uniqueness past K' — colliding siblings would then coexist silently,
119
+ // defeating both. So drop the ordering seed for a coarsened key; the only
120
+ // cost is the clustering optimization (`mv.ordering` is informational). A
121
+ // non-coarsening lineage key is a true key, so the seed stays uniqueness-
122
+ // preserving there, exactly as for a `keysOf`-proved key.
123
+ const ordering = coarsenedKey
124
+ ? undefined
125
+ : root.physical?.ordering?.map(o => ({ index: o.column, desc: o.desc }));
126
+ return {
127
+ columns,
128
+ primaryKey,
129
+ ordering: ordering && ordering.length > 0 ? ordering : undefined,
130
+ sourceTables: collectSourceTables(plan),
131
+ coarsenedKey,
132
+ allProvedKeys: keys.length > 0 ? keys.map(k => Array.from(k)) : undefined,
133
+ };
134
+ }
135
+ /** Lift the structural {@link CoarsenedBackingKey} into the named, record-facing
136
+ * {@link CoarsenedKeyInfo} (backing column names instead of indices). */
137
+ function buildCoarsenedKeyInfo(key, columns) {
138
+ const nameOf = (idx) => columns[idx]?.name ?? `col${idx}`;
139
+ return {
140
+ columns: key.keyIndices.map(nameOf),
141
+ weakened: key.columns
142
+ .filter(c => c.coarsens)
143
+ .map(c => ({
144
+ column: nameOf(c.outputIndex),
145
+ sourceCollation: c.sourceCollation,
146
+ outputCollation: c.outputCollation,
147
+ })),
148
+ };
149
+ }
150
+ /** Walks the plan collecting qualified (lowercased) names of every base table referenced. */
151
+ function collectSourceTables(plan) {
152
+ const out = new Set();
153
+ const visited = new Set();
154
+ const walk = (node) => {
155
+ if (visited.has(node))
156
+ return;
157
+ visited.add(node);
158
+ if (node instanceof TableReferenceNode) {
159
+ out.add(`${node.tableSchema.schemaName}.${node.tableSchema.name}`.toLowerCase());
160
+ }
161
+ for (const c of node.getChildren())
162
+ walk(c);
163
+ for (const r of node.getRelations())
164
+ walk(r);
165
+ };
166
+ walk(plan);
167
+ return [...out];
168
+ }
169
+ /**
170
+ * Computes the backing table's *physical* primary key. When the body carries an
171
+ * `order by`, the ordering columns lead the key so the btree clusters (and scans)
172
+ * in the body's order — "seeding the backing-table ordering" — with the logical
173
+ * key (from `keysOf`) appended as a uniqueness-preserving tiebreaker. Without an
174
+ * `order by`, the physical key is just the logical key.
175
+ *
176
+ * NOTE: this diverges from `TableDerivation.logicalKey`, which keeps the
177
+ * logical `keysOf` identity. The covering ticket replaces this seeding with a
178
+ * proper materialized index.
179
+ */
180
+ export function computeBackingPrimaryKey(shape) {
181
+ if (!shape.ordering || shape.ordering.length === 0) {
182
+ return shape.primaryKey;
183
+ }
184
+ const seeded = [];
185
+ const seen = new Set();
186
+ for (const o of shape.ordering) {
187
+ if (!seen.has(o.index)) {
188
+ seeded.push({ index: o.index, desc: o.desc });
189
+ seen.add(o.index);
190
+ }
191
+ }
192
+ for (const k of shape.primaryKey) {
193
+ if (!seen.has(k.index)) {
194
+ seeded.push({ index: k.index, desc: k.desc });
195
+ seen.add(k.index);
196
+ }
197
+ }
198
+ return seeded.length > 0 ? seeded : shape.primaryKey;
199
+ }
200
+ /**
201
+ * Constructs the backing-table {@link TableSchema} for a materialized view from a
202
+ * derived {@link BackingShape}, hosted in `moduleName` (default `'memory'`).
203
+ * The capability check here is defense-in-depth — the create builder already
204
+ * gates, but the catalog-import path reaches this without it.
205
+ */
206
+ export function buildBackingTableSchema(db, schemaName, backingTableName, shape, moduleName, moduleArgs,
207
+ /** Table-level metadata tags (the MV's `with tags (…)` — top-level on the unified record). */
208
+ tags) {
209
+ const resolvedModuleName = moduleName ?? 'memory';
210
+ const moduleInfo = db.schemaManager.getModule(resolvedModuleName);
211
+ if (!moduleInfo || !moduleInfo.module) {
212
+ throw new QuereusError(`no virtual table module named '${resolvedModuleName}'`, StatusCode.ERROR);
213
+ }
214
+ if (!moduleInfo.module.getBackingHost) {
215
+ throw new QuereusError(`module '${resolvedModuleName}' cannot host a materialized-view backing table (it does not implement the backing-host capability)`, StatusCode.UNSUPPORTED);
216
+ }
217
+ const backingPk = computeBackingPrimaryKey(shape);
218
+ const pkDefinition = backingPk.map(pk => ({
219
+ index: pk.index,
220
+ desc: pk.desc,
221
+ collation: shape.columns[pk.index]?.collation,
222
+ }));
223
+ // Reflect the physical PK in the column flags (cosmetic; the memory table reads
224
+ // `primaryKeyDefinition`, but catalog/introspection consults column flags).
225
+ backingPk.forEach((pk, order) => {
226
+ const col = shape.columns[pk.index];
227
+ if (col) {
228
+ col.primaryKey = true;
229
+ col.pkOrder = order + 1;
230
+ }
231
+ });
232
+ return {
233
+ name: backingTableName,
234
+ schemaName,
235
+ columns: Object.freeze(shape.columns),
236
+ columnIndexMap: buildColumnIndexMap(shape.columns),
237
+ primaryKeyDefinition: Object.freeze(pkDefinition),
238
+ checkConstraints: Object.freeze([]),
239
+ vtabModule: moduleInfo.module,
240
+ vtabModuleName: resolvedModuleName,
241
+ vtabArgs: moduleArgs ? { ...moduleArgs } : {},
242
+ vtabAuxData: moduleInfo.auxData,
243
+ isView: false,
244
+ estimatedRows: 0,
245
+ tags: tags && Object.keys(tags).length > 0 ? tags : undefined,
246
+ };
247
+ }
248
+ /** Runs the body to completion and returns its rows (raw `Row` arrays). Uses the
249
+ * no-transaction-management primitive — the caller is already inside DDL execution. */
250
+ export async function collectBodyRows(db, bodySql) {
251
+ // Suppress the read-side rewrite for the whole prepare+iterate: this body is run
252
+ // to (re)compute the MV's OWN backing (create fill / refresh rebuild), so it must
253
+ // recompute from the source, never read the backing it is populating.
254
+ return db.schemaManager.withSuppressedMaterializedViewRewriteAsync(async () => {
255
+ const stmt = db.prepare(bodySql);
256
+ try {
257
+ const rows = [];
258
+ for await (const row of stmt._iterateRowsRaw()) {
259
+ rows.push(row);
260
+ }
261
+ return rows;
262
+ }
263
+ finally {
264
+ await stmt.finalize();
265
+ }
266
+ });
267
+ }
268
+ /**
269
+ * Throws the sited declared-column-arity diagnostic when `def`'s explicit column
270
+ * list disagrees with the body's output arity. Build-time creation already
271
+ * validated this (with a build-located diagnostic); this guards the import path —
272
+ * both the refill arm ({@link materializeView}) and the adopt gate check
273
+ * (`SchemaManager.tryAdoptPreExistingBacking`, which must raise it BEFORE the
274
+ * caller drops a durable backing: the entry can never materialize, so dropping
275
+ * would destroy rows for nothing). The refresh path deliberately does NOT share
276
+ * this — it reaches a legitimate mismatch after a source ALTER and has its own
277
+ * "drop and recreate" diagnostic.
278
+ */
279
+ export function assertDeclaredColumnArity(def, shape) {
280
+ if (def.columns && def.columns.length > 0 && def.columns.length !== shape.columns.length) {
281
+ throw new QuereusError(`materialized view '${def.schemaName}.${def.viewName}' has ${def.columns.length} declared columns but body produces ${shape.columns.length}`, StatusCode.ERROR);
282
+ }
283
+ }
284
+ /**
285
+ * Builds the {@link TableDerivation} record for `def` over the derived
286
+ * `shape` — the single record formula shared by {@link materializeView} (refill)
287
+ * and {@link adoptMaterializedView} (adopt), so the two paths cannot drift: an
288
+ * adopted and a refilled maintained table are indistinguishable (fixed point:
289
+ * export DDL after adopt == after refill).
290
+ *
291
+ * `bodyHash` hashes the canonical DEFINITION (explicit columns + body — the body
292
+ * string carries any trailing `with defaults (…)` clause), NOT the executable
293
+ * bodySql — the declarative differ recomputes the same form from a declared MV,
294
+ * so a defaults-only or explicit-columns-only change is detected as drift.
295
+ * `def.bodySql` is the full body render (it carries the inert trailing
296
+ * `with defaults (…)` clause, which the read planner ignores — defaults are
297
+ * realized only in the view write-through rewrite): it feeds execution
298
+ * (collectBodyRows / deriveBackingShape / linkCoveredUniqueConstraints).
299
+ */
300
+ function buildTableDerivation(def, shape) {
301
+ return {
302
+ selectAst: def.selectAst,
303
+ columns: def.columns,
304
+ logicalKey: shape.primaryKey,
305
+ coarsenedKey: shape.coarsenedKey,
306
+ bodyHash: computeBodyHash(viewDefinitionToCanonicalString(def.columns, def.selectAst)),
307
+ ordering: shape.ordering,
308
+ sourceTables: shape.sourceTables,
309
+ stale: false,
310
+ };
311
+ }
312
+ /**
313
+ * Rejects a body that references the maintained table being created. The
314
+ * unified model makes self-reference *lexically* possible mid-create (the
315
+ * table registers under the MV's own name before the fill runs), so the
316
+ * create/import paths reject it up front — a self-referential derivation can
317
+ * never be maintained coherently.
318
+ */
319
+ function assertNoSelfReference(def, shape) {
320
+ const self = `${def.schemaName}.${def.viewName}`.toLowerCase();
321
+ if (shape.sourceTables.includes(self)) {
322
+ throw new QuereusError(`materialized view '${def.schemaName}.${def.viewName}' body may not reference the view itself`, StatusCode.ERROR);
323
+ }
324
+ }
325
+ /**
326
+ * The key-coarsening warning `docs/migration.md` § Convergence hazards
327
+ * specifies — emitted (structured logger, `warn` channel) when an MV
328
+ * materializes over a coarsened backing key, with `TableDerivation.coarsenedKey`
329
+ * as the record-side complement. Warn, don't reject: the merge-on-coarsen
330
+ * behavior is often exactly what the migration intends.
331
+ */
332
+ function warnKeyCoarsening(schemaName, viewName, info) {
333
+ const detail = info.weakened
334
+ .map(w => `${w.column}: collation ${w.sourceCollation} → ${w.outputCollation}`)
335
+ .join(', ');
336
+ warnLog(`materialized view '%s.%s': backing key (%s) is coarser than the source primary key (%s); `
337
+ + `colliding source rows will last-write-win until they are merged`, schemaName, viewName, info.columns.join(', '), detail);
338
+ }
339
+ /**
340
+ * The materialize core shared by `emitCreateMaterializedView` and the
341
+ * catalog-import path (`SchemaManager.importMaterializedView`): derive the
342
+ * backing shape from the planned body → create the maintained table under the
343
+ * MV's own name in the declared backing-host module (memory default) → fill it
344
+ * from the body → attach the {@link TableDerivation} → compile + register
345
+ * row-time write-through maintenance. Returns the registered maintained table.
346
+ *
347
+ * Fires `table_added` for the table (it is created like any table) but
348
+ * deliberately does NOT fire `materialized_view_added` — the create emitter
349
+ * notifies after this returns, while import stays silent (a store rehydrating
350
+ * its own catalog must not re-emit persistence events).
351
+ *
352
+ * Rollback-on-throw: a fill failure (including the "must be a set"
353
+ * duplicate-key gate) drops the half-built table; a registration failure (the
354
+ * mandatory row-time eligibility gate runs there) drops the table — derivation
355
+ * and all — either way the schema is left exactly as before the call.
356
+ * Existence/collision checks are the caller's job (the create emitter checks
357
+ * before calling; on import a duplicate surfaces as a table-name conflict).
358
+ *
359
+ * `preDerivedShape` short-circuits the shape derivation for a caller that
360
+ * already planned the body (the import path derives it once for its gates).
361
+ */
362
+ export async function materializeView(db, def, preDerivedShape) {
363
+ const sm = db.schemaManager;
364
+ const shape = preDerivedShape ?? deriveBackingShape(db, def.bodySql, def.columns);
365
+ // Lives here — not in deriveBackingShape — because the refresh path reaches a
366
+ // legitimate mismatch after a source ALTER (see the assert's docstring).
367
+ assertDeclaredColumnArity(def, shape);
368
+ // The table registers under the MV's own name BEFORE the fill runs, so a
369
+ // self-referential body must be rejected up front (it would otherwise read
370
+ // the empty table being populated).
371
+ assertNoSelfReference(def, shape);
372
+ const backingSchema = buildBackingTableSchema(db, def.schemaName, def.viewName, shape, def.backingModuleName, def.backingModuleArgs, def.tags);
373
+ const completeBacking = await sm.createBackingTable(backingSchema);
374
+ try {
375
+ const rows = await collectBodyRows(db, def.bodySql);
376
+ const host = resolveBackingHost(db, completeBacking);
377
+ // `replaceContents` runs NO derived-row constraint validation: this caller's
378
+ // backing is the MV-sugar shape (`buildBackingTableSchema` hard-codes empty
379
+ // checkConstraints and carries no foreignKeys), so there is nothing to
380
+ // validate. The constraint-bearing refresh path that DOES need validation
381
+ // over a `replaceContents`-style whole-set swap runs it in `rebuildBacking`
382
+ // (pending-layer `replace-all` + `validateDeclaredConstraintsOverContents`),
383
+ // not here.
384
+ await host.replaceContents(rows, () => materializedViewNotASetError(def.schemaName, def.viewName));
385
+ }
386
+ catch (e) {
387
+ // Roll back: drop the table, do not attach a derivation.
388
+ try {
389
+ await sm.dropTable(def.schemaName, def.viewName, /*ifExists*/ true);
390
+ }
391
+ catch { /* best-effort cleanup */ }
392
+ throw e;
393
+ }
394
+ const maintained = sm.attachDerivation(def.schemaName, def.viewName, buildTableDerivation(def, shape));
395
+ // Eagerly record the constraint↔structure link if this MV covers a UNIQUE
396
+ // constraint (informational — enforcement still routes through the
397
+ // synchronously-maintained auto-index).
398
+ linkCoveredUniqueConstraints(db, maintained, def.bodySql);
399
+ // Compile + register row-time write-through maintenance. The mandatory
400
+ // eligibility gate runs here (it needs the analyzed body) and throws on a
401
+ // body that is not row-time maintainable — roll the whole MV back so an
402
+ // ineligible body errors cleanly.
403
+ try {
404
+ db.registerMaterializedView(maintained);
405
+ }
406
+ catch (e) {
407
+ unlinkCoveredUniqueConstraints(db, maintained);
408
+ try {
409
+ await sm.dropTable(def.schemaName, def.viewName, /*ifExists*/ true);
410
+ }
411
+ catch { /* best-effort cleanup */ }
412
+ throw e;
413
+ }
414
+ // After the MV fully materialized (a fill/registration failure must error, not
415
+ // warn): surface the key-coarsening hazard the coarsened backing key carries.
416
+ if (maintained.derivation.coarsenedKey) {
417
+ warnKeyCoarsening(def.schemaName, def.viewName, maintained.derivation.coarsenedKey);
418
+ }
419
+ return maintained;
420
+ }
421
+ /**
422
+ * The adopt-without-refill counterpart of {@link materializeView}: the
423
+ * registration tail without create+fill, for the catalog-import path
424
+ * (`SchemaManager.importMaterializedView`) when a pre-existing durable backing
425
+ * passed every adopt gate (same module, shape match, all sources same-module
426
+ * with upstream maintained tables themselves adopted, caller-attested
427
+ * `trustBackings`). The table's rows are trusted as-is — no body execution.
428
+ *
429
+ * **Backing schema re-stamp.** `preExisting` is a phase-1 DDL round-trip and
430
+ * loses ScalarType fidelity the refill path would carry (the registry-interned
431
+ * logical types survive only by name in DDL). Re-registering the body-derived
432
+ * {@link buildBackingTableSchema} result — shape-verified identical by the
433
+ * caller's `backingShapeMatches` gate — makes post-adopt state equivalent to
434
+ * post-refill state for the row-time plan `registerMaterializedView` binds.
435
+ * Module identity/args come from `def` exactly as the refill path's
436
+ * `buildBackingTableSchema` call does (gate 1 verified the registered module
437
+ * matches); `estimatedRows` carries over from the registered schema (the rows
438
+ * are preserved, so the prior estimate stays truthful). The module-side LIVE
439
+ * table instance still caches the phase-1 schema — the importing host
440
+ * reconciles it after import (the store module's `rehydrateCatalog` runs
441
+ * `StoreTable.updateSchema` over every connected table); reads are unaffected
442
+ * either way since the shapes are identical.
443
+ *
444
+ * Rollback on a registration failure (the mandatory row-time eligibility gate
445
+ * runs there): unlink + detach the derivation + rethrow — the table stays
446
+ * REGISTERED, reverting to its plain (derivation-less) state. Dropping a
447
+ * durable backing on a registration error would destroy the very rows a later
448
+ * retry could adopt; the caller records the throw as a per-entry rehydration
449
+ * error.
450
+ */
451
+ export async function adoptMaterializedView(db, def, preExisting, shape) {
452
+ const sm = db.schemaManager;
453
+ const schema = sm.getSchemaOrFail(def.schemaName);
454
+ assertNoSelfReference(def, shape);
455
+ const stamped = buildBackingTableSchema(db, def.schemaName, def.viewName, shape, def.backingModuleName, def.backingModuleArgs, def.tags);
456
+ schema.addTable({ ...stamped, estimatedRows: preExisting.estimatedRows ?? 0 });
457
+ const maintained = sm.attachDerivation(def.schemaName, def.viewName, buildTableDerivation(def, shape));
458
+ linkCoveredUniqueConstraints(db, maintained, def.bodySql);
459
+ try {
460
+ db.registerMaterializedView(maintained);
461
+ }
462
+ catch (e) {
463
+ unlinkCoveredUniqueConstraints(db, maintained);
464
+ // Detach the derivation: the table reverts to a plain table (re-stamped
465
+ // schema is shape-identical to its phase-1 state) — deliberately NOT dropped.
466
+ const { derivation: _derivation, ...plain } = maintained;
467
+ schema.addTable(plain);
468
+ throw e;
469
+ }
470
+ return maintained;
471
+ }
472
+ /* ──────────────── attach / detach lifecycle verbs ────────────────
473
+ * The maintained-table lifecycle verbs: `create table … maintained as <body>`
474
+ * (attach-to-empty), `alter table … set maintained as <body>` (attach /
475
+ * re-attach with verify-by-diff reconcile), and `alter table … drop maintained`
476
+ * (detach). The attach core never trusts existing rows blindly and never refills
477
+ * wholesale: it re-derives the body and reconciles by keyed diff (the
478
+ * 'replace-all' MaintenanceOp), so identical derivable content means ZERO row
479
+ * writes and zero reported changes, while divergence resolves derived-wins with
480
+ * only the genuine per-row changes reported (and cascaded to consumer maintained
481
+ * tables). Blind trust remains the rehydrate fast path's domain, where
482
+ * clean-shutdown attestation gates it (`SchemaManager.tryAdoptPreExistingBacking`). */
483
+ /**
484
+ * Names the first difference between a table's declared/live shape and the
485
+ * derived body `shape` — the attach-time strict shape check (null when the body
486
+ * derives exactly the declared shape). Unlike {@link describeBackingShapeMismatch}
487
+ * (the structural, name-blind refresh check) this one is part of the
488
+ * declared-shape contract and therefore compares column NAMES too: the declared
489
+ * layout is the frozen basis, so the body must be aliased to produce it
490
+ * verbatim — names, types, not-null, collations, and the physical primary key
491
+ * (order, direction, per-component collation). Not-null is exact in BOTH
492
+ * directions: tolerating a body-notNull/declared-nullable skew would make the
493
+ * next refresh's reshape pass "tighten" the declared column, silently mutating
494
+ * the frozen basis.
495
+ *
496
+ * `skipNames` drops the per-column NAME comparison for the `create table …
497
+ * maintained (columns) as` form: there the authored rename list is the
498
+ * authoritative output-name vector (body outputs are renamed positionally to it),
499
+ * so a body whose natural names differ from the declared columns is accepted as a
500
+ * positional rename. Everything else — column count, types, not-null (both ways),
501
+ * collations, and the physical primary key — stays strict.
502
+ */
503
+ function describeAttachShapeMismatch(table, shape, skipNames = false) {
504
+ if (table.columns.length !== shape.columns.length) {
505
+ return `body produces ${shape.columns.length} columns but the table declares ${table.columns.length}`;
506
+ }
507
+ for (let i = 0; i < shape.columns.length; i++) {
508
+ const declared = table.columns[i];
509
+ const derived = shape.columns[i];
510
+ if (!skipNames && declared.name.toLowerCase() !== derived.name.toLowerCase()) {
511
+ return `body output column ${i + 1} is named '${derived.name}' but the table declares '${declared.name}' (alias the body output to match the declared shape)`;
512
+ }
513
+ if (!backingTypeMatches(declared, derived)) {
514
+ return `column '${declared.name}': body derives type ${derived.logicalType.name} but the table declares ${declared.logicalType.name}`;
515
+ }
516
+ if (!backingNotNullMatches(declared, derived)) {
517
+ return `column '${declared.name}': body derives ${derived.notNull ? 'not null' : 'nullable'} but the table declares ${declared.notNull ? 'not null' : 'nullable'}`;
518
+ }
519
+ if (!backingCollationMatches(declared, derived)) {
520
+ return `column '${declared.name}': body derives collation ${derived.collation ?? 'BINARY'} but the table declares ${declared.collation ?? 'BINARY'}`;
521
+ }
522
+ }
523
+ const derivedPk = computeBackingPrimaryKey(shape);
524
+ const declaredPk = table.primaryKeyDefinition;
525
+ if (declaredPk.length !== derivedPk.length) {
526
+ return `body derives a ${derivedPk.length}-column primary key but the table declares ${declaredPk.length} (a body \`order by\` seeds the derived key — see computeBackingPrimaryKey)`;
527
+ }
528
+ for (let k = 0; k < derivedPk.length; k++) {
529
+ const declaredCol = table.columns[declaredPk[k].index];
530
+ const derivedCol = shape.columns[derivedPk[k].index];
531
+ if (declaredPk[k].index !== derivedPk[k].index) {
532
+ return `primary-key component ${k + 1}: body derives '${derivedCol?.name}' but the table declares '${declaredCol?.name}'`;
533
+ }
534
+ if ((declaredPk[k].desc === true) !== (derivedPk[k].desc === true)) {
535
+ return `primary-key component ${k + 1} ('${declaredCol?.name}'): direction differs`;
536
+ }
537
+ const declaredColl = declaredPk[k].collation ?? declaredCol?.collation ?? 'BINARY';
538
+ const derivedColl = derivedCol?.collation ?? 'BINARY';
539
+ if (declaredColl !== derivedColl) {
540
+ return `primary-key component ${k + 1} ('${declaredCol?.name}'): body derives collation ${derivedColl} but the table declares ${declaredColl}`;
541
+ }
542
+ }
543
+ return null;
544
+ }
545
+ /**
546
+ * Rejects an attach whose body would close a derivation cycle. Create-MV can
547
+ * never form one (a consumer is created after its producer), but attach can:
548
+ * `alter table A set maintained as select … from B` where B's derivation
549
+ * (transitively) reads A — including the degenerate self-reference (`… from A`).
550
+ * Walks the sourceTables→derivation edges of the LIVE catalog from the new
551
+ * body's sources; reaching the attach target names the cycle path in the
552
+ * diagnostic. The maintenance cascade's depth guard
553
+ * (`assertCascadeDepth`) stays as defense-in-depth behind this.
554
+ */
555
+ function assertNoDerivationCycle(db, schemaName, tableName, sourceTables) {
556
+ const target = `${schemaName}.${tableName}`.toLowerCase();
557
+ const sm = db.schemaManager;
558
+ const visited = new Set();
559
+ const walk = (qualified, path) => {
560
+ if (qualified === target) {
561
+ // Render in data-flow order, closing the loop on the target. `path` is the
562
+ // derived-from chain outward from the new body (path[0] = a body source,
563
+ // path[last] = the table derived from the target), so data flows
564
+ // target → path[last] → … → path[0] → target.
565
+ const cycle = [target, ...[...path].reverse(), target].join(' → ');
566
+ throw new QuereusError(`cannot attach derivation to '${schemaName}.${tableName}': the body would create a derivation cycle (${cycle})`, StatusCode.ERROR);
567
+ }
568
+ if (visited.has(qualified))
569
+ return;
570
+ visited.add(qualified);
571
+ const dot = qualified.indexOf('.');
572
+ const srcSchema = dot >= 0 ? qualified.slice(0, dot) : 'main';
573
+ const srcName = dot >= 0 ? qualified.slice(dot + 1) : qualified;
574
+ const source = sm.getTable(srcSchema, srcName);
575
+ if (source && isMaintainedTable(source)) {
576
+ for (const next of source.derivation.sourceTables)
577
+ walk(next, [...path, qualified]);
578
+ }
579
+ };
580
+ for (const src of sourceTables)
581
+ walk(src, []);
582
+ }
583
+ /**
584
+ * The loud "must be a set" reject for attach, BEFORE any catalog or data
585
+ * mutation: the keyed reconcile diff would otherwise last-write-win duplicate
586
+ * derived keys silently. Collation-aware pairing — duplicates are detected
587
+ * under the backing primary-key collations (the same key identity the
588
+ * 'replace-all' diff uses), so a coarsened-key collision present in the source
589
+ * rejects here, naming the colliding key. `pk` is the SHAPE-derived physical
590
+ * key ({@link computeBackingPrimaryKey} over the derived shape): the rows are
591
+ * indexed by the shape, and under a reshape-on-attach the table's own PK
592
+ * definition may carry pre-reshape column indices. Equivalent to the table's
593
+ * PK whenever the shapes match (the strict attach check verifies index, desc,
594
+ * and collation equality).
595
+ *
596
+ * `onDuplicate` overrides the default attach-time diagnostic with a caller-built
597
+ * one (receiving the rendered colliding key values) — the refresh path threads
598
+ * {@link materializedViewNotASetError} through {@link assertRefreshRowsAreSet} so
599
+ * its constraint-bearing branch rejects duplicates identically to the
600
+ * `replaceContents` fast path, single-sourcing the collation-aware dup detection.
601
+ */
602
+ function assertDerivedRowsAreSet(rows, pk, schemaName, name, onDuplicate) {
603
+ if (rows.length < 2)
604
+ return;
605
+ const compareKeys = (ra, rb) => {
606
+ for (const c of pk) {
607
+ const cmp = compareSqlValues(ra[c.index], rb[c.index], c.collation ?? 'BINARY');
608
+ if (cmp !== 0)
609
+ return cmp;
610
+ }
611
+ return 0;
612
+ };
613
+ const order = rows.map((_r, i) => i).sort((a, b) => compareKeys(rows[a], rows[b]));
614
+ for (let i = 1; i < order.length; i++) {
615
+ if (compareKeys(rows[order[i - 1]], rows[order[i]]) === 0) {
616
+ const keyVals = pk.map(c => formatKeyValue(rows[order[i]][c.index])).join(', ');
617
+ throw onDuplicate?.(keyVals) ?? new QuereusError(`cannot attach derivation to '${schemaName}.${name}': the body produces duplicate rows for primary key (${keyVals}), but a maintained table must be a set — `
618
+ + `project a unique key or merge the colliding source rows first`, StatusCode.CONSTRAINT);
619
+ }
620
+ }
621
+ }
622
+ /**
623
+ * Refresh's duplicate-derived-key reject — the constraint-bearing
624
+ * {@link rebuildBacking} branch's parity with the `replaceContents` fast path,
625
+ * which rejects duplicate backing PKs via {@link materializedViewNotASetError}.
626
+ * `applyMaintenance('replace-all')` would otherwise silently LWW-merge colliding
627
+ * keys, so this raises the IDENTICAL diagnostic BEFORE the pending-layer reconcile,
628
+ * keeping the two refresh branches indistinguishable on duplicate handling.
629
+ * Delegates to {@link assertDerivedRowsAreSet} so the collation-aware detection
630
+ * stays single-sourced.
631
+ */
632
+ function assertRefreshRowsAreSet(rows, pk, schemaName, name) {
633
+ assertDerivedRowsAreSet(rows, pk, schemaName, name, () => materializedViewNotASetError(schemaName, name));
634
+ }
635
+ /**
636
+ * Resolve (or lazily create + register) the table's backing connection for the
637
+ * current transaction — the same discipline as the maintenance manager's
638
+ * `getBackingConnection`, so the reconcile's pending writes ride the
639
+ * coordinated commit in lockstep with the statement, and a `select` from the
640
+ * table inside the same transaction observes them (reads-own-writes).
641
+ */
642
+ async function resolveAttachConnection(db, host, qualifiedName) {
643
+ for (const c of db.getConnectionsForTable(qualifiedName)) {
644
+ if (host.ownsConnection(c))
645
+ return c;
646
+ }
647
+ const conn = host.connect();
648
+ await db.registerConnection(conn);
649
+ return conn;
650
+ }
651
+ /**
652
+ * Whether `mt` declares ≥1 constraint the {@link rebuildBacking} refresh path must
653
+ * validate over the recomputed row set — the same predicate
654
+ * {@link validateDeclaredConstraintsOverContents} gates on: any CHECK whose op-mask
655
+ * intersects INSERT | UPDATE (the derived-row op-mask collapse — a derived row's
656
+ * presence is neither a user INSERT nor UPDATE), or any child-side FK.
657
+ *
658
+ * The FK term is additionally gated on `pragma foreign_keys`: with enforcement off
659
+ * the bulk FK scan no-ops, so an FK-only maintained table keeps the zero-overhead
660
+ * `replaceContents` fast path rather than spinning up a connection for a no-op scan.
661
+ * A table also declaring an applicable CHECK always takes the validating branch
662
+ * regardless of the pragma.
663
+ */
664
+ function hasApplicableConstraints(db, mt) {
665
+ const hasCheck = mt.checkConstraints.some(c => (c.operations & (RowOpFlag.INSERT | RowOpFlag.UPDATE)) !== 0);
666
+ if (hasCheck)
667
+ return true;
668
+ const fks = mt.foreignKeys ?? [];
669
+ return fks.length > 0 && db.options.getBooleanOption('foreign_keys');
670
+ }
671
+ /**
672
+ * Bulk derived-row constraint validation for the attach paths (create-fill and
673
+ * attach/re-attach reconcile): after the `'replace-all'` reconcile lands the
674
+ * derived row set in the connection's pending layer, scan the table's EFFECTIVE
675
+ * (pending-over-committed) contents against every declared CHECK whose op-mask
676
+ * intersects INSERT | UPDATE (the derived-row op-mask collapse — a derived row's
677
+ * presence is neither a user INSERT nor UPDATE, see docs/materialized-views.md)
678
+ * and every declared child-side FK (pragma-gated inside the FK validator,
679
+ * MATCH SIMPLE). Post-reconcile contents are exactly the derived set, so this
680
+ * validates every row the table will hold — which is also why detach can never
681
+ * strand a violator. Zero overhead when nothing is declared (every MV-sugar
682
+ * backing: `buildBackingTableSchema` hard-codes empty constraints).
683
+ *
684
+ * The scan is a plain table read of the backing (a maintained table resolves
685
+ * through the ORDINARY table path in `building/select.ts` — never a
686
+ * re-derivation), observing the pending reconcile writes through the registered
687
+ * attach connection (reads-own-writes). An `old.`/`new.`-qualified CHECK —
688
+ * which this SQL scan could not resolve — was already rejected at registration
689
+ * (`buildDerivedRowValidator`), which runs before this validation on every
690
+ * create/attach path.
691
+ *
692
+ * Declared-constraint folding: the optimizer trusts a declared CHECK / FK as a
693
+ * proven invariant (`ruleFilterContradiction` / `ruleAntiJoinFkEmpty`), and —
694
+ * unlike the ALTER ADD paths — the constraints under validation are already on
695
+ * the LIVE record here. So the live record is swapped for a constraint-stripped
696
+ * clone for the duration of the scans (the ADD COLUMN intermediate-schema
697
+ * discipline, see `runtime/emit/alter-table.ts`), then restored.
698
+ */
699
+ async function validateDeclaredConstraintsOverContents(db, mt) {
700
+ const applicableChecks = mt.checkConstraints.filter(c => (c.operations & (RowOpFlag.INSERT | RowOpFlag.UPDATE)) !== 0);
701
+ const fks = mt.foreignKeys ?? [];
702
+ if (applicableChecks.length === 0 && fks.length === 0)
703
+ return;
704
+ const schema = db.schemaManager.getSchemaOrFail(mt.schemaName);
705
+ const stripped = { ...mt, checkConstraints: Object.freeze([]), foreignKeys: undefined };
706
+ schema.addTable(stripped);
707
+ try {
708
+ await validateChecksOverExistingRows(db, mt, applicableChecks, (check, exprSql) => maintainedTableCheckViolationError(mt.schemaName, mt.name, check.name ?? `_check_${mt.checkConstraints.indexOf(check)}`, exprSql));
709
+ for (const fk of fks) {
710
+ await validateForeignKeyOverExistingRows(db, mt, fk, () => maintainedTableFkViolationError(mt.schemaName, mt.name, fk.name ?? `_fk_${mt.name}`, fk.referencedSchema ?? mt.schemaName, fk.referencedTable));
711
+ }
712
+ }
713
+ finally {
714
+ schema.addTable(mt);
715
+ }
716
+ }
717
+ /**
718
+ * The attach core shared by `alter table … set maintained as` (fresh attach and
719
+ * re-attach) and `create table … maintained as` (attach-to-empty, via
720
+ * {@link createMaintainedTable}): verify-by-diff, never trust, never refill
721
+ * wholesale.
722
+ *
723
+ * Sequence — every gate runs BEFORE any catalog or data mutation:
724
+ * 1. backing-host capability (defense-in-depth; the builders gate with a sited
725
+ * error);
726
+ * 2. derive the body's shape (rewrite-suppressed) and run the STRICT
727
+ * declared-shape check ({@link describeAttachShapeMismatch} — names
728
+ * included);
729
+ * 3. cycle / self-reference check over the live derivation graph;
730
+ * 4. evaluate the body once and reject duplicate derived keys (the loud
731
+ * "must be a set" reject);
732
+ * 5. catalog flip (`attachDerivation`) + maintenance registration — the
733
+ * create-time gates (determinism, keyed-or-coarsened body, full-rebuild
734
+ * size threshold) run inside `registerMaterializedView`, before any row is
735
+ * written; a throw restores the prior record (and the prior plan, on
736
+ * re-attach);
737
+ * 6. reconcile-by-diff: one `'replace-all'` op against the table's effective
738
+ * contents through the backing host — collation-aware pairing,
739
+ * byte-faithful identical-row skip, so identical content writes nothing and
740
+ * divergence resolves derived-wins with the minimal genuine
741
+ * {@link BackingRowChange}s. The writes land in the connection's PENDING
742
+ * state, committing/rolling back in lockstep with the statement;
743
+ * 7. covering links (clear the prior body's, stamp the new body's), cascade
744
+ * the genuine changes to consumer maintained tables, fire
745
+ * `materialized_view_added` (fresh) / `materialized_view_modified`
746
+ * (re-attach) so store catalogs re-persist the canonical table-form DDL,
747
+ * and surface the key-coarsening warning exactly as create does.
748
+ *
749
+ * `recordedColumns` is recorded verbatim as `derivation.columns` (the lossless
750
+ * implicit/explicit signal the persist + import paths already use): the authored
751
+ * column names for the explicit forms — `create table … maintained (columns) as`
752
+ * AND the re-attach verb `set maintained (columns) as` — or `undefined` for the
753
+ * implicit forms — `create table … maintained as` (which reshapes its source on
754
+ * reopen) AND the implicit re-attach verb `set maintained as` (which reshapes to
755
+ * follow the body's natural names). When `positionalRename` is set — every
756
+ * explicit form — the body outputs are renamed positionally to `recordedColumns`
757
+ * and the per-column name check is skipped (the authored list is the authoritative
758
+ * output-name vector); otherwise the strict declared-shape check (names included)
759
+ * applies. `buildTableDerivation` hashes `recordedColumns` into `bodyHash`, so
760
+ * live exec and catalog import of the same canonical DDL agree on both the record
761
+ * and the hash — making attach/create → persist → reopen a fixed point.
762
+ *
763
+ * An explicit list whose arity disagrees with the body raises a sited error (the
764
+ * list-vs-body arity guard) before anything is recorded — `deriveBackingShape`
765
+ * sizes the shape to the body, so a surplus/short list would otherwise persist a
766
+ * miscounted `derivation.columns`.
767
+ *
768
+ * **Reshape-on-attach (`allowReshape`).** The verb path (`set maintained [(cols)]
769
+ * as` — manual AND differ-emitted) passes `allowReshape = true`; create and any
770
+ * non-verb caller pass `false` and keep the strict declared-shape error. Two
771
+ * reshape triggers, both reusing the two-phase splice + restore handlers below:
772
+ *
773
+ * - **Implicit call** (no rename list): on a strict-shape mismatch the backing
774
+ * reshapes in place to follow the body's natural names — the same "the body
775
+ * owns an implicit table's shape" contract the refresh reshape and the implicit
776
+ * table form's reopen honor. Now permitted over a prior-EXPLICIT record too:
777
+ * `set maintained as <body>` over an `(a, b)` table abandons the authored list
778
+ * and relabels the backing to the body's names, recording an implicit
779
+ * derivation (the deliberate "go implicit" re-attach).
780
+ * - **Explicit call** (`positionalRename`): a same-arity output-NAME drift
781
+ * `(a, b) → (a, c)` produces no strict mismatch (names are skipped), yet the
782
+ * backing must be relabeled to the recorded names — classified as a reshape.
783
+ * The derived shape carries the TARGET names, so {@link classifyBackingReshape}
784
+ * emits a pure positional RENAME (`b → c`); a renamed PK output column is
785
+ * matched through the rename map (not a key change). A count/type/PK delta is
786
+ * still the strict error, and a reorder/swap (`(a, b) → (b, a)`) classifies as
787
+ * inexpressible.
788
+ *
789
+ * An inexpressible delta (interleave / physical-PK change, or a host module
790
+ * without `alterTable`) raises {@link inexpressibleReshapeError} with the table
791
+ * untouched. An expressible plan splices around the verify-by-diff reconcile —
792
+ * see the sequencing notes inside.
793
+ */
794
+ export async function attachMaintainedDerivation(db, table, select, recordedColumns, positionalRename = false, allowReshape = false,
795
+ /**
796
+ * When true, a FAILED FRESH attach discards (via {@link VirtualTableModule.discardBackingForAttach})
797
+ * any backing store {@link VirtualTableModule.ensureBackingForAttach} created IN
798
+ * THIS attach. Set by the `set maintained` ATTACH verb ({@link runSetMaintained}),
799
+ * which owns its own backing cleanup. NOT set by `create table … maintained`
800
+ * ({@link createMaintainedTable}) — there the store was created by the prior
801
+ * `createTable(preferBacking)`, and the create path's own `dropTable` cleanup
802
+ * retires it; a discard here would double-drop and strand the catalog entry.
803
+ */
804
+ discardBackingOnFailure = false) {
805
+ const sm = db.schemaManager;
806
+ const schemaName = table.schemaName;
807
+ const name = table.name;
808
+ const schema = sm.getSchemaOrFail(schemaName);
809
+ const module = requireVtabModule(table);
810
+ if (!module.getBackingHost) {
811
+ throw new QuereusError(`cannot attach derivation to '${schemaName}.${name}': module '${table.vtabModuleName}' cannot host a maintained table (it does not implement the backing-host capability)`, StatusCode.UNSUPPORTED);
812
+ }
813
+ const bodySql = astToString(select);
814
+ // With an authored rename list (`maintained (columns)` create form) the body is
815
+ // renamed positionally to it and the name check skipped; otherwise natural output
816
+ // names with the strict declared-shape check (the body must already be aliased to
817
+ // the declared names — the attach verb / implicit-create posture).
818
+ const shape = deriveBackingShape(db, bodySql, positionalRename ? recordedColumns : undefined);
819
+ // Explicit rename-list arity guard. `deriveBackingShape` sizes the shape to the
820
+ // BODY's arity (a surplus rename name is dropped, a missing one padded), so a
821
+ // list whose length disagrees with the body would otherwise record an
822
+ // over/under-counted `derivation.columns` over the backing. The CREATE path
823
+ // catches this via its table-vs-body count check before reaching here (the
824
+ // freshly-created table mirrors the list); on re-attach the existing table can
825
+ // match the body while the list does not, so guard the list-vs-body arity
826
+ // directly. The implicit form (`recordedColumns === undefined`) is exempt.
827
+ if (recordedColumns !== undefined && recordedColumns.length !== shape.columns.length) {
828
+ throw new QuereusError(`cannot attach derivation to '${schemaName}.${name}': the rename list declares ${recordedColumns.length} columns but the body produces ${shape.columns.length}`, StatusCode.ERROR);
829
+ }
830
+ const mismatch = describeAttachShapeMismatch(table, shape, positionalRename);
831
+ // Reshape-on-attach (see the docstring). The verb (`allowReshape`) reshapes the
832
+ // backing in place instead of erroring on a shape change; create and any
833
+ // non-verb caller keep the strict error. Two reshape triggers:
834
+ // - IMPLICIT call (no rename list): a strict mismatch follows the body's
835
+ // natural names — now also over a prior-explicit record (the deliberate
836
+ // "go implicit" re-attach); the explicit forms keep the strict count/type/PK
837
+ // error instead.
838
+ // - EXPLICIT call (`positionalRename`): a same-arity NAME drift `(a,b)→(a,c)`
839
+ // produces NO mismatch under `skipNames`, yet the backing must be relabeled
840
+ // to the recorded names — classify that as a reshape too. The shape carries
841
+ // the TARGET names, so `classifyBackingReshape` emits a pure positional
842
+ // RENAME; a reorder/swap classifies inexpressible (table untouched) and a
843
+ // renamed PK column is matched through the rename map (not a key change).
844
+ const strictMismatchReshape = mismatch !== null && allowReshape && !positionalRename && recordedColumns === undefined;
845
+ const explicitNameDriftReshape = mismatch === null && positionalRename && allowReshape
846
+ && table.columns.some((c, i) => c.name.toLowerCase() !== shape.columns[i].name.toLowerCase());
847
+ let reshapePlan;
848
+ if (mismatch && !strictMismatchReshape) {
849
+ throw new QuereusError(`cannot attach derivation to '${schemaName}.${name}': ${mismatch}`, StatusCode.ERROR);
850
+ }
851
+ if (strictMismatchReshape || explicitNameDriftReshape) {
852
+ if (!module.alterTable) {
853
+ throw inexpressibleReshapeError(schemaName, name, `its backing module '${table.vtabModuleName}' does not support in-place ALTER`);
854
+ }
855
+ const classification = classifyBackingReshape(table, shape);
856
+ if (!classification.expressible) {
857
+ throw inexpressibleReshapeError(schemaName, name, classification.reason);
858
+ }
859
+ reshapePlan = classification.plan;
860
+ }
861
+ assertNoDerivationCycle(db, schemaName, name, shape.sourceTables);
862
+ const rows = await collectBodyRows(db, bodySql);
863
+ // Shape-derived physical key (see assertDerivedRowsAreSet): under a reshape the
864
+ // table's own PK definition may carry pre-reshape indices; equivalent otherwise.
865
+ const shapePk = computeBackingPrimaryKey(shape)
866
+ .map(c => ({ index: c.index, collation: shape.columns[c.index]?.collation }));
867
+ assertDerivedRowsAreSet(rows, shapePk, schemaName, name);
868
+ const def = {
869
+ schemaName,
870
+ viewName: name,
871
+ selectAst: select,
872
+ bodySql,
873
+ // Recorded as authored: declared names for the explicit forms, undefined for
874
+ // the implicit create form — the lossless signal persist + import already use.
875
+ // Any `with defaults (…)` rides inside `select` (→ derivation.selectAst).
876
+ columns: recordedColumns,
877
+ };
878
+ const prior = schema.getTable(name) ?? table;
879
+ const priorMaintained = isMaintainedTable(prior) ? prior : undefined;
880
+ // Undo the catalog flip after a gate/reconcile failure: restore the prior
881
+ // record and, on re-attach, the prior row-time plan (registerMaterializedView
882
+ // released it when registering the new one).
883
+ const restorePrior = () => {
884
+ schema.addTable(prior);
885
+ if (priorMaintained) {
886
+ if (!priorMaintained.derivation.stale) {
887
+ try {
888
+ db.registerMaterializedView(priorMaintained);
889
+ }
890
+ catch (e) {
891
+ // The prior plan registered before, so this should not throw; if it
892
+ // does, fail safe: stale (reads re-validate) beats silently live.
893
+ db.markMaterializedViewStale(priorMaintained);
894
+ log('Re-registering the prior derivation of %s.%s failed during attach rollback; marked stale: %s', schemaName, name, e instanceof Error ? e.message : String(e));
895
+ }
896
+ }
897
+ }
898
+ else {
899
+ db.unregisterMaterializedView(schemaName, name);
900
+ }
901
+ };
902
+ // Defensive-guard input: capture whether the gate-time backing host is absent
903
+ // BEFORE the gate registration below. Resolved against `table` — the pre-reshape
904
+ // catalog record the gate registration also resolves against
905
+ // (`tryResolveBackingHost` keys only on schema+name, never the shape). A module
906
+ // that materializes its host LATE (`getBackingHost` undefined until
907
+ // `ensureBackingForAttach`) reads absent here, so the replicable-determinism gate
908
+ // inside `registerMaterializedView` was skipped; the guard after the late seam
909
+ // re-checks once the now-present host is in hand. See the eager-resolution
910
+ // invariant on `BackingHost.requiresReplicableDerivations`.
911
+ const gateHostAbsent = tryResolveBackingHost(db, table) === undefined;
912
+ const maintained = sm.attachDerivation(schemaName, name, buildTableDerivation(def, shape));
913
+ try {
914
+ // The create-time gates (determinism, keyed-or-coarsened body, relational
915
+ // output, full-rebuild size threshold) run here — identical to create.
916
+ // Under a reshape this registration is a GATE only: the catalog still
917
+ // holds the pre-reshape columns, so the plan it builds may classify into
918
+ // the full-rebuild floor where the final record fits a bounded-delta arm;
919
+ // the post-reshape re-registration below rebuilds the binding plan, and
920
+ // nothing exercises the interim plan inside this DDL statement.
921
+ db.registerMaterializedView(maintained);
922
+ }
923
+ catch (e) {
924
+ restorePrior();
925
+ throw e;
926
+ }
927
+ // Failure restore once the module's live schema has (partially) reshaped:
928
+ // module column ops are NOT transactional, so restoring the PRIOR record would
929
+ // strand a catalog/module divergence. Keep the catalog tracking the module
930
+ // instead — fresh attach: the table reverts to a plain (derivation-less) table
931
+ // at the reshaped schema; re-attach: the prior derivation rides the reshaped
932
+ // backing marked STALE (its body no longer derives this shape — a later
933
+ // refresh reshapes it back). Coherent and re-runnable either way.
934
+ const restoreReshaped = (moduleSchema) => {
935
+ if (priorMaintained) {
936
+ const restored = graftReshapedRecord(moduleSchema, priorMaintained);
937
+ schema.addTable(restored);
938
+ db.markMaterializedViewStale(restored);
939
+ }
940
+ else {
941
+ const { derivation: _derivation, ...plain } = moduleSchema;
942
+ schema.addTable(plain);
943
+ db.unregisterMaterializedView(schemaName, name);
944
+ }
945
+ };
946
+ let live = maintained;
947
+ let changes;
948
+ let current = table;
949
+ let moduleMutated = false;
950
+ let reconcileCommitted = false;
951
+ try {
952
+ if (reshapePlan) {
953
+ // Pre-reconcile structural ops (rename/add/loosen/drop — none throw on
954
+ // data), then re-register the reshaped schema with the new derivation so
955
+ // the reconcile resolves the reshaped backing. Mirrors
956
+ // reshapeBackingInPlace's pre batch; ops address columns by name.
957
+ for (const op of reshapePlan.preReconcileOps) {
958
+ current = await module.alterTable(db, schemaName, name, reshapeOpToChange(op));
959
+ moduleMutated = true;
960
+ }
961
+ live = graftReshapedRecord(current, maintained);
962
+ schema.addTable(live);
963
+ }
964
+ // Materialize the durable backing store the reconcile will write into,
965
+ // BEFORE resolving the host. A module whose `getBackingHost` resolves over a
966
+ // SEPARATE durable store (e.g. lamina) needs the store created here — the
967
+ // attach core only RESOLVES the host, never creates it, and on the
968
+ // non-reshape path there is no other async module call beforehand. Placed
969
+ // AFTER the reshape `preReconcileOps` + `schema.addTable(live)` so `live`
970
+ // carries the reshaped shape and the store is sized to it. A no-op for
971
+ // modules that omit the hook (memory hosts the live table directly).
972
+ await module.ensureBackingForAttach?.(db, schemaName, name, live);
973
+ // Verify-by-diff reconcile against the (possibly reshaped) backing: the
974
+ // re-resolved host keys the 'replace-all' diff by the module's CURRENT
975
+ // physical PK, so a reshape that shifted PK column indices stays aligned.
976
+ const host = resolveBackingHost(db, live);
977
+ // Defensive guard (defense-in-depth — see `tryResolveBackingHost` and the
978
+ // eager-resolution invariant on `BackingHost.requiresReplicableDerivations`).
979
+ // The gate-time host was absent, so the replicable-determinism gate inside
980
+ // `registerMaterializedView` could not run — yet the now-resolved host DEMANDS
981
+ // replicable derivations. A demanding host MUST resolve eagerly (at plan-build
982
+ // time, before this late-backing seam); reaching here means it violated that
983
+ // contract and a non-replicable body would have slipped the gate. Fail loud
984
+ // rather than corrupt peers. The throw is inside this try, so the catch runs
985
+ // `restorePrior()` / `discardBackingForAttach` cleanup and the statement rolls
986
+ // back — the table reverts to ordinary, untouched. INTERNAL because reaching it
987
+ // is a host-author contract violation, not user error. Single-sited after the
988
+ // sole `resolveBackingHost(db, live)`, so it covers the reshape arm too.
989
+ if (gateHostAbsent && host.requiresReplicableDerivations) {
990
+ throw new QuereusError(`cannot attach derivation to '${schemaName}.${name}': its backing host requires `
991
+ + `replicable derivations but did not resolve until after the durable backing was `
992
+ + `materialized, so the replicable-determinism gate could not run. A host that sets `
993
+ + `requiresReplicableDerivations must resolve via getBackingHost at plan-build time `
994
+ + `(before ensureBackingForAttach).`, StatusCode.INTERNAL);
995
+ }
996
+ const conn = await resolveAttachConnection(db, host, `${schemaName}.${name}`);
997
+ changes = await host.applyMaintenance(conn, [{ kind: 'replace-all', rows }]);
998
+ // Declared CHECK / child-side FK over the reconciled (derived) row set —
999
+ // inside this try so a violation restores the prior record; the pending
1000
+ // reconcile writes roll back with the failing statement.
1001
+ await validateDeclaredConstraintsOverContents(db, live);
1002
+ if (reshapePlan && reshapePlan.postReconcileOps.length > 0) {
1003
+ // Data-validating attribute ops (retype / recollate / tighten NOT NULL)
1004
+ // must validate the RECONCILED body rows, not the stale backing — but the
1005
+ // module's alterTable scans COMMITTED contents (memory's alterColumn walks
1006
+ // the base layer) while the reconcile above sits in the connection's
1007
+ // PENDING layer. So commit the reconcile eagerly first (refresh-parity
1008
+ // commit-first semantics — the structural ops above are already
1009
+ // non-transactional, so the reshaping attach is DDL-grade atomicity
1010
+ // regardless; the later coordinated commit no-ops). Then mirror
1011
+ // reshapeBackingInPlace's post batch: re-register the catalog after EACH
1012
+ // op so a mid-batch throw cannot strand catalog/module divergence.
1013
+ await conn.commit();
1014
+ reconcileCommitted = true;
1015
+ for (const op of reshapePlan.postReconcileOps) {
1016
+ current = await module.alterTable(db, schemaName, name, reshapeOpToChange(op));
1017
+ live = graftReshapedRecord(current, maintained);
1018
+ schema.addTable(live);
1019
+ }
1020
+ }
1021
+ if (reshapePlan) {
1022
+ // Final binding: the early registration gated against the pre-reshape
1023
+ // record; re-register (idempotent) so the row-time plan binds the
1024
+ // RESHAPED backing's columns and physical PK.
1025
+ db.registerMaterializedView(live);
1026
+ }
1027
+ }
1028
+ catch (e) {
1029
+ if (reconcileCommitted) {
1030
+ // The reconciled rows are already committed and the catalog tracks the
1031
+ // module per-op — leave the new record in place, stale (reads re-validate;
1032
+ // a refresh applies the remaining attribute reshape).
1033
+ db.markMaterializedViewStale(live);
1034
+ }
1035
+ else if (moduleMutated) {
1036
+ restoreReshaped(current);
1037
+ }
1038
+ else {
1039
+ restorePrior();
1040
+ }
1041
+ // Discard a backing store freshly created by `ensureBackingForAttach` for a
1042
+ // FAILED FRESH attach (no prior derivation): the table reverts to ordinary,
1043
+ // whose storage still holds the pre-attach rows, so the just-created (empty /
1044
+ // rolled-back) store must be dropped — otherwise the module would keep routing
1045
+ // reads to it. A re-attach (priorMaintained) reused the existing store, which
1046
+ // `restorePrior` keeps for the restored prior derivation, so it is NOT
1047
+ // discarded. The reconcile-committed branch keeps its committed store (stale).
1048
+ if (discardBackingOnFailure && !reconcileCommitted && !priorMaintained) {
1049
+ await module.discardBackingForAttach?.(db, schemaName, name);
1050
+ }
1051
+ throw e;
1052
+ }
1053
+ if (priorMaintained)
1054
+ unlinkCoveredUniqueConstraints(db, priorMaintained);
1055
+ linkCoveredUniqueConstraints(db, live, bodySql);
1056
+ if (reshapePlan) {
1057
+ // The table's column SHAPE changed, and the modified-event channel has no
1058
+ // maintenance listener — fire the same single table_modified the refresh
1059
+ // reshape fires, BEFORE the row cascade below, so consumer maintained
1060
+ // tables go stale (and their released plans never receive shape-shifted
1061
+ // rows); cached plans scanning the table directly recompile.
1062
+ sm.getChangeNotifier().notifyChange({
1063
+ type: 'table_modified',
1064
+ schemaName, objectName: name,
1065
+ oldObject: prior, newObject: live,
1066
+ });
1067
+ }
1068
+ // Cascade the GENUINE reconcile changes to consumer maintained tables: the
1069
+ // reconcile wrote this table through the privileged surface, so the DML
1070
+ // boundary never saw the writes. Identical content produced zero changes and
1071
+ // therefore zero dispatch. Full-rebuild consumers defer + drain once,
1072
+ // mirroring the statement flush.
1073
+ if (changes.length > 0) {
1074
+ const base = `${schemaName}.${name}`;
1075
+ const deferred = new Set();
1076
+ for (const change of changes) {
1077
+ await db._maintainRowTimeCoveringStructures(base, change, undefined, deferred);
1078
+ }
1079
+ await db._flushDeferredRebuilds(deferred);
1080
+ }
1081
+ sm.getChangeNotifier().notifyChange(priorMaintained
1082
+ ? {
1083
+ type: 'materialized_view_modified',
1084
+ schemaName, objectName: name,
1085
+ oldObject: priorMaintained, newObject: live,
1086
+ }
1087
+ : {
1088
+ type: 'materialized_view_added',
1089
+ schemaName, objectName: name,
1090
+ newObject: live,
1091
+ });
1092
+ if (live.derivation.coarsenedKey) {
1093
+ warnKeyCoarsening(schemaName, name, live.derivation.coarsenedKey);
1094
+ }
1095
+ return live;
1096
+ }
1097
+ /**
1098
+ * Detach a maintained table's derivation — `alter table … drop maintained`.
1099
+ * Catalog-only: nothing physical changes. The row-time plan is released, the
1100
+ * covering-structure link un-stamped (UNIQUE enforcement falls back to the
1101
+ * auto-index), and the registered record swapped for the same table minus the
1102
+ * derivation — rows, indexes, module identity, and tags all stay; staleness
1103
+ * state lives on the derivation and leaves with it. The table becomes ordinary
1104
+ * and user-writable.
1105
+ *
1106
+ * Fires `materialized_view_removed` ONLY: the maintenance manager releases any
1107
+ * remaining plan, store catalogs delete the persisted maintained entry (a
1108
+ * store-hosted table's plain bundle is already clause-free), and cached
1109
+ * statement plans over the table invalidate (a cached write-through plan
1110
+ * compiled against the old derivation must not survive the flip). Deliberately
1111
+ * NO `table_modified`: the table's shape and rows are unchanged, so consumer
1112
+ * maintained tables reading it stay live — subsequent user writes drive their
1113
+ * maintenance exactly like any source write.
1114
+ */
1115
+ export function detachMaintainedDerivation(db, mv) {
1116
+ const sm = db.schemaManager;
1117
+ const schema = sm.getSchemaOrFail(mv.schemaName);
1118
+ db.unregisterMaterializedView(mv.schemaName, mv.name);
1119
+ unlinkCoveredUniqueConstraints(db, mv);
1120
+ const live = schema.getTable(mv.name);
1121
+ const source = live && isMaintainedTable(live) ? live : mv;
1122
+ const { derivation: _derivation, ...plain } = source;
1123
+ schema.addTable(plain);
1124
+ sm.getChangeNotifier().notifyChange({
1125
+ type: 'materialized_view_removed',
1126
+ schemaName: mv.schemaName,
1127
+ objectName: mv.name,
1128
+ oldObject: source,
1129
+ });
1130
+ return plain;
1131
+ }
1132
+ /**
1133
+ * `create table … maintained as <body>` — the declared-shape authoring form,
1134
+ * executed all-or-nothing:
1135
+ *
1136
+ * - an existing table/view + `if not exists` skips ENTIRELY (never a
1137
+ * half-attach); without it, the standard already-exists error — both before
1138
+ * the body is planned;
1139
+ * - the declared shape is verified against the derived body shape BEFORE any
1140
+ * catalog registration ({@link SchemaManager.buildDeclaredTableSchema} builds
1141
+ * the schema the CREATE would register, without registering it);
1142
+ * - then the table registers through the ordinary `createTable` path (declared
1143
+ * constraints and defaults intact) and the shared {@link attachMaintainedDerivation}
1144
+ * core runs — attach-to-empty: the reconcile diff against an empty table IS
1145
+ * the fill, applied to the connection's pending state so it commits in
1146
+ * lockstep with the statement (no `replaceContents` commit-first caveat);
1147
+ * - any failure past registration (duplicate derived keys, a maintenance gate)
1148
+ * drops the just-created table — the schema is left exactly as before.
1149
+ *
1150
+ * The attach core re-derives the body AFTER the table registers, so a body that
1151
+ * resolves differently once the new name exists (e.g. a same-name reference that
1152
+ * becomes a self-reference) is caught by the cycle check and rolled back.
1153
+ */
1154
+ export async function createMaintainedTable(db, stmt) {
1155
+ const sm = db.schemaManager;
1156
+ const schemaName = stmt.table.schema ? sm.canonicalSchemaName(stmt.table.schema) : sm.getCurrentSchemaName();
1157
+ const name = stmt.table.name;
1158
+ if (sm.getTable(schemaName, name) || sm.getView(schemaName, name)) {
1159
+ if (stmt.ifNotExists)
1160
+ return undefined;
1161
+ throw new QuereusError(`Table ${schemaName}.${name} already exists`, StatusCode.CONSTRAINT, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
1162
+ }
1163
+ // An authored `maintained (columns)` list is the explicit/arity-locked form: the
1164
+ // body is renamed positionally to the declared names (name check skipped) and the
1165
+ // declared names are recorded as `derivation.columns`. No list is the implicit
1166
+ // form: the strict name check applies and `derivation.columns` is undefined, so
1167
+ // the canonical DDL omits the clause and the table reshapes its source on reopen.
1168
+ const list = stmt.maintained.columns;
1169
+ const explicit = list !== undefined && list.length > 0;
1170
+ const declared = sm.buildDeclaredTableSchema(stmt);
1171
+ const recordedColumns = explicit ? declared.columns.map(c => c.name) : undefined;
1172
+ const bodySql = astToString(stmt.maintained.select);
1173
+ const shape = deriveBackingShape(db, bodySql, explicit ? recordedColumns : undefined);
1174
+ const mismatch = describeAttachShapeMismatch(declared, shape, explicit);
1175
+ if (mismatch) {
1176
+ throw new QuereusError(`cannot create maintained table '${schemaName}.${name}': ${mismatch}`, StatusCode.ERROR, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
1177
+ }
1178
+ // `preferBacking = true`: route the create through the module's durable backing
1179
+ // seam (`createBacking?() ?? create()`) so a durable-backing module (lamina)
1180
+ // builds the basis `RowStore` that `resolveBackingHost` → `getBackingHost`
1181
+ // resolves below for the attach-to-empty fill. Without this the table is an
1182
+ // ordinary relational collection with no basis store and the fill throws
1183
+ // `backing host not found`. Memory (no `createBacking`) falls through to `create`.
1184
+ const table = await sm.createTable(stmt, /*preferBacking*/ true);
1185
+ try {
1186
+ return await attachMaintainedDerivation(db, table, stmt.maintained.select, explicit ? table.columns.map(c => c.name) : undefined, explicit);
1187
+ }
1188
+ catch (e) {
1189
+ try {
1190
+ await sm.dropTable(schemaName, name, /*ifExists*/ true);
1191
+ }
1192
+ catch { /* best-effort cleanup */ }
1193
+ throw e;
1194
+ }
1195
+ }
1196
+ /**
1197
+ * Full-rebuild of a maintained table's contents: re-run the body to completion
1198
+ * and swap the table to the recomputed set. The always-correct path the two
1199
+ * `refresh materialized view` arms funnel through — the fast (data-only) path
1200
+ * (`backingShapeMatches` ⇒ direct `rebuildBacking`) and the reshape arm
1201
+ * (`reshapeBackingInPlace`, between its pre- and post-reconcile structural ops).
1202
+ * It is used by NOTHING else: create/import (`materializeView`) calls
1203
+ * `replaceContents` directly, and the incremental manager's full-rebuild arm
1204
+ * (`applyFullRebuild` in `core/database-materialized-views.ts`) does its own
1205
+ * `applyMaintenance` + per-delta validation (`validateDerivedChanges`).
1206
+ *
1207
+ * **Constraint-bearing branch.** When the maintained table declares ≥1 applicable
1208
+ * CHECK or (FK-enforcement-on) child-side FK ({@link hasApplicableConstraints}),
1209
+ * the swap mirrors the attach core instead of calling `replaceContents`: reject
1210
+ * duplicate derived keys ({@link assertRefreshRowsAreSet} — parity with
1211
+ * `replaceContents`'s set gate), land the recomputed set in the connection's
1212
+ * PENDING layer via `applyMaintenance('replace-all')`, run the eager bulk
1213
+ * anti-join / `not (<check>)` scan ({@link validateDeclaredConstraintsOverContents})
1214
+ * which throws the maintained-table-attributed CONSTRAINT diagnostic on the first
1215
+ * violator BEFORE the swap is committed (the failing statement unwinds and the
1216
+ * pending reconcile is discarded by statement-level rollback — the pre-refresh
1217
+ * COMMITTED contents stay intact), then `conn.commit()`.
1218
+ *
1219
+ * The commit is **commit-first parity** and load-bearing two ways: (1)
1220
+ * `replaceContents` already swaps committed state (a `begin; refresh; rollback`
1221
+ * does NOT undo a refresh today), so committing here preserves that exact
1222
+ * observable behavior; (2) on the reshape arm, `reshapeBackingInPlace`'s
1223
+ * post-reconcile data-validating ops (retype/recollate/tighten-NOT-NULL) scan
1224
+ * COMMITTED contents after this returns, so they must see the rebuilt rows —
1225
+ * `replaceContents` gives that implicitly, the pending-layer branch matches it by
1226
+ * committing (as the attach reshape path does before its own post-reconcile ops).
1227
+ *
1228
+ * The real-world trigger is a STALE table: a body-relevant source change released
1229
+ * the MV's row-time plan, subsequent source writes drifted unvalidated, and a
1230
+ * refresh recomputes that drifted set — so this scan is where a declared CHECK/FK
1231
+ * is enforced over rows that never crossed the maintenance boundary. A
1232
+ * continuously-maintained table re-derives an already-validated set, so the scan
1233
+ * is redundant-but-cheap there.
1234
+ *
1235
+ * Constraint-less maintained tables and every MV-sugar backing take the untouched
1236
+ * `replaceContents` fast path — no connection, no scan, byte-for-byte the prior
1237
+ * behavior. The caller is responsible for staleness re-validation when relevant;
1238
+ * this helper assumes the derivation body plans. Throws if the table is missing
1239
+ * from the catalog.
1240
+ */
1241
+ export async function rebuildBacking(db, mv) {
1242
+ const bodySql = astToString(mv.derivation.selectAst);
1243
+ const rows = await collectBodyRows(db, bodySql);
1244
+ const backing = db.schemaManager.getTable(mv.schemaName, mv.name);
1245
+ if (!backing) {
1246
+ throw new QuereusError(`Internal error: maintained table '${mv.name}' not found during rebuild`, StatusCode.INTERNAL);
1247
+ }
1248
+ const host = resolveBackingHost(db, backing);
1249
+ if (!isMaintainedTable(backing) || !hasApplicableConstraints(db, backing)) {
1250
+ // Fast path: nothing declared to validate (every MV-sugar backing, and a
1251
+ // constraint-less table-form maintained table). `replaceContents` swaps
1252
+ // COMMITTED contents and runs no derived-row validation — byte-for-byte the
1253
+ // historical path. (A pragma-off FK-only table also lands here: its bulk FK
1254
+ // scan would no-op anyway — see hasApplicableConstraints.)
1255
+ await host.replaceContents(rows, () => materializedViewNotASetError(mv.schemaName, mv.name));
1256
+ return;
1257
+ }
1258
+ // Constraint-bearing branch: pending-layer replace-all + eager bulk scan, then a
1259
+ // commit-first commit (see the docstring). `shapePk` is the live backing's
1260
+ // physical key — re-derived shape matches it on the fast path, and on the reshape
1261
+ // arm the catalog was already re-registered with the post-reshape PK before this
1262
+ // runs, so the live `primaryKeyDefinition` is the correct keying either way.
1263
+ const shapePk = backing.primaryKeyDefinition.map(c => ({
1264
+ index: c.index,
1265
+ collation: c.collation ?? backing.columns[c.index]?.collation,
1266
+ }));
1267
+ assertRefreshRowsAreSet(rows, shapePk, mv.schemaName, mv.name);
1268
+ const conn = await resolveAttachConnection(db, host, `${mv.schemaName}.${mv.name}`);
1269
+ await host.applyMaintenance(conn, [{ kind: 'replace-all', rows }]);
1270
+ // Throws the maintained-table-attributed diagnostic BEFORE the commit; on a
1271
+ // violation the failing statement unwinds and discards the pending reconcile,
1272
+ // leaving the pre-refresh committed contents intact (the MV stays stale, so the
1273
+ // next read re-validates rather than serving the rejected set).
1274
+ //
1275
+ // Documented limitation (collation-sensitive CHECK on the reshape arm): on the
1276
+ // reshape path this scan validates the rows in their PRE-recollate physical form
1277
+ // — the catalog column still carries the OLD collation here, and any
1278
+ // `recollate` op runs post-reconcile in reshapeBackingInPlace, AFTER this commit.
1279
+ // So a CHECK whose truth flips under a recollate-during-reshape (e.g. `v <> 'abc'`
1280
+ // with v recollated BINARY → NOCASE over a row 'ABC') passes here and is then
1281
+ // recollated into a violating state. The same class of corner applies to a
1282
+ // `retype` op (it shares this `postReconcileOps` batch): a CHECK whose truth flips
1283
+ // under the column's NEW affinity (e.g. `v < '9'` retyped TEXT → INTEGER over a
1284
+ // row '10' — lexicographic-true then numeric-false) passes here and is retyped into
1285
+ // violation. Not closed: this commit is load-bearing (commit-first parity + the
1286
+ // post-reconcile ops scan committed contents), and the attach reshape path uses the
1287
+ // identical ordering. See docs/materialized-views.md § REFRESH MATERIALIZED VIEW
1288
+ // "Known limitation — collation-sensitive CHECK" / "… type-sensitive CHECK" and
1289
+ // maintained-table-refresh-revalidation.spec.ts § "reshape arm: collation-
1290
+ // sensitive CHECK" / "reshape arm: type-sensitive CHECK".
1291
+ await validateDeclaredConstraintsOverContents(db, backing);
1292
+ await conn.commit();
1293
+ }
1294
+ /**
1295
+ * True iff the live backing `TableSchema` is structurally identical to what the
1296
+ * derived `shape` would build — so a `refresh` can take the data-only fast path
1297
+ * (`rebuildBacking`, preserving the backing identity and warm caches) instead of
1298
+ * rebuilding the backing table. Compares, in order:
1299
+ * - column **count**;
1300
+ * - per column: **name** (case-insensitive — matching the matcher's name compare),
1301
+ * **logical type**, **not-null**, **collation**;
1302
+ * - the **physical** PK ({@link computeBackingPrimaryKey} vs the backing's
1303
+ * `primaryKeyDefinition`, by index + desc + collation, in order).
1304
+ *
1305
+ * Returns false when a source schema change has shifted the body's output shape
1306
+ * (most visibly a `select *` body whose new source column interleaves into the
1307
+ * output) — the caller then rebuilds the backing to match the re-planned body.
1308
+ */
1309
+ export function backingShapeMatches(current, shape) {
1310
+ if (!backingShapeMatchesStructurally(current, shape))
1311
+ return false;
1312
+ for (let i = 0; i < shape.columns.length; i++) {
1313
+ if (current.columns[i].name.toLowerCase() !== shape.columns[i].name.toLowerCase())
1314
+ return false;
1315
+ }
1316
+ return true;
1317
+ }
1318
+ /**
1319
+ * The structural (name-blind) half of {@link backingShapeMatches}: column count,
1320
+ * per-column logical type / not-null / collation, and the physical PK. The rename
1321
+ * propagation ({@link propagateColumnRenameToMaterializedViews}) uses it to assert
1322
+ * a source column rename produced a *pure name shift* in the body's output before
1323
+ * carrying the new names onto the live backing — anything structural is not a
1324
+ * rename outcome and fails the propagation instead of rebuilding data.
1325
+ */
1326
+ function backingShapeMatchesStructurally(current, shape) {
1327
+ return describeBackingShapeMismatch(current, shape) === null;
1328
+ }
1329
+ /* ──────────────── shared backing-column comparison primitives ────────────────
1330
+ * The per-column attribute comparisons below are the single shape-diff vocabulary
1331
+ * shared by the positional {@link describeBackingShapeMismatch} (the rename
1332
+ * propagation's "pure name shift?" assertion) and the alignment-based
1333
+ * {@link classifyBackingReshape} (refresh's in-place reshape gate) — neither rolls
1334
+ * its own column compare. All compare by NAME / normalized value, not identity:
1335
+ * logical types resolve through the (name-interned) registry, but a module may
1336
+ * rebuild its TableSchema with fresh instances after an ALTER (the store module
1337
+ * does), so object identity is spuriously false. */
1338
+ /** The two columns carry the same logical type (by interned type name). */
1339
+ function backingTypeMatches(a, b) {
1340
+ return a.logicalType.name.toUpperCase() === b.logicalType.name.toUpperCase();
1341
+ }
1342
+ /** The two columns agree on NOT NULL. */
1343
+ function backingNotNullMatches(a, b) {
1344
+ return (a.notNull === true) === (b.notNull === true);
1345
+ }
1346
+ /** The two columns agree on declared collation (absent ⇒ BINARY). */
1347
+ function backingCollationMatches(a, b) {
1348
+ return (a.collation ?? 'BINARY') === (b.collation ?? 'BINARY');
1349
+ }
1350
+ /** Names the first structural difference between the live backing and the derived
1351
+ * shape (null when structurally identical) — the diagnostic half of
1352
+ * {@link backingShapeMatchesStructurally}. Deliberately **positional** (column i
1353
+ * vs column i): it answers "is this a pure positional name shift (or identical)?"
1354
+ * for the rename-propagation pass, which only ever carries names. The richer
1355
+ * alignment that tolerates appended / dropped / renamed columns is
1356
+ * {@link classifyBackingReshape}; both share the per-column predicates above. */
1357
+ function describeBackingShapeMismatch(current, shape) {
1358
+ if (current.columns.length !== shape.columns.length) {
1359
+ return `column count ${current.columns.length} → ${shape.columns.length}`;
1360
+ }
1361
+ for (let i = 0; i < shape.columns.length; i++) {
1362
+ const a = current.columns[i];
1363
+ const b = shape.columns[i];
1364
+ if (!backingTypeMatches(a, b)) {
1365
+ return `column ${i} type ${a.logicalType.name} → ${b.logicalType.name}`;
1366
+ }
1367
+ if (!backingNotNullMatches(a, b)) {
1368
+ return `column ${i} not-null ${a.notNull === true} → ${b.notNull === true}`;
1369
+ }
1370
+ if (!backingCollationMatches(a, b)) {
1371
+ return `column ${i} collation ${a.collation ?? 'BINARY'} → ${b.collation ?? 'BINARY'}`;
1372
+ }
1373
+ }
1374
+ const shapePk = computeBackingPrimaryKey(shape);
1375
+ const currentPk = current.primaryKeyDefinition;
1376
+ if (currentPk.length !== shapePk.length) {
1377
+ return `primary-key length ${currentPk.length} → ${shapePk.length}`;
1378
+ }
1379
+ for (let i = 0; i < shapePk.length; i++) {
1380
+ if (currentPk[i].index !== shapePk[i].index) {
1381
+ return `primary-key column ${i} index ${currentPk[i].index} → ${shapePk[i].index}`;
1382
+ }
1383
+ if ((currentPk[i].desc === true) !== (shapePk[i].desc === true)) {
1384
+ return `primary-key column ${i} direction`;
1385
+ }
1386
+ const shapeColl = shape.columns[shapePk[i].index]?.collation ?? 'BINARY';
1387
+ if ((currentPk[i].collation ?? 'BINARY') !== shapeColl) {
1388
+ return `primary-key column ${i} collation ${currentPk[i].collation ?? 'BINARY'} → ${shapeColl}`;
1389
+ }
1390
+ }
1391
+ return null;
1392
+ }
1393
+ /* ──────────────── body-irrelevant source change: recompile, never skip ────────────────
1394
+ * A `table_modified` whose old/new differ only in fields a body cannot read —
1395
+ * constraint metadata (CHECK exprs, FK targets, UNIQUE sets, index predicates),
1396
+ * `statistics`/`estimatedRows` (ANALYZE), `tags`, column defaults — cannot change
1397
+ * what a dependent MV's body *evaluates to*. But it CAN change what the body
1398
+ * **compiles to**: CHECK constraints seed domain facts (`ruleFilterContradiction`
1399
+ * may have folded a filter — or the whole body — away against a CHECK that no
1400
+ * longer holds), and `proveOneToOneJoin`'s join-residual arm rests on NOT-NULL
1401
+ * FK→PK referential integrity. So the MV manager's schema-change listener routes
1402
+ * live dependents of a qualifying event through an in-place RECOMPILE
1403
+ * ({@link tryRecompileMaterializedViewLive}) instead of marking them stale —
1404
+ * recompile, never skip. Any failure falls back to the mark-stale path. */
1405
+ /** The per-column fields a body can observe: name, logical type, NOT NULL,
1406
+ * collation (absent ⇒ BINARY), and the generated expression. `defaultValue`
1407
+ * and per-column conflict metadata are deliberately IGNORED — a body reads
1408
+ * stored values, never source defaults; the recompile-not-skip discipline
1409
+ * covers any optimizer-level concern. */
1410
+ function bodyRelevantColumnMatches(a, b) {
1411
+ return a.name.toLowerCase() === b.name.toLowerCase()
1412
+ && backingTypeMatches(a, b)
1413
+ && backingNotNullMatches(a, b)
1414
+ && backingCollationMatches(a, b)
1415
+ && (a.generated === true) === (b.generated === true)
1416
+ && (!a.generated || sameGeneratedExpr(a, b));
1417
+ }
1418
+ function sameGeneratedExpr(a, b) {
1419
+ if ((a.generatedExpr === undefined) !== (b.generatedExpr === undefined))
1420
+ return false;
1421
+ if (!a.generatedExpr || !b.generatedExpr)
1422
+ return true;
1423
+ return expressionToString(a.generatedExpr) === expressionToString(b.generatedExpr);
1424
+ }
1425
+ /** Pairwise physical-PK identity (`index`, `desc`, effective per-component
1426
+ * collation — explicit, else the keyed column's, else BINARY). */
1427
+ function samePrimaryKeyDefinition(a, b) {
1428
+ if (a.primaryKeyDefinition.length !== b.primaryKeyDefinition.length)
1429
+ return false;
1430
+ return a.primaryKeyDefinition.every((pa, i) => {
1431
+ const pb = b.primaryKeyDefinition[i];
1432
+ const collA = pa.collation ?? a.columns[pa.index]?.collation ?? 'BINARY';
1433
+ const collB = pb.collation ?? b.columns[pb.index]?.collation ?? 'BINARY';
1434
+ return pa.index === pb.index
1435
+ && (pa.desc === true) === (pb.desc === true)
1436
+ && collA === collB;
1437
+ });
1438
+ }
1439
+ /**
1440
+ * True when a `table_modified` event's old→new transition is **body-irrelevant**:
1441
+ * same table name and schema, columns pairwise identical in every body-relevant
1442
+ * field ({@link bodyRelevantColumnMatches}), and an identical physical primary
1443
+ * key. Everything else may differ — `checkConstraints`, `foreignKeys`,
1444
+ * `uniqueConstraints`, `indexes`, `statistics`, `estimatedRows`, `tags`,
1445
+ * `primaryKeyDefaultConflict`, defaults. A qualifying event cannot change what a
1446
+ * dependent body evaluates to, only what it compiles to — see the section note
1447
+ * above for why dependents are recompiled rather than skipped.
1448
+ *
1449
+ * **Reference-equality guard (load-bearing coupling).** The MV manager's
1450
+ * `emitBackingInvalidation` fires a synthetic `table_modified` on an MV's own
1451
+ * backing with the SAME object as `oldObject` and `newObject` — the event that
1452
+ * cascades staleness down MV-over-MV chains. It must classify as body-RELEVANT,
1453
+ * hence `oldObject === newObject` short-circuits to false here. Every genuine
1454
+ * emitter passes distinct old/new objects. If either side changes, change both
1455
+ * (see the matching comment in `emitBackingInvalidation`,
1456
+ * core/database-materialized-views.ts).
1457
+ */
1458
+ export function isBodyIrrelevantTableChange(oldObject, newObject) {
1459
+ if (oldObject === newObject)
1460
+ return false;
1461
+ if (oldObject.name.toLowerCase() !== newObject.name.toLowerCase())
1462
+ return false;
1463
+ if (oldObject.schemaName.toLowerCase() !== newObject.schemaName.toLowerCase())
1464
+ return false;
1465
+ if (oldObject.columns.length !== newObject.columns.length)
1466
+ return false;
1467
+ for (let i = 0; i < oldObject.columns.length; i++) {
1468
+ if (!bodyRelevantColumnMatches(oldObject.columns[i], newObject.columns[i]))
1469
+ return false;
1470
+ }
1471
+ return samePrimaryKeyDefinition(oldObject, newObject);
1472
+ }
1473
+ /** Structural (name-blind) column-only check: count + per-column type/not-null/collation,
1474
+ * WITHOUT comparing the physical PK. Used by the superkey relaxation in
1475
+ * `tryRecompileMaterializedViewLive` to gate the PK-changing case where column
1476
+ * attributes are otherwise identical. */
1477
+ function backingColumnsStructurallyMatch(current, shape) {
1478
+ if (current.columns.length !== shape.columns.length)
1479
+ return false;
1480
+ for (let i = 0; i < shape.columns.length; i++) {
1481
+ const a = current.columns[i];
1482
+ const b = shape.columns[i];
1483
+ if (!backingTypeMatches(a, b))
1484
+ return false;
1485
+ if (!backingNotNullMatches(a, b))
1486
+ return false;
1487
+ if (!backingCollationMatches(a, b))
1488
+ return false;
1489
+ }
1490
+ return true;
1491
+ }
1492
+ /** Returns true when the live backing's physical PK column set is a superkey of the
1493
+ * re-planned body — i.e., some proved minimal key from `shape.allProvedKeys` is
1494
+ * entirely contained in the backing PK's column set. Returns false when
1495
+ * `allProvedKeys` is absent (coarsened-lineage or all-columns path). */
1496
+ function isBackingPkASuperkeyInShape(current, shape) {
1497
+ if (!shape.allProvedKeys)
1498
+ return false;
1499
+ const backingPkCols = new Set(current.primaryKeyDefinition.map(pk => pk.index));
1500
+ return shape.allProvedKeys.some(k => k.every(idx => backingPkCols.has(idx)));
1501
+ }
1502
+ /* ──────────────── content-stability proof (structural-ALTER keep-live) ────────────────
1503
+ * For a CONSTRAINT-only `table_modified`, re-derived backing-shape identity implies
1504
+ * content identity (a constraint cannot change what stored rows the body evaluates
1505
+ * to, only what the body compiles to), so a recompile against the new catalog is
1506
+ * sufficient — that is the constraint-only path. For a STRUCTURAL ALTER (ADD / DROP /
1507
+ * ALTER COLUMN) the same argument does NOT carry: shape identity ⇏ content identity.
1508
+ * The classic hazard is `alter column v set collate nocase` (or `set data type`) on a
1509
+ * column the body uses only in a WHERE / join / group / order position — the output
1510
+ * shape is unchanged (v is unprojected), yet the row set the predicate admits, hence
1511
+ * the backing content, changes. So a structural keep-live additionally proves the
1512
+ * value-semantics of the change is DISJOINT from everything the body reads. The two
1513
+ * helpers below compute the two sides of that proof; the gate lives in
1514
+ * {@link tryRecompileMaterializedViewLive}. */
1515
+ /**
1516
+ * The columns whose **value semantics** the `oldObject → newObject` transition
1517
+ * changed: present **by name in both** schemas and differing in logical type or
1518
+ * collation. Returns lowercased column names (the column-index map key). NOT NULL,
1519
+ * default, generated-expr-unchanged, and add/drop are deliberately excluded —
1520
+ * NOT NULL / default are content-irrelevant (a body reads stored values, never
1521
+ * constraints or defaults), and an add/drop that the body reads is already caught
1522
+ * upstream (a `select *` reshapes ⇒ shape mismatch; a referenced dropped column
1523
+ * fails re-derivation; a referenced added column cannot exist in the authored body).
1524
+ * So the set is EMPTY for every change except ALTER COLUMN SET DATA TYPE / SET
1525
+ * COLLATE — making the disjointness proof a no-op (today's behavior) elsewhere.
1526
+ */
1527
+ function valueSemanticsChangedColumns(oldObject, newObject) {
1528
+ const out = new Set();
1529
+ for (const newCol of newObject.columns) {
1530
+ const oldCol = oldObject.columns.find(c => c.name.toLowerCase() === newCol.name.toLowerCase());
1531
+ if (!oldCol)
1532
+ continue; // added column — no value-semantics change to an existing column
1533
+ if (!backingTypeMatches(oldCol, newCol) || !backingCollationMatches(oldCol, newCol)) {
1534
+ out.add(newCol.name.toLowerCase());
1535
+ }
1536
+ }
1537
+ return out;
1538
+ }
1539
+ /**
1540
+ * The set of source-column indices (in `qualifiedSource`'s POST-ALTER schema) that a
1541
+ * materialized-view body **reads** — directly, through a predicate / join / group /
1542
+ * order position, or transitively through a generated column. The disjointness half
1543
+ * of the structural-ALTER content-stability gate (see the section note and
1544
+ * {@link tryRecompileMaterializedViewLive}): a value-semantics change (type /
1545
+ * collation) to a column NOT in this set cannot alter what the body evaluates to.
1546
+ *
1547
+ * **Why the un-optimized built plan, not `db.getPlan`.** The optimizer can absorb a
1548
+ * `where v = 'x'` predicate into an access-method seek key, dropping the explicit
1549
+ * {@link ColumnReferenceNode} from the tree — walking the optimized plan would MISS
1550
+ * that reference and falsely conclude disjoint (UNSOUND). The un-optimized built plan
1551
+ * (`db._buildPlan`) carries every reference explicitly in its projection / filter /
1552
+ * join / group / order nodes. Over-approximation is the safe direction: an extra
1553
+ * column in the read set only ever causes MORE staleness, never an unsound keep-live.
1554
+ *
1555
+ * Mechanics: walk the built tree (children AND relations, like {@link collectSourceTables},
1556
+ * so nested subqueries / EXISTS / correlated refs are reached) collecting every
1557
+ * `ColumnReferenceNode.attributeId`; for every `TableReferenceNode` whose qualified
1558
+ * name equals `qualifiedSource` (several for a self-join) map the collected attribute
1559
+ * ids back to its column indices via `getAttributes()`; union over occurrences. Then
1560
+ * expand the set DOWNWARD through `generatedColumnDependencies` to a fixed point —
1561
+ * reading a generated column reads its dependency columns even when the body never
1562
+ * names them (safe whether or not the planner inlines generated columns: if it does,
1563
+ * the dep already appears as a direct reference and the closure is a no-op; if it
1564
+ * does not, the closure is load-bearing).
1565
+ *
1566
+ * The rewrite is suppressed for the same reason {@link deriveBackingShape} suppresses
1567
+ * it. Any exception propagates to {@link tryRecompileMaterializedViewLive}'s try/catch,
1568
+ * which treats a failed analysis as "could not prove disjoint" ⇒ stale (the safe
1569
+ * default) — it must never be swallowed into a false "disjoint" conclusion.
1570
+ */
1571
+ export function referencedSourceColumns(db, bodySql, qualifiedSource) {
1572
+ const targetName = qualifiedSource.toLowerCase();
1573
+ return db.schemaManager.withSuppressedMaterializedViewRewrite(() => {
1574
+ const ast = new Parser().parse(bodySql);
1575
+ const { plan } = db._buildPlan([ast]);
1576
+ const referencedAttrIds = new Set();
1577
+ const sourceRefs = [];
1578
+ const visited = new Set();
1579
+ const walk = (node) => {
1580
+ if (visited.has(node))
1581
+ return;
1582
+ visited.add(node);
1583
+ if (node instanceof ColumnReferenceNode) {
1584
+ referencedAttrIds.add(node.attributeId);
1585
+ }
1586
+ else if (node instanceof TableReferenceNode
1587
+ && `${node.tableSchema.schemaName}.${node.tableSchema.name}`.toLowerCase() === targetName) {
1588
+ sourceRefs.push(node);
1589
+ }
1590
+ for (const c of node.getChildren())
1591
+ walk(c);
1592
+ for (const r of node.getRelations())
1593
+ walk(r);
1594
+ };
1595
+ walk(plan);
1596
+ const cols = new Set();
1597
+ let deps;
1598
+ for (const ref of sourceRefs) {
1599
+ const attrs = ref.getAttributes();
1600
+ for (let i = 0; i < attrs.length; i++) {
1601
+ if (referencedAttrIds.has(attrs[i].id))
1602
+ cols.add(i);
1603
+ }
1604
+ // The TableReferenceNode is built from the live (post-ALTER) catalog, so its
1605
+ // schema IS `newObject`; all S occurrences share it.
1606
+ deps ??= ref.tableSchema.generatedColumnDependencies;
1607
+ }
1608
+ if (deps)
1609
+ expandGeneratedDependencyClosure(cols, deps);
1610
+ return cols;
1611
+ });
1612
+ }
1613
+ /** Expand `cols` downward through `deps` (generated-column index → dependency column
1614
+ * indices) to a fixed point: a read of a generated column is a read of its dependency
1615
+ * columns, which may themselves be generated. */
1616
+ function expandGeneratedDependencyClosure(cols, deps) {
1617
+ const queue = [...cols];
1618
+ while (queue.length > 0) {
1619
+ const idx = queue.pop();
1620
+ const depIndices = deps.get(idx);
1621
+ if (!depIndices)
1622
+ continue;
1623
+ for (const d of depIndices) {
1624
+ if (!cols.has(d)) {
1625
+ cols.add(d);
1626
+ queue.push(d);
1627
+ }
1628
+ }
1629
+ }
1630
+ }
1631
+ /**
1632
+ * Recompile a LIVE materialized view's row-time plan in place after a **genuine**
1633
+ * source `table_modified` (`oldObject !== newObject` — constraint/stats/tags-only OR
1634
+ * structural ADD/DROP/ALTER COLUMN), gated by shape re-derivation and — for a
1635
+ * structural value-semantics change — a content-stability proof. The same discipline
1636
+ * as {@link restoreUnaffectedMaterializedViews}. Fully synchronous (the schema-change
1637
+ * listener is sync; shape derivation, schema lookups, the disjointness analysis, and
1638
+ * registration all are). Never throws: logs and returns `false` on any failure, and
1639
+ * the caller falls back to the mark-stale path. On success the MV stays live —
1640
+ * `stale` untouched, row-time plan rebuilt against the new catalog, no backing
1641
+ * invalidation (the backing stays maintained, so cached plans reading it remain
1642
+ * correct).
1643
+ *
1644
+ * **Structural-ALTER soundness (why a recompile is not enough on its own).** For a
1645
+ * constraint-only change, re-derived backing-shape identity IMPLIES content identity —
1646
+ * a constraint cannot change what stored rows the body evaluates to, only what the
1647
+ * body compiles to. For a structural ALTER that argument does NOT carry: shape
1648
+ * identity ⇏ content identity. `alter column v set collate nocase` / `set data type`
1649
+ * on a column the body reads only in a WHERE / join / group / order position leaves
1650
+ * the output shape identical while changing the admitted row set — the backing content
1651
+ * diverges from a fresh body evaluation. So the structural keep-live adds a final
1652
+ * **content-stability gate** proving the change's value-semantics-changed columns are
1653
+ * disjoint from everything the body reads (the {@link valueSemanticsChangedColumns} ∩
1654
+ * {@link referencedSourceColumns} proof). That changed set is EMPTY for constraint-only,
1655
+ * ADD, DROP, NOT NULL, and DEFAULT changes, so the proof is a no-op there and preserves
1656
+ * today's behavior exactly; it does real work only for ALTER COLUMN type/collation.
1657
+ *
1658
+ * Gates, in order — each failure is a stale fallback:
1659
+ * 1. `deriveBackingShape` throws when the body no longer plans against the
1660
+ * post-change catalog (e.g. a rename-cascade constraint rewrite observed
1661
+ * mid-statement, while a co-source's rename has landed but this MV's body
1662
+ * rewrite has not — the rename propagation's own MV loop restores it later).
1663
+ * A DROP COLUMN the body references directly throws here too.
1664
+ * 2. `sameSourceTables`: the re-planned source set must equal the recorded one.
1665
+ * An FK drop can un-eliminate a previously FK/PK-eliminated join (growing
1666
+ * the set); a constraint change can let `ruleFilterContradiction` fold a
1667
+ * source out of the plan entirely (shrinking it). Either way the record is
1668
+ * out of sync with the body's plan — leave it to REFRESH, which re-derives.
1669
+ * 3. `backingColumnsStructurallyMatch` + `isBackingPkASuperkeyInShape`: the column
1670
+ * structural attributes (type / not-null / collation) must match positionally,
1671
+ * AND the live backing's physical PK column set must be a superkey of the
1672
+ * re-planned body (some proved minimal key ⊆ backing PK columns). This forces
1673
+ * staleness when a dropped UNIQUE un-proves the recorded backing key (`keysOf`
1674
+ * falls back to a smaller key or all-columns → no proved key ⊆ old PK). An
1675
+ * ADD CONSTRAINT UNIQUE that subsumes the compound key passes: the new minimal
1676
+ * key is a subset of the old compound backing PK. A `select *` body over an
1677
+ * ADD/DROP COLUMN reshapes its output here ⇒ shape mismatch ⇒ stale. A PROJECTED
1678
+ * column whose type/collation changed shifts the output column ⇒ shape mismatch
1679
+ * ⇒ stale. Re-registers with the EXISTING backing (PK unchanged) on a pass.
1680
+ * 4. Content-stability gate (structural value-semantics ALTER only — see above):
1681
+ * if any value-semantics-changed column (type/collation) is read by the body
1682
+ * (transitively through generated columns), the backing content is unstable ⇒
1683
+ * stale. Empty changed set ⇒ no-op. A failure to build the disjointness analysis
1684
+ * propagates to the outer try/catch ⇒ stale (could not prove disjoint).
1685
+ * 5. `registerMaterializedView` re-runs arm selection / eligibility / cost
1686
+ * gating (`buildMaintenancePlan`) against the new catalog and throws on the
1687
+ * create-time gates (non-determinism, bag/no-key floor, full-rebuild
1688
+ * pathology against fresh ANALYZE stats — defensible: the alternative is
1689
+ * unbounded per-write rebuild cost). Registration is event-silent, so the
1690
+ * success path fires no nested schema-change notifications.
1691
+ *
1692
+ * `oldObject`/`newObject` are the genuine event's distinct schemas. The synthetic
1693
+ * backing-invalidation event (same object as old/new) is excluded by the caller's
1694
+ * `oldObject !== newObject` guard — it must cascade staleness, never recompile.
1695
+ *
1696
+ * Deliberately NOT {@link restoreMaterializedViewLive}: that path is async, may
1697
+ * rename backing columns, and clears `stale` — the wrong discipline here, where
1698
+ * the MV is live throughout and a pre-existing `stale` flag must stay untouched.
1699
+ */
1700
+ export function tryRecompileMaterializedViewLive(db, mv, oldObject, newObject) {
1701
+ try {
1702
+ const d = mv.derivation;
1703
+ const bodySql = astToString(d.selectAst);
1704
+ const shape = deriveBackingShape(db, bodySql, d.columns);
1705
+ if (!sameSourceTables(d.sourceTables, shape.sourceTables)) {
1706
+ log('Marking materialized view %s.%s stale instead of recompiling: re-planned source tables (%s) disagree with the recorded set (%s) — REFRESH re-derives', mv.schemaName, mv.name, shape.sourceTables.join(', '), d.sourceTables.join(', '));
1707
+ return false;
1708
+ }
1709
+ const schema = db.schemaManager.getSchemaOrFail(mv.schemaName);
1710
+ const live = schema.getTable(mv.name);
1711
+ const backing = isMaintainedTable(live) ? live : mv;
1712
+ const mismatch = describeBackingShapeMismatch(backing, shape);
1713
+ if (mismatch) {
1714
+ // Relaxed superkey gate: columns match structurally AND the existing backing
1715
+ // PK column set is still a superkey of the re-planned body (some proved
1716
+ // minimal key is ⊆ the backing PK's column set). Covers ADD CONSTRAINT UNIQUE
1717
+ // that subsumes the compound key — keysOf now returns a smaller key first,
1718
+ // changing the physical PK shape, but the old backing PK is still uniquely
1719
+ // identifying. Re-register with the EXISTING backing (unchanged PK).
1720
+ if (!backingColumnsStructurallyMatch(backing, shape) || !isBackingPkASuperkeyInShape(backing, shape)) {
1721
+ log('Marking materialized view %s.%s stale instead of recompiling: backing shape mismatch (%s) — REFRESH rebuilds it', mv.schemaName, mv.name, mismatch);
1722
+ return false;
1723
+ }
1724
+ log('Recompiling materialized view %s.%s with existing backing PK (superkey check passed): %s', mv.schemaName, mv.name, mismatch);
1725
+ }
1726
+ // Name-stability gate. The recompile re-registers against the EXISTING backing, so
1727
+ // it is sound only when the re-derived body's output column NAMES still match the
1728
+ // backing's. `describeBackingShapeMismatch` is deliberately name-blind (it serves the
1729
+ // rename propagation's pure-positional-name-shift detection), so a column RENAME under
1730
+ // a `select *`-style body re-derives a name-blind-identical shape — keeping it live
1731
+ // here would leave the backing column under its OLD name. Decline so the
1732
+ // rename-propagation pass owns the backing rename (restoreUnaffectedMaterializedViews);
1733
+ // an explicit-column body naming the renamed column already declined upstream
1734
+ // (deriveBackingShape threw).
1735
+ if (shape.columns.some((c, i) => c.name.toLowerCase() !== (backing.columns[i]?.name ?? '').toLowerCase())) {
1736
+ log('Marking materialized view %s.%s stale instead of recompiling: re-derived output names shifted (a column rename) — the rename-propagation pass owns the backing rename', mv.schemaName, mv.name);
1737
+ return false;
1738
+ }
1739
+ // Content-stability gate. EMPTY for constraint-only / ADD / DROP / NOT NULL /
1740
+ // DEFAULT (no-op — exactly today's behavior); for an ALTER COLUMN type/collation
1741
+ // it proves the change is disjoint from every column the body reads (directly or
1742
+ // transitively through generated columns), else the backing content is unstable.
1743
+ const valueChanged = valueSemanticsChangedColumns(oldObject, newObject);
1744
+ if (valueChanged.size > 0) {
1745
+ const source = `${newObject.schemaName}.${newObject.name}`.toLowerCase();
1746
+ const read = referencedSourceColumns(db, bodySql, source);
1747
+ const collidingName = [...valueChanged].find(name => {
1748
+ const idx = newObject.columnIndexMap.get(name);
1749
+ return idx !== undefined && read.has(idx);
1750
+ });
1751
+ if (collidingName !== undefined) {
1752
+ log("Marking materialized view %s.%s stale instead of recompiling: a value-semantics ALTER (type/collation) on '%s' — a column the body reads — changes backing content; REFRESH re-derives", mv.schemaName, mv.name, collidingName);
1753
+ return false;
1754
+ }
1755
+ log('Recompiling materialized view %s.%s after a value-semantics ALTER (type/collation) on column(s) the body does not read (%s)', mv.schemaName, mv.name, [...valueChanged].join(', '));
1756
+ }
1757
+ db.registerMaterializedView(backing);
1758
+ log('Recompiled materialized view %s.%s in place after a genuine source change', mv.schemaName, mv.name);
1759
+ return true;
1760
+ }
1761
+ catch (e) {
1762
+ log('Marking materialized view %s.%s stale instead of recompiling after a genuine source change: %s', mv.schemaName, mv.name, e instanceof Error ? e.message : String(e));
1763
+ return false;
1764
+ }
1765
+ }
1766
+ /**
1767
+ * Classifies the column-level delta old(`current`)→new(`shape`) for an
1768
+ * identity-preserving refresh reshape. **Expressible in place** — returns the
1769
+ * ordered module-op plan — iff the change is any combination of **trailing**
1770
+ * appended columns, dropped columns, positionally renamed columns, and per-column
1771
+ * attribute (type / collation / not-null) changes, with the surviving columns'
1772
+ * relative order preserved and the physical primary key unchanged. Otherwise
1773
+ * **inexpressible** (the caller raises a sited error and leaves the table
1774
+ * untouched):
1775
+ *
1776
+ * - an **interleaving** reorder — a new column landing mid-table (the canonical
1777
+ * `select *` body whose new source column lands before existing outputs):
1778
+ * append-only `addColumn` cannot place it, and renaming survivors to fake it
1779
+ * would silently re-map values;
1780
+ * - a **physical-PK definition change** (column set, order, direction,
1781
+ * collation, or a key column's type) — a maintained table's PK is its
1782
+ * replicated row identity; silently re-keying it is the fatality drop+recreate
1783
+ * was.
1784
+ *
1785
+ * Surviving columns are matched by **name** (case-insensitive — the only stable
1786
+ * identity a derived backing carries); a name absent on both sides at an aligned
1787
+ * position is a positional rename (the value-preserving trace
1788
+ * {@link renameShiftedBackingColumns} already uses). Shares the per-column
1789
+ * predicates with {@link describeBackingShapeMismatch} (the positional pure-name-
1790
+ * shift check) rather than re-implementing the column compare.
1791
+ *
1792
+ * The resulting plan is two-phase (see {@link ReshapePlan}): the structural,
1793
+ * data-lossless ops (`rename`/`add`/`loosenNotNull`/`drop`) go pre-reconcile; the
1794
+ * data-validating attribute shifts (`retype`/`recollate`) and every deferred
1795
+ * NOT NULL `tightenNotNull` go post-reconcile, so they validate the reconciled
1796
+ * body rows rather than the discarded backing.
1797
+ */
1798
+ function classifyBackingReshape(current, shape) {
1799
+ const cur = current.columns;
1800
+ const sh = shape.columns;
1801
+ const curNames = new Set(cur.map(c => c.name.toLowerCase()));
1802
+ const shNames = new Set(sh.map(c => c.name.toLowerCase()));
1803
+ const renames = [];
1804
+ const adds = [];
1805
+ const loosens = []; // pre-reconcile: NOT NULL loosen never throws on data
1806
+ const drops = [];
1807
+ const postReconcileOps = []; // retype / recollate / tightenNotNull — validate the reconciled body
1808
+ // lower(oldName) → lower(newName), for the rename-aware PK comparison below.
1809
+ const renameMap = new Map();
1810
+ // A survivor's attribute shift. The data-validating shifts (type/collation
1811
+ // retype, NOT NULL *tightening*) defer to the post-reconcile batch — the live
1812
+ // rows may still violate them, but the re-derived body rows will not. A NOT NULL
1813
+ // *loosening* never throws on data, so it stays pre-reconcile. `name` is the
1814
+ // column's post-rename (new) name.
1815
+ const recordAttrShift = (from, to, name) => {
1816
+ if (!backingTypeMatches(from, to))
1817
+ postReconcileOps.push({ kind: 'retype', name, newTypeName: to.logicalType.name });
1818
+ if (!backingCollationMatches(from, to))
1819
+ postReconcileOps.push({ kind: 'recollate', name, collation: to.collation ?? 'BINARY' });
1820
+ if (!backingNotNullMatches(from, to)) {
1821
+ if (to.notNull === true)
1822
+ postReconcileOps.push({ kind: 'tightenNotNull', name });
1823
+ else
1824
+ loosens.push({ kind: 'loosenNotNull', name });
1825
+ }
1826
+ };
1827
+ let i = 0, j = 0;
1828
+ while (i < cur.length && j < sh.length) {
1829
+ const cc = cur[i], sc = sh[j];
1830
+ const cn = cc.name.toLowerCase(), sn = sc.name.toLowerCase();
1831
+ if (cn === sn) {
1832
+ recordAttrShift(cc, sc, sc.name);
1833
+ i++;
1834
+ j++;
1835
+ }
1836
+ else if (!shNames.has(cn) && !curNames.has(sn)) {
1837
+ // Aligned position, both names "extra" ⇒ positional rename cc → sc.
1838
+ renames.push({ kind: 'rename', oldName: cc.name, oldCol: cc, newName: sc.name });
1839
+ renameMap.set(cn, sn);
1840
+ recordAttrShift(cc, sc, sc.name); // attr ops reference the post-rename name
1841
+ i++;
1842
+ j++;
1843
+ }
1844
+ else if (!shNames.has(cn)) {
1845
+ // cc's name is gone from the new shape ⇒ dropped; sc matches a later survivor.
1846
+ drops.push({ kind: 'drop', name: cc.name });
1847
+ i++;
1848
+ }
1849
+ else if (!curNames.has(sn)) {
1850
+ // A genuinely new column appearing before the current survivors are
1851
+ // exhausted ⇒ a mid-table insert, not a trailing append.
1852
+ return { expressible: false, reason: `new column '${sc.name}' lands mid-table (an interleaving reshape, not a trailing append)` };
1853
+ }
1854
+ else {
1855
+ // Both names exist on the opposite side but are not aligned here ⇒ a reorder/swap.
1856
+ return { expressible: false, reason: `columns '${cc.name}' and '${sc.name}' are reordered` };
1857
+ }
1858
+ }
1859
+ for (; i < cur.length; i++) {
1860
+ const cc = cur[i];
1861
+ if (!shNames.has(cc.name.toLowerCase()))
1862
+ drops.push({ kind: 'drop', name: cc.name });
1863
+ else
1864
+ return { expressible: false, reason: `column '${cc.name}' is reordered` };
1865
+ }
1866
+ for (; j < sh.length; j++) {
1867
+ const sc = sh[j];
1868
+ if (!curNames.has(sc.name.toLowerCase())) {
1869
+ // Added NULLABLE pre-reconcile (the reconcile fills it); any NOT NULL is
1870
+ // asserted post-reconcile against the filled rows, joining the tighten batch.
1871
+ adds.push({ kind: 'add', col: sc });
1872
+ if (sc.notNull === true)
1873
+ postReconcileOps.push({ kind: 'tightenNotNull', name: sc.name });
1874
+ }
1875
+ else {
1876
+ return { expressible: false, reason: `column '${sc.name}' is reordered` };
1877
+ }
1878
+ }
1879
+ const pkReason = describePhysicalPkChange(current, shape, renameMap);
1880
+ if (pkReason)
1881
+ return { expressible: false, reason: pkReason };
1882
+ // Pre-reconcile: the structural, data-lossless ops only (renames + adds before
1883
+ // drops, so a mid-sequence failure leaves a re-derivable state). The
1884
+ // data-validating attribute shifts + NOT NULL tightenings run post-reconcile
1885
+ // against the reconciled body, never the discarded backing.
1886
+ return {
1887
+ expressible: true,
1888
+ plan: {
1889
+ preReconcileOps: [...renames, ...adds, ...loosens, ...drops],
1890
+ postReconcileOps,
1891
+ },
1892
+ };
1893
+ }
1894
+ /**
1895
+ * Compares the live backing's physical primary key to the re-derived shape's
1896
+ * ({@link computeBackingPrimaryKey}) **by column name through the reshape's rename
1897
+ * map** — not by index, which add/drop shift. Any change to the key's column set,
1898
+ * order, direction, collation, or a key column's **type** makes the reshape
1899
+ * inexpressible: a maintained table's PK is its replicated row identity, and
1900
+ * re-keying replicated row identity in place is refused. Returns a reason string,
1901
+ * or null when the key is unchanged. (A renamed key column is *not* a key change —
1902
+ * the rename map carries its new name; but a renamed-*and*-retyped key column still
1903
+ * trips the type check, because the comparison is on the underlying column schemas,
1904
+ * whose type identity a rename does not change.)
1905
+ */
1906
+ function describePhysicalPkChange(current, shape, renameMap) {
1907
+ const shapePk = computeBackingPrimaryKey(shape);
1908
+ const currentPk = current.primaryKeyDefinition;
1909
+ if (currentPk.length !== shapePk.length) {
1910
+ return `primary-key column count ${currentPk.length} → ${shapePk.length}`;
1911
+ }
1912
+ for (let k = 0; k < shapePk.length; k++) {
1913
+ const curCol = current.columns[currentPk[k].index];
1914
+ const shCol = shape.columns[shapePk[k].index];
1915
+ const curName = renameMap.get(curCol.name.toLowerCase()) ?? curCol.name.toLowerCase();
1916
+ if (curName !== shCol.name.toLowerCase()) {
1917
+ return `primary-key column ${k} '${curCol.name}' → '${shCol.name}'`;
1918
+ }
1919
+ if (!backingTypeMatches(curCol, shCol)) {
1920
+ return `primary-key column ${k} '${curCol.name}' type ${curCol.logicalType.name} → ${shCol.logicalType.name}`;
1921
+ }
1922
+ if ((currentPk[k].desc === true) !== (shapePk[k].desc === true)) {
1923
+ return `primary-key column ${k} direction`;
1924
+ }
1925
+ const curColl = currentPk[k].collation ?? curCol.collation ?? 'BINARY';
1926
+ const shColl = shCol.collation ?? 'BINARY';
1927
+ if (curColl !== shColl) {
1928
+ return `primary-key column ${k} collation ${curColl} → ${shColl}`;
1929
+ }
1930
+ }
1931
+ return null;
1932
+ }
1933
+ /** Lifts a {@link ReshapeColumnOp} onto the module's `SchemaChangeInfo` surface. */
1934
+ function reshapeOpToChange(op) {
1935
+ switch (op.kind) {
1936
+ case 'rename':
1937
+ // Preserve the OLD column's attributes (type / not-null / collation / PK)
1938
+ // under the new name — attribute shifts ride separate alter ops.
1939
+ return { type: 'renameColumn', oldName: op.oldName, newName: op.newName, newColumnDefAst: backingColumnDef(op.oldCol, op.newName) };
1940
+ case 'add': {
1941
+ // Add NULLABLE: real values arrive with the reconcile, and any NOT NULL is
1942
+ // asserted post-reconcile so a non-empty backing never trips "ADD NOT NULL
1943
+ // without a default". An added column is never a PK column (a PK change is
1944
+ // inexpressible), so force non-PK in the lifted def.
1945
+ const nullable = { ...op.col, notNull: false, primaryKey: false, pkOrder: 0, pkDirection: undefined };
1946
+ return { type: 'addColumn', columnDef: backingColumnDef(nullable, op.col.name) };
1947
+ }
1948
+ case 'retype':
1949
+ return { type: 'alterColumn', columnName: op.name, setDataType: op.newTypeName };
1950
+ case 'recollate':
1951
+ return { type: 'alterColumn', columnName: op.name, setCollation: op.collation };
1952
+ case 'loosenNotNull':
1953
+ return { type: 'alterColumn', columnName: op.name, setNotNull: false };
1954
+ case 'tightenNotNull':
1955
+ return { type: 'alterColumn', columnName: op.name, setNotNull: true };
1956
+ case 'drop':
1957
+ return { type: 'dropColumn', columnName: op.name };
1958
+ }
1959
+ }
1960
+ /**
1961
+ * Rebuild a maintained-table catalog record from the backing module's post-reshape
1962
+ * `TableSchema`. `module.alterTable` returns ONLY the physical column shape — it
1963
+ * tracks neither the catalog-only `derivation` nor the catalog-only `tags`, so a
1964
+ * bare `{ ...moduleSchema, derivation }` graft silently drops the table's tags.
1965
+ * Graft both from the authoritative catalog record so a reshaping re-attach
1966
+ * preserves any tags a concurrent SET TAGS routed through ALTER MATERIALIZED VIEW
1967
+ * (and a refresh-driven reshape never wipes existing tags). A non-reshaping
1968
+ * re-attach keeps the whole record, tags included — this restores parity.
1969
+ */
1970
+ function graftReshapedRecord(moduleSchema, source) {
1971
+ return { ...moduleSchema, derivation: source.derivation, tags: source.tags };
1972
+ }
1973
+ /**
1974
+ * The sited error a refresh raises when the re-derived body shape cannot be
1975
+ * reconciled onto the live maintained table in place — an interleaving column
1976
+ * reorder or a physical-PK definition change (or a host module without
1977
+ * `alterTable`). The table and its rows are left **untouched** and the derivation
1978
+ * stays `stale`, recoverable exactly as the message says. Replaces the former
1979
+ * silent drop+recreate: a maintained table's PK / positional identity is its
1980
+ * replicated row identity, so an incompatible reshape is an actionable error, not
1981
+ * a new incarnation.
1982
+ */
1983
+ function inexpressibleReshapeError(schemaName, name, reason) {
1984
+ return new QuereusError(`the derivation's output shape changed incompatibly with table '${schemaName}.${name}' (${reason}); `
1985
+ + `alter the table to the new shape and re-attach, or drop and recreate`, StatusCode.ERROR);
1986
+ }
1987
+ /**
1988
+ * Identity-preserving reshape of a maintained table whose re-derived body shape
1989
+ * shifted — the refresh path's replacement for the former drop+recreate. Classify
1990
+ * the column delta; an inexpressible delta (interleave / PK-definition change)
1991
+ * raises the sited error with the table untouched, an expressible one reshapes in
1992
+ * place. The shape-match fast path (`backingShapeMatches` ⇒ data-only
1993
+ * `rebuildBacking`) is the caller's and is untouched.
1994
+ */
1995
+ export async function reshapeBacking(db, mv, shape) {
1996
+ const classification = classifyBackingReshape(mv, shape);
1997
+ if (!classification.expressible) {
1998
+ throw inexpressibleReshapeError(mv.schemaName, mv.name, classification.reason);
1999
+ }
2000
+ return reshapeBackingInPlace(db, mv, shape, classification.plan);
2001
+ }
2002
+ /**
2003
+ * Executes an expressible in-place reshape in two phases around the data reconcile:
2004
+ *
2005
+ * 1. apply the **pre-reconcile** structural ops (renames/adds/loosens/drops) →
2006
+ * 2. re-register the reshaped (structural) schema + (shape-updated) derivation →
2007
+ * 3. data-reconcile via the shared {@link rebuildBacking} (re-run the body, swap
2008
+ * contents) → 4. apply the **post-reconcile** data-validating ops
2009
+ * (retype/recollate/tighten-NOT-NULL) → 5. re-register the final schema →
2010
+ * 6. fire one `table_modified`.
2011
+ *
2012
+ * The **same table incarnation throughout** — the backing-host instance stays
2013
+ * owned, no `table_removed`/`table_added` — so a replicated basis table's row
2014
+ * metadata survives; consumer maintained tables go stale via the single
2015
+ * `table_modified` and recover by their own refresh, exactly as for any source
2016
+ * alter. Returns the reshaped maintained table for the caller to re-register
2017
+ * maintenance on.
2018
+ *
2019
+ * **Why the data-validating ops defer.** A retype (physical convert), a recollate
2020
+ * (re-key + unique re-validate), and a NOT NULL tighten each scan the rows and
2021
+ * throw on a violation — but the pre-reconcile rows are about to be discarded by
2022
+ * step 3. Running them pre-reconcile would validate the stale backing (which may
2023
+ * still hold pre-narrowing values, e.g. an MV gone stale on an unrelated source
2024
+ * change whose data-fix was never maintained in) and spuriously throw a
2025
+ * MISMATCH/CONSTRAINT on a reshape the fresh body satisfies. Deferring them past
2026
+ * the reconcile validates the re-derived body rows instead. This is sound because
2027
+ * the reconcile's insert paths do NOT validate values against the column schema
2028
+ * (`MemoryTable.replaceBaseLayer` PK-extracts + inserts raw; the store backing-host
2029
+ * `replaceContents` puts serialized rows by keyed diff), so a body value conforming
2030
+ * to the NEW attribute enters the still-OLD-typed column unvalidated, and the
2031
+ * post-reconcile op then converts/re-keys/asserts the clean body data successfully.
2032
+ * The added-NULLABLE / deferred-tighten behavior for new NOT NULL columns is the
2033
+ * same mechanism (a non-empty backing never trips "ADD NOT NULL without a default").
2034
+ *
2035
+ * **Recoverability.** Only the data-lossless structural ops run before step 2's
2036
+ * `schema.addTable`, so the window in which the catalog schema and the module's
2037
+ * live schema could diverge on a partial failure no longer arises in practice. A
2038
+ * genuine post-reconcile throw (a body the new attribute still cannot satisfy)
2039
+ * happens AFTER the catalog is consistently re-registered with the reconciled body,
2040
+ * so the caller leaves the MV `stale` over a coherent, re-runnable table that
2041
+ * converges once the underlying data is fixed.
2042
+ */
2043
+ async function reshapeBackingInPlace(db, mv, shape, plan) {
2044
+ const sm = db.schemaManager;
2045
+ const schema = sm.getSchemaOrFail(mv.schemaName);
2046
+ const backing = schema.getTable(mv.name);
2047
+ if (!backing) {
2048
+ throw new QuereusError(`Internal error: maintained table '${mv.name}' not found during reshape`, StatusCode.INTERNAL);
2049
+ }
2050
+ const module = requireVtabModule(backing);
2051
+ if (!module.alterTable) {
2052
+ throw inexpressibleReshapeError(mv.schemaName, mv.name, `its backing module '${backing.vtabModuleName}' does not support in-place ALTER`);
2053
+ }
2054
+ // Pre-reconcile structural ops (renames/adds/loosens/drops — none throw on data).
2055
+ // Each addresses its column by name, so the fresh schema each call returns need
2056
+ // not be threaded by index; track only the latest.
2057
+ let current = backing;
2058
+ for (const op of plan.preReconcileOps) {
2059
+ current = await module.alterTable(db, mv.schemaName, mv.name, reshapeOpToChange(op));
2060
+ }
2061
+ // Re-register the reshaped schema with the (shape-updated) derivation BEFORE the
2062
+ // reconcile, so `rebuildBacking` resolves the reshaped table from the catalog.
2063
+ // alterTable returns a fresh derivation-less TableSchema; carry the derivation.
2064
+ mv.derivation.logicalKey = shape.primaryKey;
2065
+ mv.derivation.coarsenedKey = shape.coarsenedKey;
2066
+ mv.derivation.ordering = shape.ordering;
2067
+ mv.derivation.sourceTables = shape.sourceTables;
2068
+ let live = graftReshapedRecord(current, mv);
2069
+ schema.addTable(live);
2070
+ // Data reconcile: re-run the body and swap contents (the identity-preserving
2071
+ // data-only path — same host, same incarnation).
2072
+ await rebuildBacking(db, live);
2073
+ // Post-reconcile data-validating ops (retype / recollate / tighten NOT NULL): the
2074
+ // reconciled body rows satisfy the new attribute where the discarded backing
2075
+ // might not, so each validates the fresh data, not the stale rows. Re-register
2076
+ // the catalog after EACH op (not once after the loop): a data-validating op can
2077
+ // throw, and unlike the pre-reconcile batch the module schema mutates per op, so
2078
+ // a single post-loop register would leave the catalog behind the module — the
2079
+ // very catalog/module divergence this two-phase split exists to avoid — on a
2080
+ // partial throw. Per-op registration keeps the catalog tracking the module so a
2081
+ // mid-batch failure leaves a coherent, re-runnable table.
2082
+ //
2083
+ // NOTE: a `recollate` here applies AFTER step 3's rebuildBacking has already
2084
+ // validated + committed the rows under the OLD collation — so a collation-
2085
+ // sensitive declared CHECK whose truth flips under this recollate is not caught
2086
+ // by that scan. A `retype` in this same batch is the affinity analog: a CHECK
2087
+ // whose truth flips under the column's NEW logical type (e.g. `v < '9'` retyped
2088
+ // TEXT → INTEGER) is likewise validated under the OLD type and missed (documented
2089
+ // limitation; see the note in rebuildBacking's constraint-bearing branch and
2090
+ // docs/materialized-views.md).
2091
+ for (const op of plan.postReconcileOps) {
2092
+ current = await module.alterTable(db, mv.schemaName, mv.name, reshapeOpToChange(op));
2093
+ live = graftReshapedRecord(current, mv);
2094
+ schema.addTable(live);
2095
+ }
2096
+ // One engine-level event for the whole reshape: invalidate cached plans scanning
2097
+ // the table directly and cascade staleness to consumer MVs — table_modified, NOT
2098
+ // table_removed/added, since the incarnation is preserved.
2099
+ sm.getChangeNotifier().notifyChange({
2100
+ type: 'table_modified',
2101
+ schemaName: mv.schemaName,
2102
+ objectName: mv.name,
2103
+ oldObject: backing,
2104
+ newObject: live,
2105
+ });
2106
+ return live;
2107
+ }
2108
+ /**
2109
+ * Resolves the {@link BackingHost} for a materialized view's backing table via
2110
+ * the owning module's backing-host capability (`vtab/backing-host.ts`). INTERNAL
2111
+ * when the module lacks the capability or does not know the table — a backing
2112
+ * table is engine-created on a capability-checked module, so either is a bug.
2113
+ */
2114
+ export function resolveBackingHost(db, backingSchema) {
2115
+ const module = requireVtabModule(backingSchema);
2116
+ if (!module.getBackingHost) {
2117
+ throw new QuereusError(`materialized view backing table '${backingSchema.name}' is owned by module `
2118
+ + `'${backingSchema.vtabModuleName}', which does not implement the backing-host capability`, StatusCode.INTERNAL);
2119
+ }
2120
+ const host = module.getBackingHost(db, backingSchema.schemaName, backingSchema.name);
2121
+ if (!host) {
2122
+ throw new QuereusError(`backing host not found for '${backingSchema.schemaName}.${backingSchema.name}'`, StatusCode.INTERNAL);
2123
+ }
2124
+ return host;
2125
+ }
2126
+ /**
2127
+ * Lenient counterpart of {@link resolveBackingHost}: returns the backing host, or
2128
+ * `undefined` when the owning module cannot (yet) resolve one — instead of
2129
+ * throwing. Used at maintenance-PLAN-BUILD time (the create-time gate registration),
2130
+ * where the only use of the host is the host-conditional, default-inert
2131
+ * `requiresReplicableDerivations` gate. A module that materializes its durable
2132
+ * backing LATE in the attach flow (e.g. lamina's `ensureBackingForAttach`, which
2133
+ * runs after the gate registration) has no host yet at plan-build time; the host is
2134
+ * resolved for real at the reconcile, and the steady-state maintenance arms
2135
+ * re-resolve it per use. Skipping the replicable gate when the host is absent is
2136
+ * sound: a host that sets `requiresReplicableDerivations` (the synced-store flavor)
2137
+ * always exists by plan-build time, so the gate still binds it — the
2138
+ * eager-resolution invariant on {@link BackingHost.requiresReplicableDerivations}.
2139
+ *
2140
+ * That soundness rests on the invariant being honored, not on prose: a host that
2141
+ * BOTH demands replicable derivations AND defers its host to the late seam would
2142
+ * skip the gate here unnoticed. The attach core's defensive guard
2143
+ * ({@link attachMaintainedDerivation}) re-checks once the late host is in hand and
2144
+ * raises a loud INTERNAL error in exactly that case, so this lenient skip can never
2145
+ * silently let a non-replicable body through.
2146
+ */
2147
+ export function tryResolveBackingHost(db, backingSchema) {
2148
+ const module = requireVtabModule(backingSchema);
2149
+ return module.getBackingHost?.(db, backingSchema.schemaName, backingSchema.name);
2150
+ }
2151
+ /**
2152
+ * Eagerly records the constraint↔structure link when this MV covers a UNIQUE
2153
+ * constraint on one of its single source tables. Runs the coverage prover
2154
+ * (`coverage-prover.ts`) over the optimized body and, on the first match, stamps
2155
+ * the MV's `origin`/`covers` reverse link and the constraint's
2156
+ * `coveringStructureName` forward pointer (the source of truth). Informational
2157
+ * in this ticket — nothing enforces through the MV's backing table yet.
2158
+ *
2159
+ * Best-effort and side-effect-bounded: the body has already planned (during
2160
+ * shape derivation), so re-planning here is cheap and safe; a non-covering MV
2161
+ * simply records nothing.
2162
+ */
2163
+ export function linkCoveredUniqueConstraints(db, mv, bodySql) {
2164
+ // The coverage prover reasons over the body's SOURCE table; suppress the
2165
+ // read-side rewrite so the body is not re-pointed at this MV's own backing.
2166
+ const root = db.schemaManager.withSuppressedMaterializedViewRewrite(() => db.getPlan(bodySql).getRelations()[0]);
2167
+ if (!root)
2168
+ return;
2169
+ const sm = db.schemaManager;
2170
+ for (const qualified of mv.derivation.sourceTables) {
2171
+ const dot = qualified.indexOf('.');
2172
+ const schemaName = dot >= 0 ? qualified.slice(0, dot) : 'main';
2173
+ const tableName = dot >= 0 ? qualified.slice(dot + 1) : qualified;
2174
+ const table = sm.getTable(schemaName, tableName);
2175
+ if (!table || !table.uniqueConstraints)
2176
+ continue;
2177
+ for (const uc of table.uniqueConstraints) {
2178
+ const result = proveCoverage(root, mv, uc, table);
2179
+ if (result.covers) {
2180
+ mv.derivation.covers = { schemaName: table.schemaName, tableName: table.name, constraintName: uc.name };
2181
+ // Forward pointer is the source of truth (see docs/schema.md).
2182
+ uc.coveringStructureName = mv.name;
2183
+ return; // singular back-pointer: link the first covered constraint.
2184
+ }
2185
+ }
2186
+ }
2187
+ }
2188
+ /**
2189
+ * Clears the constraint↔structure link a covering MV established (drop path).
2190
+ * Matches on the forward pointer (`coveringStructureName === mv.name`) so it
2191
+ * works for unnamed constraints too; no enforcement demotion — physical schemas
2192
+ * still enforce via the implicit auto-index.
2193
+ */
2194
+ export function unlinkCoveredUniqueConstraints(db, mv) {
2195
+ if (!mv.derivation.covers)
2196
+ return;
2197
+ const table = db.schemaManager.getTable(mv.derivation.covers.schemaName, mv.derivation.covers.tableName);
2198
+ if (!table?.uniqueConstraints)
2199
+ return;
2200
+ for (const uc of table.uniqueConstraints) {
2201
+ if (uc.coveringStructureName === mv.name)
2202
+ uc.coveringStructureName = undefined;
2203
+ }
2204
+ }
2205
+ /** Re-validates a stale MV's body against the current source schemas. Throws the
2206
+ * staleness diagnostic when the body no longer plans. Returns the optimized
2207
+ * relational root on success. */
2208
+ export function revalidateBody(db, mvName, bodySql) {
2209
+ let root;
2210
+ try {
2211
+ // Re-validate the body against the SOURCE schemas; suppress the read-side
2212
+ // rewrite so it is not re-pointed at this MV's own backing.
2213
+ root = db.schemaManager.withSuppressedMaterializedViewRewrite(() => db.getPlan(bodySql).getRelations()[0]);
2214
+ }
2215
+ catch (e) {
2216
+ const message = e instanceof Error ? e.message : String(e);
2217
+ throw new QuereusError(`materialized view '${mvName}' is stale; a source changed in an incompatible way — drop and recreate (${message})`, StatusCode.ERROR, e instanceof Error ? e : undefined);
2218
+ }
2219
+ if (!root) {
2220
+ throw new QuereusError(`materialized view '${mvName}' is stale; a source changed in an incompatible way — drop and recreate`, StatusCode.ERROR);
2221
+ }
2222
+ return root;
2223
+ }
2224
+ /* ──────────────── ALTER … RENAME propagation into MV bodies ──────────────── */
2225
+ /**
2226
+ * Lowercased `schema.name` keys of every MV that is stale *right now*. The rename
2227
+ * emitters snapshot this BEFORE the statement's first schema-change notify, so the
2228
+ * propagation pass can distinguish "stale from this very rename statement" (safe to
2229
+ * clear after a successful in-place rewrite — no DML can interleave within the
2230
+ * statement) from "stale from an earlier un-refreshed change" (the backing may
2231
+ * already be behind — writes during staleness are not maintained — so only a
2232
+ * successful REFRESH may clear it).
2233
+ */
2234
+ export function snapshotStaleMaterializedViews(db) {
2235
+ const out = new Set();
2236
+ for (const mv of db.schemaManager.getAllMaintainedTables()) {
2237
+ if (mv.derivation.stale)
2238
+ out.add(mvStaleKey(mv));
2239
+ }
2240
+ return out;
2241
+ }
2242
+ function mvStaleKey(mv) {
2243
+ return `${mv.schemaName}.${mv.name}`.toLowerCase();
2244
+ }
2245
+ /** All maintained tables registered in `schema`, snapshotted (the propagation
2246
+ * loops re-register tables mid-iteration). */
2247
+ function maintainedTablesOf(schema) {
2248
+ return Array.from(schema.getAllTables()).filter(isMaintainedTable);
2249
+ }
2250
+ /**
2251
+ * Rewrites every dependent materialized view in `schema` after a source TABLE
2252
+ * RENAME — the MV mirror of the plain-view loop in `propagateTableRenameInSchema`
2253
+ * ("MV ≡ faster view"): the caller applies the same same-schema gate, and the body
2254
+ * `selectAst` is mutated in place by the same `renameTableInAst` walker. An MV is
2255
+ * processed when its body AST changed, its `insert defaults` clause changed (an
2256
+ * expr subquery can name the renamed table even when the body doesn't), OR its
2257
+ * `sourceTables` carries the old base — the latter catches a body that reads the
2258
+ * renamed table *through a plain view* (the view's AST was rewritten by the view
2259
+ * loop, but this MV's own AST never names the table while its row-time plan is
2260
+ * still keyed under the old base).
2261
+ *
2262
+ * Per processed MV the derived fields are recomputed on a shallow clone
2263
+ * (`sourceTables` re-keyed old→new, `bodyHash`, regenerated `sql`, the `covers`
2264
+ * reverse link), then {@link applyMaterializedViewRewrite} re-registers row-time
2265
+ * maintenance / preserves pre-existing staleness and fires
2266
+ * `materialized_view_modified`. Failures mark the MV stale and propagation
2267
+ * continues — best-effort, like the rest of the rename propagation.
2268
+ */
2269
+ export async function propagateTableRenameToMaterializedViews(db, schema, renamedSchemaName, oldName, newName, preStale) {
2270
+ const schemaLower = renamedSchemaName.toLowerCase();
2271
+ const oldBase = `${schemaLower}.${oldName.toLowerCase()}`;
2272
+ const newBase = `${schemaLower}.${newName.toLowerCase()}`;
2273
+ for (const mv of maintainedTablesOf(schema)) {
2274
+ try {
2275
+ const d = mv.derivation;
2276
+ // The body walk also descends the trailing `with defaults (…)` clause
2277
+ // (now on `selectAst.defaults`), so a defaults-expr subquery naming the
2278
+ // renamed table flips `bodyChanged` even when the body never names it.
2279
+ const bodyChanged = renameTableInAst(d.selectAst, oldName, newName, renamedSchemaName);
2280
+ if (!bodyChanged && !d.sourceTables.includes(oldBase))
2281
+ continue;
2282
+ const covers = d.covers
2283
+ && d.covers.schemaName.toLowerCase() === schemaLower
2284
+ && d.covers.tableName.toLowerCase() === oldName.toLowerCase()
2285
+ ? { ...d.covers, tableName: newName }
2286
+ : d.covers;
2287
+ await applyMaterializedViewRewrite(db, schema, mv, {
2288
+ sourceTables: d.sourceTables.map(s => (s === oldBase ? newBase : s)),
2289
+ covers,
2290
+ }, preStale, /*renamedColumns*/ false);
2291
+ }
2292
+ catch (e) {
2293
+ failMaterializedViewRenamePropagation(db, schema, mv, e);
2294
+ }
2295
+ }
2296
+ }
2297
+ /**
2298
+ * Rewrites every dependent materialized view in `schema` after a source COLUMN
2299
+ * RENAME — the MV mirror of the plain-view loop in `propagateColumnRenameInSchema`
2300
+ * (same same-schema gate at the caller, same in-place `renameColumnInAst` walk).
2301
+ * The body walk also descends the trailing `with defaults (…)` clause (now on
2302
+ * `selectAst.defaults`): the clause target is typically a projected-away NOT NULL
2303
+ * column the body never mentions, so its rewrite still flips `bodyChanged` and
2304
+ * forces the re-hash / regenerate-DDL / fire-event path. An MV the walk does not
2305
+ * touch that the schema-change listener marked stale (an unreferenced-column
2306
+ * rename, a `select *` body) is restored by the
2307
+ * {@link restoreUnaffectedMaterializedViews} pass the ALTER emitter runs after
2308
+ * all per-schema loops. A changed BODY can shift the MV's *exposed output names*
2309
+ * (a bare passthrough projection of the renamed column — plain-view parity),
2310
+ * which {@link applyMaterializedViewRewrite} carries onto the live backing table.
2311
+ */
2312
+ export async function propagateColumnRenameToMaterializedViews(db, schema, renamedSchemaName, tableName, oldCol, newCol, preStale, resolveColumnInSource) {
2313
+ for (const mv of maintainedTablesOf(schema)) {
2314
+ try {
2315
+ const d = mv.derivation;
2316
+ // `resolveColumnInSource` keeps the body walk scope-aware for a defaults-expr
2317
+ // subquery referencing a like-named column on its own FROM — plain-view /
2318
+ // differ-reconcile parity (see `renameColumnInAst`).
2319
+ const bodyChanged = renameColumnInAst(d.selectAst, tableName, oldCol, newCol, renamedSchemaName, resolveColumnInSource);
2320
+ if (!bodyChanged)
2321
+ continue;
2322
+ await applyMaterializedViewRewrite(db, schema, mv, {}, preStale, /*renamedColumns*/ true);
2323
+ }
2324
+ catch (e) {
2325
+ failMaterializedViewRenamePropagation(db, schema, mv, e);
2326
+ }
2327
+ }
2328
+ }
2329
+ /**
2330
+ * The per-MV core both rename propagations share. `mv.selectAst` — including its
2331
+ * trailing `with defaults (…)` clause — has already been rewritten in place;
2332
+ * `overrides` carries the recomputed catalog fields — `sourceTables` / `covers`
2333
+ * (table rename). The remaining derived fields are recomputed on a shallow clone
2334
+ * (mirroring the tag setters — `oldObject` in the event shares the rewritten AST,
2335
+ * only the derived fields differ) and swapped into the catalog. The `bodyHash`
2336
+ * and regenerated `sql` both read the rewritten body (defaults included), so they
2337
+ * agree with each other and with what the differ recomputes from the post-rename
2338
+ * declared form; the `materialized_view_modified` → store re-persist path
2339
+ * round-trips the new name.
2340
+ *
2341
+ * Staleness discipline: `stale` means the row-time plan was released and the
2342
+ * backing may already be BEHIND, so a flag that predates this statement is never
2343
+ * cleared — the body/sql/hash/sources are still rewritten (a later REFRESH then
2344
+ * resolves the new name; today it cannot), but maintenance is NOT re-registered
2345
+ * and the backing columns are NOT renamed (refresh's shape-mismatch rebuild owns
2346
+ * that). An MV that was live before the statement is fully restored: backing
2347
+ * column names follow the body's output names (column rename only), row-time
2348
+ * maintenance re-plans against the already-renamed catalog (re-keying the
2349
+ * source-base index, recomputing `sourceScope`), and the staleness this very
2350
+ * statement's events set is cleared — no DML can interleave within the statement,
2351
+ * so the backing cannot be behind.
2352
+ */
2353
+ async function applyMaterializedViewRewrite(db, schema, mv, overrides, preStale, renamedColumns) {
2354
+ const wasPreStale = preStale.has(mvStaleKey(mv));
2355
+ const d = mv.derivation;
2356
+ const bodySql = astToString(d.selectAst);
2357
+ if (overrides.sourceTables)
2358
+ d.sourceTables = overrides.sourceTables;
2359
+ if ('covers' in overrides)
2360
+ d.covers = overrides.covers;
2361
+ // Canonical-definition hash (columns + body — the body string carries the
2362
+ // rewritten `with defaults (…)` clause) — must match the formula stamped at
2363
+ // create / recomputed by the differ, or every post-rename diff would churn a
2364
+ // spurious rebuild. `bodySql` also feeds renameShiftedBackingColumns below. The
2365
+ // DDL itself is rendered on demand from the unified record, so no stored `sql`.
2366
+ d.bodyHash = computeBodyHash(viewDefinitionToCanonicalString(d.columns, d.selectAst));
2367
+ if (!wasPreStale) {
2368
+ // Only a changed BODY can shift output names; a table rename / clause-only
2369
+ // change skips the backing-name pass (no re-plan needed).
2370
+ await restoreMaterializedViewLive(db, schema, mv, renamedColumns ? { bodySql } : undefined);
2371
+ }
2372
+ // Fired for still-stale MVs too: the rewritten body must re-persist so a
2373
+ // post-reopen REFRESH resolves the new name. The registered table object is
2374
+ // re-fetched — the backing-name pass may have swapped it.
2375
+ const live = schema.getTable(mv.name) ?? mv;
2376
+ db.schemaManager.getChangeNotifier().notifyChange({
2377
+ type: 'materialized_view_modified',
2378
+ schemaName: mv.schemaName,
2379
+ objectName: mv.name,
2380
+ oldObject: mv,
2381
+ newObject: live,
2382
+ });
2383
+ }
2384
+ /**
2385
+ * The shared restore tail both per-MV restore paths run — the changed-AST rewrite
2386
+ * ({@link applyMaterializedViewRewrite}) and the provably-unaffected restoration
2387
+ * pass ({@link restoreUnaffectedMaterializedViews}) — so the restore discipline
2388
+ * cannot drift between them: carry any body output-name shift onto the live
2389
+ * backing (`backingNames` present), re-register row-time maintenance, and only
2390
+ * then clear `stale`.
2391
+ *
2392
+ * `backingNames` is absent when the body's output names provably did not move (a
2393
+ * table rename / clause-only change), skipping the backing-name pass and its body
2394
+ * re-plan; when present, `shape` short-circuits the re-derivation for a caller
2395
+ * that already planned the body.
2396
+ */
2397
+ async function restoreMaterializedViewLive(db, schema, mv, backingNames) {
2398
+ if (backingNames) {
2399
+ await renameShiftedBackingColumns(db, schema, mv, backingNames.bodySql, backingNames.shape);
2400
+ }
2401
+ // Re-register BEFORE clearing `stale`: if registration throws, the caller's
2402
+ // failure path leaves the MV stale rather than serving an unmaintained backing.
2403
+ // Register the LIVE registered table (the backing-name pass may have swapped
2404
+ // the catalog object); the shared derivation rides either way.
2405
+ const live = schema.getTable(mv.name);
2406
+ db.registerMaterializedView(isMaintainedTable(live) ? live : mv);
2407
+ mv.derivation.stale = false;
2408
+ }
2409
+ /**
2410
+ * Restores every dependent MV that THIS rename statement marked stale but the
2411
+ * rename provably did not affect. Runs once at the end of the table-/column-rename
2412
+ * propagation, after all per-schema loops — so every body rewrite, backing-column
2413
+ * rename, and cascade event has already fired and the catalog is fully renamed.
2414
+ *
2415
+ * The schema-change listener marks **every** MV whose `sourceTables` includes a
2416
+ * `table_modified` table stale (and detaches its row-time plan), but the rename
2417
+ * propagation only restores MVs it processes (changed AST / clause, or — table
2418
+ * rename — `sourceTables` carrying the old base). An MV the rename does not touch
2419
+ * fell through stale-but-valid: reads silently served the now-unmaintained backing
2420
+ * and writes never propagated until a manual REFRESH. Three concrete shapes: a
2421
+ * column rename the body never references; a rename whose only effect on another
2422
+ * source is a constraint rewrite (e.g. an FK `references` target) firing that
2423
+ * source's `table_modified`; and a `select *` body whose output is a pure name
2424
+ * shift (the AST is unchanged, so the body rewrite never sees it).
2425
+ *
2426
+ * Per candidate (`stale` now, not stale at the pre-statement snapshot — a
2427
+ * pre-existing flag means the backing may be BEHIND and only REFRESH may clear it):
2428
+ * re-derive the backing shape from the body against the renamed catalog; a
2429
+ * **structural** mismatch is not a rename no-op → leave stale (REFRESH's
2430
+ * shape-mismatch rebuild owns it); otherwise run the shared restore tail —
2431
+ * {@link renameShiftedBackingColumns} carries a pure name shift onto the live
2432
+ * backing (no-op when names already match; its backing `table_modified`
2433
+ * deliberately cascades staleness to chained MVs referencing the old output name),
2434
+ * then re-register row-time maintenance and clear `stale`.
2435
+ *
2436
+ * Deliberately fires NO `materialized_view_modified`: the MV record (AST, hash,
2437
+ * sql, sourceTables) is unchanged here — `stale` is runtime state, not persisted.
2438
+ * Walks all schemas (the listener marks cross-schema dependents too), in creation
2439
+ * order — topological for same-schema MV chains, so a producer restores before its
2440
+ * consumer is examined. A chained MV whose body references a renamed-away producer
2441
+ * output name fails shape derivation and stays stale (staleness-diagnostic parity
2442
+ * with a broken plain-view chain). Best-effort like the rest of the propagation:
2443
+ * a per-MV failure logs, leaves that MV stale, and continues.
2444
+ */
2445
+ export async function restoreUnaffectedMaterializedViews(db, preStale) {
2446
+ for (const mv of db.schemaManager.getAllMaintainedTables()) {
2447
+ if (!mv.derivation.stale || preStale.has(mvStaleKey(mv)))
2448
+ continue;
2449
+ try {
2450
+ const schema = db.schemaManager.getSchemaOrFail(mv.schemaName);
2451
+ const d = mv.derivation;
2452
+ const bodySql = astToString(d.selectAst);
2453
+ // Throws when the body no longer plans against the renamed catalog
2454
+ // (e.g. a chained MV referencing a renamed-away output name) → catch
2455
+ // below leaves it stale.
2456
+ const shape = deriveBackingShape(db, bodySql, d.columns);
2457
+ // The retry of a failure-marked MV must not revive an inconsistent record: a
2458
+ // rewrite that threw between the in-place AST mutation and the derived-field
2459
+ // re-key leaves the OLD derivation (un-re-keyed `sourceTables`) holding the
2460
+ // rewritten body. Registering that would compute `sourceScope` (and key the
2461
+ // read-side rewrite) off the wrong bases — leave it stale instead.
2462
+ if (!sameSourceTables(d.sourceTables, shape.sourceTables)) {
2463
+ log('Leaving materialized view %s.%s stale after rename: recorded sourceTables disagree with the re-planned body — REFRESH recovers', mv.schemaName, mv.name);
2464
+ continue;
2465
+ }
2466
+ const backing = schema.getTable(mv.name);
2467
+ if (!backing) {
2468
+ throw new QuereusError(`Internal error: maintained table '${mv.name}' not found during restore`, StatusCode.INTERNAL);
2469
+ }
2470
+ const mismatch = describeBackingShapeMismatch(backing, shape);
2471
+ if (mismatch) {
2472
+ log('Leaving materialized view %s.%s stale after rename: backing shape mismatch (%s) — REFRESH rebuilds it', mv.schemaName, mv.name, mismatch);
2473
+ continue;
2474
+ }
2475
+ await restoreMaterializedViewLive(db, schema, mv, { bodySql, shape });
2476
+ }
2477
+ catch (e) {
2478
+ log('Could not restore materialized view %s.%s after rename; leaving it stale: %s', mv.schemaName, mv.name, e instanceof Error ? e.message : String(e));
2479
+ }
2480
+ }
2481
+ }
2482
+ /** Set-equality over qualified (already-lowercased) source-table lists. Order is
2483
+ * irrelevant — both sides come from `collectSourceTables`' Set walk. */
2484
+ function sameSourceTables(a, b) {
2485
+ if (a.length !== b.length)
2486
+ return false;
2487
+ const set = new Set(a);
2488
+ return b.every(s => set.has(s));
2489
+ }
2490
+ /**
2491
+ * Carries a column-rename-induced output-name shift onto the MV's live backing
2492
+ * table. The backing's column names were derived from the body's output names at
2493
+ * create ({@link deriveBackingShape}); after the body rewrite a bare passthrough
2494
+ * projection of the renamed column exposes the NEW name, so the backing follows —
2495
+ * positionally, data-preserving, via the host module's own `alterTable` (a host
2496
+ * without `alterTable` throws UNSUPPORTED and the caller's failure path leaves
2497
+ * the MV stale). Explicit-column MVs (`mv(a, b)`) and
2498
+ * expression-aliased outputs produce no mismatch and no-op. Any structural
2499
+ * difference (count / types / PK) is NOT a rename outcome — throw so the caller's
2500
+ * failure path leaves the MV stale rather than rebuilding data here.
2501
+ *
2502
+ * The backing `table_modified` fired on a real rename deliberately cascades: a
2503
+ * chained MV whose body references the OLD output name is marked stale by the
2504
+ * manager's listener and surfaces the staleness diagnostic on its next read
2505
+ * (parity with a broken plain-view chain — strictly better than silently freezing),
2506
+ * and cached plans scanning the backing directly recompile against the new names.
2507
+ */
2508
+ async function renameShiftedBackingColumns(db, schema, mv, bodySql, preDerivedShape) {
2509
+ const shape = preDerivedShape ?? deriveBackingShape(db, bodySql, mv.derivation.columns);
2510
+ const backing = schema.getTable(mv.name);
2511
+ if (!backing) {
2512
+ throw new QuereusError(`Internal error: maintained table '${mv.name}' not found during backing-column rename`, StatusCode.INTERNAL);
2513
+ }
2514
+ const mismatch = describeBackingShapeMismatch(backing, shape);
2515
+ if (mismatch) {
2516
+ throw new QuereusError(`materialized view '${mv.schemaName}.${mv.name}': source column rename shifted the body's backing shape structurally (beyond a pure name shift): ${mismatch}`, StatusCode.INTERNAL);
2517
+ }
2518
+ const module = requireVtabModule(backing);
2519
+ let current = backing;
2520
+ for (let i = 0; i < shape.columns.length; i++) {
2521
+ const liveCol = current.columns[i];
2522
+ const newName = shape.columns[i].name;
2523
+ if (liveCol.name.toLowerCase() === newName.toLowerCase())
2524
+ continue;
2525
+ if (!module.alterTable) {
2526
+ throw new QuereusError(`module for backing table '${backing.name}' does not support ALTER TABLE`, StatusCode.UNSUPPORTED);
2527
+ }
2528
+ current = await module.alterTable(db, mv.schemaName, backing.name, {
2529
+ type: 'renameColumn',
2530
+ oldName: liveCol.name,
2531
+ newName,
2532
+ newColumnDefAst: backingColumnDef(liveCol, newName),
2533
+ });
2534
+ }
2535
+ if (current !== backing) {
2536
+ // The module's alterTable returns a fresh TableSchema that does NOT carry
2537
+ // the derivation or the catalog-only tags — re-graft both so the registered
2538
+ // record stays maintained and keeps its tags.
2539
+ const renamed = graftReshapedRecord(current, mv);
2540
+ schema.addTable(renamed);
2541
+ db.schemaManager.getChangeNotifier().notifyChange({
2542
+ type: 'table_modified',
2543
+ schemaName: mv.schemaName,
2544
+ objectName: backing.name,
2545
+ oldObject: backing,
2546
+ newObject: renamed,
2547
+ });
2548
+ }
2549
+ }
2550
+ /** Minimal ColumnDef AST for a backing-column rename. Backing columns carry only
2551
+ * type / not-null / PK / collation — never defaults or generated expressions
2552
+ * (see {@link buildBackingTableSchema}) — so the lift is total. */
2553
+ function backingColumnDef(col, newName) {
2554
+ const constraints = [col.notNull ? { type: 'notNull' } : { type: 'null' }];
2555
+ if (col.primaryKey)
2556
+ constraints.push({ type: 'primaryKey', direction: col.pkDirection });
2557
+ if (col.collation && col.collation !== 'BINARY')
2558
+ constraints.push({ type: 'collate', collation: col.collation });
2559
+ return { name: newName, dataType: col.logicalType.name, constraints };
2560
+ }
2561
+ /**
2562
+ * Failure path for one MV's rename rewrite: whatever partial state the rewrite
2563
+ * reached (AST possibly mutated, catalog record possibly swapped), the MV must not
2564
+ * keep serving its backing as if live — force-mark it stale, release its row-time
2565
+ * plan, and invalidate cached backing reads so the next reference re-hits the
2566
+ * build-time stale guard. A pre-existing stale flag is unaffected (it is never
2567
+ * cleared here). The caller continues with the remaining MVs.
2568
+ */
2569
+ function failMaterializedViewRenamePropagation(db, schema, mv, cause) {
2570
+ log('Rename propagation failed for materialized view %s.%s; leaving it stale: %s', mv.schemaName, mv.name, cause instanceof Error ? cause.message : String(cause));
2571
+ // A swap may or may not have landed before the throw — mark whichever object
2572
+ // the catalog currently holds (the shared derivation rides either).
2573
+ const live = schema.getTable(mv.name);
2574
+ db.markMaterializedViewStale(isMaintainedTable(live) ? live : mv);
2575
+ }
2576
+ //# sourceMappingURL=materialized-view-helpers.js.map