@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,2915 @@
1
+ import { resolveReferencedColumns } from '../../schema/table.js';
2
+ import { PlanNode } from '../nodes/plan-node.js';
3
+ import { sqlValuesEqual } from '../../util/comparison.js';
4
+ import { TableReferenceNode, ColumnReferenceNode } from '../nodes/reference.js';
5
+ import { InternalRecursiveCTERefNode } from '../nodes/internal-recursive-cte-ref-node.js';
6
+ import { analyzeBodyLineage } from './backward-body.js';
7
+ import { buildExpression } from '../building/expression.js';
8
+ import { columnSchemaToScalarType } from '../type-utils.js';
9
+ import { JoinNode } from '../nodes/join-node.js';
10
+ import { EXISTENCE_FLAG_TYPE } from '../nodes/join-utils.js';
11
+ import { FilterNode } from '../nodes/filter.js';
12
+ import { ProjectNode } from '../nodes/project-node.js';
13
+ import { raiseMutationDiagnostic } from './mutation-diagnostic.js';
14
+ import { combineAnd, flattenAnd, makeViewColumnDescend, assertTopLevelViewColumns, raiseUnknownViewColumn, SELF_ALIAS, assertReturningStarQualifier } from './single-source.js';
15
+ import { transformExpr, cloneExpr, mapQueryExprUniform, substituteNewRefs, transformScopedExpr, transformAliasScopedExpr } from './scope-transform.js';
16
+ import { requireValidatedNewRefIndex } from '../analysis/authored-inverse.js';
17
+ /**
18
+ * Multi-source view-mediated DML decomposition — the **key-preserving join**
19
+ * acceptance case of the view-mutation substrate (docs/view-updateability.md
20
+ * § Per-Operator Semantics — Inner Join, § Outer Joins, § Multi-Base-Table Mutations).
21
+ *
22
+ * Scope: a view body that is an **n-way (≥2) equi-join** of base tables — `inner`,
23
+ * `left`, or `full` — including composite-PK sides and **self-joins** (one base table
24
+ * under two or more distinct aliases) — written through with `update` / `delete` /
25
+ * `insert`. **LEFT** and **RIGHT** outer joins are admitted for the
26
+ * **statically-expressible** cases: preserved-side update passthrough,
27
+ * delete-to-the-preserved-side, and insert routing (both-side / preserved-only /
28
+ * presence-gated non-preserved member). RIGHT is the exact **mirror** of LEFT — the
29
+ * right operand of a `right` join is preserved, the left operand is null-extended — and
30
+ * the runtime now executes a RIGHT join (`outer-join-right-full-runtime`), so a
31
+ * RIGHT-join view is both readable and writable. **FULL** is admitted only to carry its
32
+ * precise conservative diagnostics (no preserved side, so all writes reject and the
33
+ * surfaces report all-`NO`; FULL write-through — a preserved anchor for a body that is
34
+ * null-extended per row — is a separable future concern). The one outer-join case that
35
+ * needs new runtime — an UPDATE of a **non-preserved** column (a per-row matched-update /
36
+ * null-extended-insert branch) — defers with `unsupported-outer-join-update`
37
+ * (`view-write-optional-member-transitions`). The body is
38
+ * **planned once**
39
+ * (`analyzeJoinView`); its `PhysicalProperties.updateLineage` (threaded by
40
+ * `view-mutation-physical-lineage`) routes each output column to its owning base
41
+ * table. Each per-base SET/value is still lowered to an AST `BaseOp` so the ordinary
42
+ * base-table builders are reused verbatim (the documented lower-risk path — the base
43
+ * builders stay untouched), but **row identification no longer round-trips through a
44
+ * re-planned AST body**: every affected view row's base-PK identities are captured
45
+ * ONCE up-front, built as plan nodes directly over the already-planned join body
46
+ * (`Project_{k<side>}(Filter_{idPred}(joinNode))` — the derived backward walk the
47
+ * docs name, § Round-Trip Laws and the Derived Backward Walk), materialized before
48
+ * any base op fires, and each base op reads its identifying values back from that
49
+ * `__vmupd_keys` set:
50
+ *
51
+ * ```sql
52
+ * -- view: select j1.id as id, j1.a as a, j2.c as c
53
+ * -- from tj1 j1 join tj2 j2 on j2.id = j1.t2id
54
+ * update jv set a = 5, c = 9 where id = 3
55
+ * -- capture (plan nodes over the planned join body, materialized once):
56
+ * -- __vmupd_keys = π_{j1.id as k0, j2.id as k1}( σ_{id = 3}( tj1 ⋈ tj2 ) )
57
+ * -> update tj1 set a = 5 where id in (select k0 from __vmupd_keys)
58
+ * update tj2 set c = 9 where id in (select k1 from __vmupd_keys)
59
+ * ```
60
+ *
61
+ * The capture reconstructs the row-identifying predicate (each owning side's base PK)
62
+ * from the planned join body — exactly the predicate the optimizer already proves a
63
+ * key over — so a side whose own PK is hidden by the projection (`tj2.id` above) is
64
+ * still addressable, and a both-sides write is mutation-order-independent (the
65
+ * FK-parent op cannot rewrite a predicate column out from under the FK-child op).
66
+ * UPDATE RETURNING re-queries the same planned `joinNode` (post-mutation, restricted
67
+ * to the captured identities); DELETE RETURNING projects the planned body `root` (the
68
+ * `pre` OLD image). The body is planned once and reused — no second `buildSelectStmt`
69
+ * / `cloneFromClause` of it for identification or RETURNING.
70
+ *
71
+ * Multi-source **`insert`** is analysed here (`analyzeMultiSourceInsert`) but *built* by
72
+ * `building/view-mutation-builder.ts` (`buildMultiSourceInsert`), because it needs the
73
+ * plan-level shared-surrogate envelope rather than an AST `BaseOp`: the shared join key is
74
+ * not a view column, so it is sourced from the anchor key column's declared `default` once
75
+ * per row at the envelope and threaded into the active base inserts via the equivalence
76
+ * class (§ Mutation Context). For an outer join the **non-preserved** side is an *optional*
77
+ * member of the fan-out — dropped when its columns are absent (the preserved-only insert),
78
+ * presence-gated per row when supplied (the both-side insert).
79
+ *
80
+ * **Deferred, rejected here with a structured diagnostic:**
81
+ * - UPDATE of a non-preserved outer-join column — `unsupported-outer-join-update`.
82
+ * - INSERT of only non-preserved columns with no preserved anchor — `null-extended-create-conflict`.
83
+ * - cross / set-op / aggregate / window bodies — `unsupported-*`.
84
+ * - comma (implicit) joins, `select *` join bodies, and cross-side `set` value
85
+ * references — each a precise diagnostic.
86
+ * - composite **shared-key insert** (the surrogate envelope threads a single-column
87
+ * key) — `unsupported-decomposition-key`. (Composite-PK *identification* on the
88
+ * update/delete capture path IS supported here; only the insert envelope's shared
89
+ * key stays single-column.)
90
+ */
91
+ /**
92
+ * The shared identity-capture CTE name for a multi-source (n-way inner-join) UPDATE /
93
+ * multi-side DELETE fan-out. Each affected view row's base-PK identities are
94
+ * materialized ONCE — *before* any base op fires — into `rctx.tableContexts` under a
95
+ * shared descriptor. *Every* per-side base op reads its identifying values back from
96
+ * this set via a correlated EXISTS (`exists (select 1 from __vmupd_keys k where
97
+ * k.k<side>_<j> = <side>.<pk<j>> …)`) instead of a live re-query of the join body, so
98
+ * the first op cannot empty the join — or rewrite a predicate column — out from under
99
+ * a later op's identifying subquery (a mutation-order-independent identity). The same
100
+ * capture backs the UPDATE RETURNING re-query (docs/view-updateability.md § Inner Join,
101
+ * § `returning`).
102
+ *
103
+ * The capture relation carries one column **per side per PK column**, named
104
+ * `k<sideIndex>_<pkColumnOrdinal>` ({@link keyColumnName}) — so a composite-PK side
105
+ * contributes `k<side>_0, k<side>_1, …`. This flattened per-side-per-column shape is
106
+ * what generalizes the substrate past the retired single-column `(k0, k1)` tuple.
107
+ */
108
+ export const MS_UPDATE_KEYS_CTE = '__vmupd_keys';
109
+ /**
110
+ * The capture column name for side `sideIndex`'s `j`-th PK column. A single-column-PK
111
+ * side yields just `k<side>_0`; a composite-PK side yields `k<side>_0, k<side>_1, …`.
112
+ */
113
+ export function keyColumnName(sideIndex, j) {
114
+ return `k${sideIndex}_${j}`;
115
+ }
116
+ // --- entry ----------------------------------------------------------------
117
+ /**
118
+ * True when the view body is a join (and so routes to this multi-source path
119
+ * rather than the single-source spine). Cheap AST peek — no plan built.
120
+ *
121
+ * A **compound (set-op) body** returns `false` even when its left-most leg's FROM is a
122
+ * join (`select … from a join b … union …`): a set-op body routes to the set-op write path
123
+ * (`set-op.ts`), never this multi-source join spine, whose capture/lineage walk reads only
124
+ * the leg's `JoinNode` and silently ignores the surrounding compound — mishandling it
125
+ * (`set-op-write-multisource-leg-reject`). Excluding it here lets such a body fall through to
126
+ * the single-source spine's clean `classifyViewBody` reject (the intent
127
+ * `propagate.ts` already documents). The set-op path itself detects a multi-source LEG by
128
+ * calling this same predicate on the **leg** SELECT (which carries no `compound`).
129
+ */
130
+ export function isJoinBody(selectAst) {
131
+ if (selectAst.type !== 'select' || !selectAst.from)
132
+ return false;
133
+ if (selectAst.compound)
134
+ return false;
135
+ return selectAst.from.length > 1 || selectAst.from.some(f => f.type === 'join');
136
+ }
137
+ /**
138
+ * Non-throwing AST shape check — the boolean shadow of {@link collectJoinSources}'s
139
+ * acceptance: `true` iff the body is a single explicit **n-way (≥2) equi-join** — `inner`,
140
+ * `left`, `right`, or `full` (RIGHT is the exact mirror of LEFT; FULL has no preserved
141
+ * side, so it self-conservatizes downstream; see {@link collectJoinSources}) with an ON
142
+ * (or USING) predicate over plain base tables (the
143
+ * exact multi-source shape `propagate()` decomposes), including **composite-PK sides** and
144
+ * **self-joins** (one base table under two or more distinct aliases). Every other
145
+ * multi-table body — cross / comma (implicit) / subquery- or function-source — returns
146
+ * `false`.
147
+ *
148
+ * Shared with the static updateability surfaces (`deriveViewInfo` /
149
+ * `deriveColumnInfo` in `func/builtins/schema.ts`): they gate on this so they
150
+ * agree with what a real mutation through the view accepts. An outer join is now
151
+ * **partially** writable (preserved-side update, delete-to-preserved, insert), so it is
152
+ * decomposable here; those surfaces read per-column `null-extended` lineage to report a
153
+ * non-preserved column non-updatable (the matching deferral). The throwing
154
+ * {@link collectJoinSources} stays the substrate's source of truth; this mirrors only its
155
+ * AST-level shape gate (it does not re-check DISTINCT/LIMIT/`select *`, which are deeper
156
+ * semantic rejects handled downstream — PK shape is no longer a reject now that composite
157
+ * keys are admitted).
158
+ */
159
+ export function isDecomposableJoinBody(selectAst) {
160
+ if (selectAst.type !== 'select' || !selectAst.from)
161
+ return false;
162
+ const from = selectAst.from;
163
+ if (from.length !== 1 || from[0].type !== 'join')
164
+ return false;
165
+ let tableCount = 0;
166
+ const visit = (fc) => {
167
+ switch (fc.type) {
168
+ case 'table':
169
+ tableCount += 1;
170
+ return true;
171
+ case 'join': {
172
+ // INNER / LEFT / RIGHT / FULL join with an explicit ON predicate or a USING column
173
+ // list. RIGHT is now **admitted**: the runtime reads a RIGHT join and write-through
174
+ // recognition mirrors LEFT (the right of a `right` is preserved, the left
175
+ // null-extended; `view-write-right-join-readmit`). FULL is admitted but has no
176
+ // preserved side, so it self-conservatizes downstream (no false positive); FULL
177
+ // write-through is a separable future concern.
178
+ const accepted = fc.joinType === 'inner' || fc.joinType === 'left' || fc.joinType === 'right' || fc.joinType === 'full';
179
+ if (!accepted || (!fc.condition && !(fc.columns && fc.columns.length > 0)))
180
+ return false;
181
+ return visit(fc.left) && visit(fc.right);
182
+ }
183
+ default:
184
+ return false; // subquery / function source — not a plain base table
185
+ }
186
+ };
187
+ if (!visit(from[0]))
188
+ return false;
189
+ // ≥2 plain base tables. A self-join (the same base table under distinct aliases) is
190
+ // now accepted; routing is alias-keyed downstream, so the table names need not be
191
+ // distinct here.
192
+ return tableCount >= 2;
193
+ }
194
+ /**
195
+ * True iff `selectAst` is a decomposable join body ({@link isDecomposableJoinBody}) whose
196
+ * joins are **all INNER**. The set-op join-leg compose
197
+ * (`set-op-write-multisource-leg-compose`) ships INNER join legs — the gate is purely
198
+ * `joinType`-based, so a non-equi (theta) INNER join is admitted identically to an equi-join.
199
+ * An OUTER (left/right/full) join leg's set-op write composition is deferred. The set-op
200
+ * recognizers gate on this so a body with an outer-join leg reports the conservative
201
+ * all-`NO` static surface AND rejects the dynamic write cleanly (never an internal error),
202
+ * matching exactly. A cross / no-ON join (not decomposable) likewise fails this and is
203
+ * deferred — agreeing with `analyzeJoinView`'s downstream reject.
204
+ */
205
+ export function isInnerJoinBody(selectAst) {
206
+ if (!isDecomposableJoinBody(selectAst))
207
+ return false;
208
+ const from = selectAst.from;
209
+ const allInner = (fc) => fc.type !== 'join' || (fc.joinType === 'inner' && allInner(fc.left) && allInner(fc.right));
210
+ return allInner(from[0]);
211
+ }
212
+ /**
213
+ * Decompose a multi-source (n-way `inner`/`left`/`full` join) view mutation into
214
+ * an ordered `BaseOp[]`. Throws a structured diagnostic for any unsupported shape.
215
+ */
216
+ export function propagateMultiSource(ctx, view, req) {
217
+ // Validate the join shape first (this rejects cross/comma joins, non-table sources,
218
+ // etc. with a `cannot write through view` diagnostic), so every unsupported join —
219
+ // including an `insert` through one — surfaces the precise shape reason before the
220
+ // op-specific handling. Outer joins are admitted: an UPDATE of a non-preserved column
221
+ // defers (`unsupported-outer-join-update`), DELETE routes to the preserved side(s).
222
+ const analysis = analyzeJoinView(ctx, view);
223
+ switch (req.op) {
224
+ case 'update': return decomposeUpdate(ctx, view, analysis, req.stmt);
225
+ case 'delete': return decomposeDelete(ctx, view, analysis, req.stmt);
226
+ case 'insert':
227
+ // Insert needs the plan-level shared-surrogate envelope, so it is built
228
+ // directly by `building/view-mutation-builder.ts` (`buildMultiSourceInsert`,
229
+ // off `analyzeMultiSourceInsert` below), not lowered to AST BaseOps here.
230
+ // `buildViewMutation` routes a join insert there before `propagate` runs,
231
+ // so this case is unreachable on the supported path.
232
+ raiseMutationDiagnostic({
233
+ reason: 'unsupported-multisource-insert',
234
+ table: view.name,
235
+ message: `internal: multi-source insert must be built via buildMultiSourceInsert, not propagate`,
236
+ });
237
+ }
238
+ }
239
+ /**
240
+ * Decompose an n-way key-preserving INSERT into the per-side base inserts plus the
241
+ * shared-surrogate envelope they fan out from. Throws a structured diagnostic for any
242
+ * unsupported shape (computed target column, a not-null base column with no value, a
243
+ * non-equi-join key, …). The shared key remains **single-column**: a side contributing a
244
+ * composite shared key to the join's equivalence class is rejected with
245
+ * `unsupported-decomposition-key` (the envelope threads one key value per row).
246
+ *
247
+ * **Outer joins** (§ Outer Joins — Inserts): a **non-preserved** side is an *optional*
248
+ * member of the fan-out. A side whose columns are all absent emits no insert (it is
249
+ * dropped); a non-preserved side that IS supplied is presence-gated per row (it inserts
250
+ * only for rows supplying ≥1 of its columns). The shared key is minted/threaded only when
251
+ * ≥2 sides are active (the preserved-only case is a single preserved insert — the row
252
+ * reads back null-extended); an insert supplying *only* non-preserved columns, with no
253
+ * preserved anchor row to attach to, is rejected `null-extended-create-conflict` (v1).
254
+ */
255
+ export function analyzeMultiSourceInsert(ctx, view, stmt) {
256
+ rejectReturning(view, stmt.returning);
257
+ const analysis = analyzeJoinView(ctx, view);
258
+ const { sides, outColumns } = analysis;
259
+ const keyColumns = extractJoinKeyColumns(view, analysis.sel, sides);
260
+ // Constant-FD insert-defaults from the join body's σ (where-clause): each `column =
261
+ // literal` conjunct, resolved to its owning join side, is lifted as a per-side
262
+ // insert-default (the side-aware analog of single-source `extractFilterConstants`). An
263
+ // omitted σ-constrained column is then supplied its σ constant so the inserted row
264
+ // satisfies the view predicate and is visible through the view; an explicit value that
265
+ // contradicts the constant rejects at plan time below (§ Inner Join — Inserts).
266
+ const filterConstants = extractJoinFilterConstants(analysis.sel.where, sides);
267
+ // Supplied view columns: the explicit list, or every base-routed (identity, rename,
268
+ // or outer-join null-extended) view output column. An `inverse`-profile column
269
+ // (writable on the UPDATE path) is NOT insertable — the shared-surrogate envelope
270
+ // writes supplied values to base columns verbatim, with no hook to apply the column's
271
+ // inverse, so an inserted `cv1` would land raw in `cv`. Excluding it from the implicit
272
+ // set lets it fall to its base default / not-null check; an explicit supply is
273
+ // rejected below. A non-preserved (null-extended) base column IS insertable here (the
274
+ // both-sides envelope supplies it), so it is included even though it is read-only on
275
+ // the UPDATE path.
276
+ const suppliedNames = stmt.columns && stmt.columns.length > 0
277
+ ? stmt.columns
278
+ : outColumns.filter(c => c.sideIndex !== undefined && c.baseColumn !== undefined && !c.inverse).map(c => c.name);
279
+ const supplied = suppliedNames.map((rawName, columnIndex) => {
280
+ const name = rawName.toLowerCase();
281
+ const out = outColumns.find(c => c.name === name);
282
+ // An existence flag is consumed as a routing directive, not stored. Pull its uniform
283
+ // boolean literal out of the VALUES source (`true` ⇒ insert the non-preserved side;
284
+ // `false` ⇒ omit it / preserved-only). It stays an envelope column for arity but is
285
+ // never a base target (no `sideIndex`/`baseColumn`).
286
+ if (out?.existenceComponent) {
287
+ if (out.existenceSide === undefined) {
288
+ raiseMutationDiagnostic({
289
+ reason: 'unsupported-outer-join-update',
290
+ column: rawName,
291
+ table: view.name,
292
+ message: `cannot insert through view '${view.name}': the existence column '${rawName}' does not resolve to a single non-preserved side (an ambiguous / full-outer existence shape is deferred)`,
293
+ });
294
+ }
295
+ const existenceFlag = existenceInsertFlag(view, stmt, columnIndex, rawName);
296
+ return { name, type: EXISTENCE_FLAG_TYPE, isKey: false, existenceSide: out.existenceSide, existenceFlag };
297
+ }
298
+ // Evaluating an authored (`with inverse`) column's puts through the multi-source
299
+ // shared-surrogate envelope is deferred (the envelope projects supplied columns
300
+ // verbatim per side; per-row put evaluation over it is a follow-up — recorded in
301
+ // docs/view-updateability.md § Authored inverses). Name the deferral precisely
302
+ // rather than letting it fall into the generic non-insertable reject below.
303
+ if (out?.authored) {
304
+ raiseMutationDiagnostic({
305
+ reason: 'no-inverse',
306
+ column: rawName,
307
+ table: view.name,
308
+ message: `cannot insert through view '${view.name}': column '${rawName}' carries an authored inverse (WITH INVERSE); evaluating authored puts through a join view's insert envelope is deferred — insert into the base tables directly`,
309
+ });
310
+ }
311
+ // A base-routed column (identity/rename or outer-join null-extended) carries
312
+ // `sideIndex` + `baseColumn`; a computed column does not, and an `inverse` column
313
+ // cannot store a raw value. Either of the latter is non-insertable.
314
+ if (!out || out.inverse || out.sideIndex === undefined || !out.baseColumn) {
315
+ raiseMutationDiagnostic({
316
+ reason: 'no-inverse',
317
+ column: rawName,
318
+ table: view.name,
319
+ message: `cannot insert through view '${view.name}': column '${rawName}' is computed (non-invertible), a transformed (invertible) column, or not a base column, so it cannot receive an inserted value`,
320
+ });
321
+ }
322
+ const sideIndex = out.sideIndex;
323
+ const baseCol = columnByName(sides[sideIndex].schema, out.baseColumn);
324
+ const isKey = out.baseColumn.toLowerCase() === keyColumns[sideIndex].toLowerCase();
325
+ return { name, sideIndex, baseColumn: out.baseColumn, type: columnSchemaToScalarType(baseCol), isKey };
326
+ });
327
+ // A FULL outer join has no preserved anchor side to mint/thread the shared key from
328
+ // (every side is null-extended per row), so a statically-routed insert is not
329
+ // expressible — defer it (the static `view_info` surface short-circuits the same body
330
+ // to all-`NO`).
331
+ const hasPreservedSide = sides.some(s => s.preserved);
332
+ if (!hasPreservedSide) {
333
+ raiseMutationDiagnostic({
334
+ reason: 'unsupported-join',
335
+ table: view.name,
336
+ message: `cannot insert through view '${view.name}': a FULL outer join has no preserved anchor side to mint/thread the shared key from; inserting through a full-outer view is deferred`,
337
+ });
338
+ }
339
+ // Existence directives (§ Existence columns — Inserts): an `exists … as` flag forces its
340
+ // non-preserved side active (`true`) or inactive (`false`), overriding the columns-
341
+ // supplied inference. A `false` directive on a side whose columns ARE supplied, or a
342
+ // `true`+`false` collision, contradicts — reject rather than silently pick one.
343
+ const forcedActive = new Set();
344
+ const forcedInactive = new Set();
345
+ for (const s of supplied) {
346
+ if (s.existenceSide === undefined)
347
+ continue;
348
+ (s.existenceFlag ? forcedActive : forcedInactive).add(s.existenceSide);
349
+ }
350
+ const baseSupplied = supplied.filter((s) => s.sideIndex !== undefined);
351
+ const suppliedSides = new Set(baseSupplied.map(s => s.sideIndex));
352
+ for (const i of forcedInactive) {
353
+ if (forcedActive.has(i) || suppliedSides.has(i)) {
354
+ raiseMutationDiagnostic({
355
+ reason: 'conflicting-assignment',
356
+ table: view.name,
357
+ message: `cannot insert through view '${view.name}': an existence flag is false (omit base table '${sides[i].schema.name}') but the same insert ${forcedActive.has(i) ? 'also sets that flag true' : 'supplies one of its columns'} — the two contradict`,
358
+ });
359
+ }
360
+ }
361
+ // Active sides: a preserved (or inner) side is always inserted (the anchor row); a
362
+ // non-preserved (outer) side is active when ≥1 of its columns is supplied OR an
363
+ // existence flag forces it (`true`). A `false` directive forces it inactive even if a
364
+ // stray column slipped through (already rejected above). An absent non-preserved side
365
+ // emits no insert (the per-row null-extension semantics).
366
+ const isActive = (i) => forcedInactive.has(i) ? false : (forcedActive.has(i) || sides[i].preserved || suppliedSides.has(i));
367
+ const activeIndices = sides.map((_, i) => i).filter(isActive);
368
+ // Non-preserved-only reject (§ Outer Joins — Inserts): activating only a non-preserved
369
+ // side (columns supplied or `hasB = true`), with no preserved anchor row to mint/thread
370
+ // the shared key from, is not yet expressible (the envelope sources the key from the
371
+ // preserved anchor).
372
+ const anyNonPreservedActive = sides.some((s, i) => !s.preserved && isActive(i));
373
+ const anyPreservedSupplied = baseSupplied.some(s => sides[s.sideIndex].preserved);
374
+ if (anyNonPreservedActive && !anyPreservedSupplied) {
375
+ raiseMutationDiagnostic({
376
+ reason: 'null-extended-create-conflict',
377
+ table: view.name,
378
+ message: `cannot insert through view '${view.name}': only non-preserved-side columns were supplied through the outer join, with no preserved-side row to attach to; supply the preserved side's columns too (the shared key is minted/threaded from the preserved anchor)`,
379
+ });
380
+ }
381
+ // The shared key relates two or more active sides; with only one active side (the
382
+ // preserved-only insert) no key is needed — the single side inserts and the row reads
383
+ // back null-extended.
384
+ const needsSharedKey = activeIndices.length >= 2;
385
+ // The shared key is either directly supplied (a supplied view column maps to a
386
+ // join-key base column) or sourced from the anchor key column's declared `default`,
387
+ // evaluated once per row at the envelope and EC-threaded into the active sides. The
388
+ // engine mints nothing of its own — the basis author declares the policy
389
+ // (docs/view-updateability.md § Mutation Context).
390
+ const suppliedKeys = supplied.filter(s => s.isKey);
391
+ if (suppliedKeys.length > 1) {
392
+ raiseMutationDiagnostic({
393
+ reason: 'unsupported-join',
394
+ table: view.name,
395
+ message: `cannot insert through view '${view.name}': the shared join key is exposed by more than one view column (${suppliedKeys.map(s => `'${s.name}'`).join(', ')}); supply it through a single view column`,
396
+ });
397
+ }
398
+ const suppliedKeyIndex = supplied.findIndex(s => s.isKey);
399
+ let keyDefault;
400
+ let keyEnvelopeIndex = -1;
401
+ if (needsSharedKey) {
402
+ if (suppliedKeyIndex >= 0) {
403
+ keyEnvelopeIndex = suppliedKeyIndex;
404
+ }
405
+ else {
406
+ keyEnvelopeIndex = supplied.length; // the default-sourced column is appended last
407
+ // The anchor is the FK-root among the **active** sides (so a dropped optional
408
+ // member never seeds the surrogate); its key column's declared default sources
409
+ // the minted value.
410
+ const anchorIndex = orderSides(sides).find(isActive);
411
+ const anchorKeyCol = columnByName(sides[anchorIndex].schema, keyColumns[anchorIndex]);
412
+ keyDefault = requireKeyDefault(view, sides[anchorIndex].schema, anchorKeyCol);
413
+ }
414
+ }
415
+ // Per active side: the shared key (when needed) plus the supplied view columns it
416
+ // owns. A non-preserved active side carries a presence gate over its supplied columns.
417
+ //
418
+ // When a non-preserved side IS active but a *given row's* supplied values are all null
419
+ // (its presence gate fails for that row), that row's non-preserved insert is dropped.
420
+ // An FK-child side that threads the minted key into its join column unconditionally
421
+ // would then point that FK column at a key with no partner row (a dangling reference —
422
+ // an FK violation under enforcement, a latent spooky-join otherwise). The per-row
423
+ // conditional key thread below (`keyGate`) closes that: the FK-child's key column is
424
+ // nulled for exactly the rows whose presence-gated partner is absent, so the
425
+ // preserved row reads back cleanly null-extended with no dangling FK. The *statically*
426
+ // absent case (a non-preserved side with NO supplied columns) needs no gate — it is
427
+ // inactive ⇒ no key is threaded at all.
428
+ // σ-constant contradiction reject (the supplied-and-contradicting case): when an
429
+ // explicit insert value lands on a σ-constrained base column AND contradicts its
430
+ // constant, reject `predicate-contradiction` at plan time — parity with single-source
431
+ // `checkContradiction`. Match a supplied entry by its resolved (sideIndex, baseColumn)
432
+ // so a *renamed* view column is covered (the supplied entry carries the base column,
433
+ // not the view spelling). Only literal VALUES cells are provable; a non-literal cell, a
434
+ // parameter, or a SELECT source is unprovable ⇒ skipped (proceed), exactly as single-
435
+ // source. The user supplies the value, so no σ-default is appended for it.
436
+ for (const fc of filterConstants) {
437
+ const idx = supplied.findIndex(s => s.sideIndex === fc.sideIndex && s.baseColumn !== undefined && s.baseColumn.toLowerCase() === fc.baseColumn.toLowerCase());
438
+ if (idx >= 0)
439
+ checkJoinFilterContradiction(stmt.source, idx, fc, view);
440
+ }
441
+ const specByIndex = new Map();
442
+ for (const sideIndex of activeIndices) {
443
+ const side = sides[sideIndex];
444
+ const targetColumns = [];
445
+ const envelopeIndices = [];
446
+ if (needsSharedKey) {
447
+ targetColumns.push(keyColumns[sideIndex]);
448
+ envelopeIndices.push(keyEnvelopeIndex);
449
+ }
450
+ const presenceGateIndices = [];
451
+ supplied.forEach((s, idx) => {
452
+ // An existence directive (no `baseColumn`/`sideIndex`) is never stored — it is an
453
+ // unused envelope column. Base columns route to their owning side as before.
454
+ if (s.sideIndex !== sideIndex || s.baseColumn === undefined)
455
+ return;
456
+ if (needsSharedKey && s.isKey)
457
+ return; // the key is threaded above
458
+ targetColumns.push(s.baseColumn);
459
+ envelopeIndices.push(idx);
460
+ if (!side.preserved)
461
+ presenceGateIndices.push(idx);
462
+ });
463
+ // σ-default routing (the not-supplied, owning-side-active case): a σ-constrained base
464
+ // column this side owns that the insert did NOT supply is defaulted to its σ constant
465
+ // — appended as a constant projection on the side (the builder compiles `valueExpr`),
466
+ // NOT an envelope column. The shared join key is skipped (`keyColumns[sideIndex]` — the
467
+ // EC/key thread owns that value; a σ on a join key is degenerate); a supplied column is
468
+ // skipped (the user's value wins, contradiction-checked above). The σ default is a
469
+ // per-row constant, never added to `presenceGateIndices`, so it never makes an
470
+ // otherwise-absent optional side "present". An inactive side reaches neither this loop
471
+ // nor a default (no base row to default into — the documented structural residual).
472
+ const sigmaDefaults = [];
473
+ for (const fc of filterConstants) {
474
+ if (fc.sideIndex !== sideIndex)
475
+ continue;
476
+ if (fc.baseColumn.toLowerCase() === keyColumns[sideIndex].toLowerCase())
477
+ continue;
478
+ if (supplied.some(s => s.sideIndex === sideIndex && s.baseColumn !== undefined && s.baseColumn.toLowerCase() === fc.baseColumn.toLowerCase()))
479
+ continue;
480
+ if (sigmaDefaults.some(d => d.baseColumn.toLowerCase() === fc.baseColumn.toLowerCase()))
481
+ continue;
482
+ sigmaDefaults.push({ baseColumn: fc.baseColumn, valueExpr: fc.valueExpr });
483
+ }
484
+ // A σ default legitimately covers a NOT-NULL-without-default base column (e.g. `where
485
+ // color='red'` covering a NOT NULL `color`), so fold the σ-default columns into the
486
+ // covered set BEFORE the NOT-NULL assertion.
487
+ assertNoMissingNotNull(view, side.schema, [...targetColumns, ...sigmaDefaults.map(d => d.baseColumn)]);
488
+ specByIndex.set(sideIndex, { table: side.table, schema: side.schema, targetColumns, envelopeIndices, presenceGateIndices, ...(sigmaDefaults.length > 0 ? { sigmaDefaults } : {}) });
489
+ }
490
+ // Per-row conditional key thread (the FK-dangling-key fix). With the key MINTED and
491
+ // threaded, any active side `S` that declares a foreign key onto a presence-gated active
492
+ // partner `P` must NOT point its key (FK) column at the shared key for a row where `P` is
493
+ // per-row absent (its presence gate fails, dropping its insert) — otherwise `S`'s row
494
+ // references a freshly minted key with no partner row. Gate `S`'s key column on the AND,
495
+ // over each such partner, of that partner's presence predicate (the OR of its supplied
496
+ // columns being non-null — its own `presenceGateIndices`), nulling the key when all such
497
+ // partners are absent. A parent/anchor side (whose key is its own referenced PK)
498
+ // declares no FK onto the partner ⇒ no gate ⇒ its key threads unconditionally (nulling
499
+ // a NOT NULL PK would be wrong); a key shared only among always-active sides likewise
500
+ // stays unconditional. The key sits at target index 0 (pushed first under
501
+ // `needsSharedKey`).
502
+ //
503
+ // A *supplied* shared key (a view column carries it, `suppliedKeyIndex >= 0`) is NEVER
504
+ // gated: the value is the user's explicit reference, which may point at a PRE-EXISTING
505
+ // parent the insert does not touch (`pv` left null because the parent already exists), so
506
+ // nulling it would silently discard the user's key and orphan the child. The both-side-
507
+ // create "no partner ⇒ no key" reasoning holds only for the engine-minted key, whose
508
+ // referent exists iff this insert creates it; for a supplied key, FK enforcement is the
509
+ // correct validator of a dangling reference (an honest error beats a silent null).
510
+ if (needsSharedKey && suppliedKeyIndex < 0) {
511
+ for (const sideIndex of activeIndices) {
512
+ const groups = [];
513
+ for (const partnerIndex of activeIndices) {
514
+ if (partnerIndex === sideIndex)
515
+ continue;
516
+ const partner = specByIndex.get(partnerIndex);
517
+ if (partner.presenceGateIndices.length === 0)
518
+ continue; // an always-active partner
519
+ if (!sideDeclaresFkOnto(sides[sideIndex], sides[partnerIndex]))
520
+ continue;
521
+ groups.push([...partner.presenceGateIndices]);
522
+ }
523
+ // `groups.length >= 2` is the under-determined multi-parent shape: this FK-child
524
+ // threads its SINGLE shared key column into ≥2 presence-gated (outer-joined)
525
+ // parents (`cc.pr references p1(pp) references p2(qq)`, both LEFT-joined and
526
+ // supplied). One key value `K` must satisfy two FK constraints at once, so a
527
+ // both-create row needs BOTH parents present; a partial-supply row (one parent's
528
+ // value null) nulls `pr` entirely via the AND-gate, yet the present parent still
529
+ // materializes through its own presence filter — silently losing the supplied
530
+ // value and orphaning that parent. We cannot statically prove every row supplies
531
+ // all parents, so the shape is rejected rather than threaded as a broken AND-gated
532
+ // key (§ Outer Joins — Inserts; the per-parent-key-columns generalization is future
533
+ // work). The single-parent (`groups.length === 1`) gate below is the shipped,
534
+ // tested `ojv2` behavior and is unaffected.
535
+ if (groups.length >= 2) {
536
+ raiseMutationDiagnostic({
537
+ reason: 'unsupported-decomposition-key',
538
+ table: view.name,
539
+ message: `cannot insert through view '${view.name}': the FK-child side '${sides[sideIndex].schema.name}' threads a single shared key into ${groups.length} optional (outer-joined) parents; one key column cannot reference some-but-not-all of them per row (a multi-parent shared-key insert is not yet supported — supply all parents, or split into per-parent key columns)`,
540
+ });
541
+ }
542
+ if (groups.length > 0) {
543
+ const spec = specByIndex.get(sideIndex);
544
+ specByIndex.set(sideIndex, { ...spec, keyGate: { keyTargetIndex: 0, groups } });
545
+ }
546
+ }
547
+ }
548
+ const order = orderSides(sides).filter(i => specByIndex.has(i));
549
+ return {
550
+ suppliedColumns: supplied.map(s => ({ name: s.name, type: s.type })),
551
+ orderedSides: order.map(i => specByIndex.get(i)),
552
+ keyDefault,
553
+ };
554
+ }
555
+ /**
556
+ * The uniform boolean directive an `exists … as` existence column supplies on a
557
+ * multi-source INSERT — `true` ⇒ insert the non-preserved side, `false` ⇒ omit it
558
+ * (preserved-only). The flag is a *routing directive*, decided at plan time, so it must
559
+ * be a boolean literal that is the **same** across every inserted VALUES row (a per-row
560
+ * branch on the written value, or a SELECT/DML source whose value is not statically
561
+ * known, is deferred — `unsupported-outer-join-update` / `unsupported-source`).
562
+ * `columnIndex` is the flag's position in the explicit column list, hence its position in
563
+ * each VALUES tuple.
564
+ */
565
+ function existenceInsertFlag(view, stmt, columnIndex, columnName) {
566
+ if (stmt.source.type !== 'values') {
567
+ raiseMutationDiagnostic({
568
+ reason: 'unsupported-source',
569
+ column: columnName,
570
+ table: view.name,
571
+ message: `cannot insert through view '${view.name}': the existence column '${columnName}' is a routing directive that must be a literal in a VALUES source (a SELECT/DML source's per-row value is deferred)`,
572
+ });
573
+ }
574
+ let flag;
575
+ for (const row of stmt.source.values) {
576
+ const cell = row[columnIndex];
577
+ const b = cell ? asBooleanLiteral(cell) : undefined;
578
+ if (b === undefined) {
579
+ raiseMutationDiagnostic({
580
+ reason: 'unsupported-outer-join-update',
581
+ column: columnName,
582
+ table: view.name,
583
+ message: `cannot insert through view '${view.name}': the existence column '${columnName}' must be a boolean literal (true/false); a non-literal per-row directive is deferred`,
584
+ });
585
+ }
586
+ if (flag === undefined)
587
+ flag = b;
588
+ else if (flag !== b) {
589
+ raiseMutationDiagnostic({
590
+ reason: 'unsupported-outer-join-update',
591
+ column: columnName,
592
+ table: view.name,
593
+ message: `cannot insert through view '${view.name}': the existence column '${columnName}' must be uniform across the inserted rows (a per-row mix of true/false is deferred)`,
594
+ });
595
+ }
596
+ }
597
+ // An empty VALUES list cannot reach here (the parser requires ≥1 row); default false.
598
+ return flag ?? false;
599
+ }
600
+ /**
601
+ * Walk every nested `JoinClause`'s ON predicate (flattened on AND) and USING column
602
+ * list across the n-way join tree, collecting cross-side `column = column` equalities
603
+ * (each operand resolving to a *different* side via {@link resolveColumnSide}). The
604
+ * shared backward read the insert envelope's shared-key extraction relies on — it must
605
+ * see ALL conjunctions (not just the outermost join's ON), since for `a join b on …
606
+ * join c on …` only the last ON is on `from[0]`.
607
+ */
608
+ function collectCrossSideEqualities(from, sides) {
609
+ const out = [];
610
+ const sidesUnder = (fc) => {
611
+ switch (fc.type) {
612
+ case 'table': {
613
+ const alias = (fc.alias ?? fc.table.name).toLowerCase();
614
+ const idx = sides.findIndex(s => s.alias === alias);
615
+ return idx >= 0 ? [idx] : [];
616
+ }
617
+ case 'join':
618
+ return [...sidesUnder(fc.left), ...sidesUnder(fc.right)];
619
+ default:
620
+ return [];
621
+ }
622
+ };
623
+ const visit = (fc) => {
624
+ if (fc.type !== 'join')
625
+ return;
626
+ visit(fc.left);
627
+ visit(fc.right);
628
+ if (fc.condition) {
629
+ for (const conj of flattenAnd(fc.condition)) {
630
+ if (conj.type !== 'binary' || conj.operator !== '=')
631
+ continue;
632
+ if (conj.left.type !== 'column' || conj.right.type !== 'column')
633
+ continue;
634
+ const sa = resolveColumnSide(conj.left, sides);
635
+ const sb = resolveColumnSide(conj.right, sides);
636
+ if (sa === undefined || sb === undefined || sa === sb)
637
+ continue;
638
+ out.push({ sideA: sa, colA: conj.left.name, sideB: sb, colB: conj.right.name });
639
+ }
640
+ }
641
+ // USING (c, …): each named column equates the same-named column on the left and
642
+ // right operands. The operands may be nested joins, so locate the unique owning
643
+ // side under each (a column present on exactly one side of each operand subtree).
644
+ if (fc.columns) {
645
+ const ownerUnder = (operand, col) => {
646
+ const owners = sidesUnder(operand).filter(i => sides[i].schema.columns.some(c => c.name.toLowerCase() === col.toLowerCase()));
647
+ return owners.length === 1 ? owners[0] : undefined;
648
+ };
649
+ for (const colName of fc.columns) {
650
+ const sa = ownerUnder(fc.left, colName);
651
+ const sb = ownerUnder(fc.right, colName);
652
+ if (sa === undefined || sb === undefined || sa === sb)
653
+ continue;
654
+ out.push({ sideA: sa, colA: colName, sideB: sb, colB: colName });
655
+ }
656
+ }
657
+ };
658
+ visit(from[0]);
659
+ return out;
660
+ }
661
+ /**
662
+ * The per-side shared-key base columns of an n-way inner equi-join, aligned to `sides`
663
+ * by index. Walks every ON conjunction / USING column ({@link collectCrossSideEqualities})
664
+ * and requires they connect all sides into a **single** shared-key equivalence class
665
+ * with exactly one key column per side (the surrogate the decomposition threads through
666
+ * the envelope's equivalence class). A side contributing more than one column to the EC
667
+ * is the deferred multi-column-surrogate shape — rejected `unsupported-decomposition-key`;
668
+ * a join that does not relate every side through one shared value (a chained / multi-key
669
+ * join) is rejected `unsupported-join`.
670
+ */
671
+ function extractJoinKeyColumns(view, sel, sides) {
672
+ const equalities = collectCrossSideEqualities(sel.from, sides);
673
+ if (equalities.length === 0) {
674
+ raiseMutationDiagnostic({
675
+ reason: 'unsupported-join',
676
+ table: view.name,
677
+ message: `cannot insert through view '${view.name}': the join must carry an explicit equi-join ON/USING predicate naming the shared key`,
678
+ });
679
+ }
680
+ // Per side: the distinct columns it contributes to a cross-side equality.
681
+ const perSideCols = sides.map(() => new Set());
682
+ // Union-find over `<side>:<col>` keys — proves a single shared-key equivalence class.
683
+ const parent = new Map();
684
+ const ensure = (k) => { if (!parent.has(k))
685
+ parent.set(k, k); };
686
+ const find = (k) => { ensure(k); let r = k; while (parent.get(r) !== r)
687
+ r = parent.get(r); parent.set(k, r); return r; };
688
+ const union = (a, b) => { parent.set(find(a), find(b)); };
689
+ const nodeKey = (side, col) => `${side}:${col.toLowerCase()}`;
690
+ for (const eq of equalities) {
691
+ perSideCols[eq.sideA].add(eq.colA);
692
+ perSideCols[eq.sideB].add(eq.colB);
693
+ union(nodeKey(eq.sideA, eq.colA), nodeKey(eq.sideB, eq.colB));
694
+ }
695
+ const keyCols = sides.map((side, i) => {
696
+ const cols = [...perSideCols[i]];
697
+ if (cols.length === 0) {
698
+ raiseMutationDiagnostic({
699
+ reason: 'unsupported-join',
700
+ table: view.name,
701
+ message: `cannot insert through view '${view.name}': base table '${side.schema.name}' is not related to the shared join key by any equi-join predicate`,
702
+ });
703
+ }
704
+ if (cols.length > 1) {
705
+ raiseMutationDiagnostic({
706
+ reason: 'unsupported-decomposition-key',
707
+ table: view.name,
708
+ message: `cannot insert through view '${view.name}': base table '${side.schema.name}' contributes a composite shared key (${cols.join(', ')}); a multi-column shared-key insert envelope is not yet supported`,
709
+ });
710
+ }
711
+ return cols[0];
712
+ });
713
+ // All sides' key columns must belong to ONE equivalence class — a single shared value
714
+ // threaded into every side via the EC. A chain (`a.x=b.y join … b.z=c.w`) yields
715
+ // disjoint key classes that no single surrogate can thread.
716
+ const root0 = find(nodeKey(0, keyCols[0]));
717
+ for (let i = 1; i < sides.length; i++) {
718
+ if (find(nodeKey(i, keyCols[i])) !== root0) {
719
+ raiseMutationDiagnostic({
720
+ reason: 'unsupported-join',
721
+ table: view.name,
722
+ message: `cannot insert through view '${view.name}': the join does not relate all base tables through a single shared key (a chained / multi-key join insert is not yet supported)`,
723
+ });
724
+ }
725
+ }
726
+ return keyCols;
727
+ }
728
+ /**
729
+ * Lift the join body's σ (`where`) `column = literal` equality conjuncts as per-side
730
+ * constant-FD insert-defaults — the side-aware analog of single-source
731
+ * `extractFilterConstants`. Each conjunct's column operand is resolved to its owning join
732
+ * side via {@link resolveColumnSide} (which handles alias-qualified `sv1.color` AND
733
+ * unqualified columns, returning `undefined` for ambiguous/unresolved — skipped
734
+ * conservatively, parity with {@link joinCorrelatesMutualFk}); the side's canonical base-
735
+ * column name is resolved via {@link columnByName}. Only a **literal** RHS is lifted (parity
736
+ * with single-source — a `where color = :p` parameter, or a `col = col` / non-equality
737
+ * conjunct, is not a constant-FD producer and is skipped). The σ-constrained column is
738
+ * frequently projected away (`color` is not a view output column), so re-scanning the AST
739
+ * — rather than reading the planned body's output attributes — is the established write-path
740
+ * pattern.
741
+ */
742
+ function extractJoinFilterConstants(where, sides) {
743
+ const out = [];
744
+ if (!where)
745
+ return out;
746
+ for (const conj of flattenAnd(where)) {
747
+ if (conj.type !== 'binary' || conj.operator !== '=')
748
+ continue;
749
+ const colSide = conj.left.type === 'column' ? conj.left : conj.right.type === 'column' ? conj.right : undefined;
750
+ const litSide = conj.left.type === 'literal' ? conj.left : conj.right.type === 'literal' ? conj.right : undefined;
751
+ if (!colSide || !litSide)
752
+ continue;
753
+ const sideIndex = resolveColumnSide(colSide, sides);
754
+ if (sideIndex === undefined)
755
+ continue; // ambiguous / unresolved — skip conservatively
756
+ const baseCol = columnByName(sides[sideIndex].schema, colSide.name);
757
+ const value = litSide.value instanceof Promise ? undefined : litSide.value;
758
+ out.push({ sideIndex, baseColumn: baseCol.name, valueExpr: litSide, value });
759
+ }
760
+ return out;
761
+ }
762
+ /**
763
+ * Reject an insert literal cell that contradicts a join-σ constant — the multi-source
764
+ * analog of single-source `checkContradiction`. Walks every VALUES row's `columnIndex`
765
+ * cell (the supplied entry's position in the envelope/VALUES tuple) and rejects
766
+ * `predicate-contradiction` when a literal cell ≠ the σ constant. A non-literal cell, a
767
+ * parameter, an unprovable constant (`fc.value === undefined`), or a non-VALUES (SELECT)
768
+ * source is skipped (unprovable ⇒ proceed).
769
+ */
770
+ function checkJoinFilterContradiction(source, columnIndex, fc, view) {
771
+ if (source.type !== 'values' || fc.value === undefined)
772
+ return;
773
+ for (const row of source.values) {
774
+ const cell = row[columnIndex];
775
+ if (!cell || cell.type !== 'literal' || cell.value instanceof Promise)
776
+ continue;
777
+ if (!sqlValuesEqual(cell.value, fc.value)) {
778
+ raiseMutationDiagnostic({
779
+ reason: 'predicate-contradiction',
780
+ column: fc.baseColumn,
781
+ table: view.name,
782
+ message: `insert into view '${view.name}' contradicts its selection predicate on column '${fc.baseColumn}'`,
783
+ });
784
+ }
785
+ }
786
+ }
787
+ /** Reject a not-null base column with no declared default that no envelope value covers. */
788
+ function assertNoMissingNotNull(view, schema, targetColumns) {
789
+ const covered = new Set(targetColumns.map(c => c.toLowerCase()));
790
+ for (const col of schema.columns) {
791
+ if (col.generated || !col.notNull || col.defaultValue !== null)
792
+ continue;
793
+ if (covered.has(col.name.toLowerCase()))
794
+ continue;
795
+ raiseMutationDiagnostic({
796
+ reason: 'no-default',
797
+ column: col.name,
798
+ table: view.name,
799
+ message: `cannot insert through view '${view.name}': base table '${schema.name}' column '${col.name}' is NOT NULL with no default and no value supplied through the view`,
800
+ });
801
+ }
802
+ }
803
+ /**
804
+ * The anchor key column's declared `default` — the surrogate's per-row source —
805
+ * evaluated once per produced row at the envelope (with `mutation_ordinal()` in
806
+ * scope) and threaded into both sides via the equivalence class. The engine no
807
+ * longer invents a surrogate: a key that is neither supplied nor defaulted raises
808
+ * `no-default` with the migration recipe.
809
+ */
810
+ function requireKeyDefault(view, schema, keyCol) {
811
+ if (keyCol.defaultValue === null) {
812
+ raiseMutationDiagnostic({
813
+ reason: 'no-default',
814
+ column: keyCol.name,
815
+ table: view.name,
816
+ message: `cannot insert through view '${view.name}': the shared key '${schema.name}.${keyCol.name}' is neither supplied nor declares a DEFAULT; declare a default (e.g. \`default (coalesce((select max(${keyCol.name}) from ${schema.name}), 0) + mutation_ordinal())\`) or supply the key through a view column`,
817
+ suggestion: `declare a DEFAULT on '${schema.name}.${keyCol.name}', or expose the key as a supplied view column`,
818
+ });
819
+ }
820
+ return keyCol.defaultValue;
821
+ }
822
+ function columnByName(schema, name) {
823
+ const col = schema.columns.find(c => c.name.toLowerCase() === name.toLowerCase());
824
+ if (!col) {
825
+ raiseMutationDiagnostic({ reason: 'no-base-lineage', table: schema.name, column: name, message: `column '${name}' not found on base table '${schema.name}'` });
826
+ }
827
+ return col;
828
+ }
829
+ // --- analysis -------------------------------------------------------------
830
+ export function analyzeJoinView(ctx, view) {
831
+ if (view.selectAst.type !== 'select') {
832
+ raiseMutationDiagnostic({
833
+ reason: 'no-base-lineage',
834
+ table: view.name,
835
+ message: `view '${view.name}' has a ${view.selectAst.type.toUpperCase()} body, which has no recoverable base operation`,
836
+ });
837
+ }
838
+ const sel = view.selectAst;
839
+ // LIMIT / OFFSET / DISTINCT escape the predicate-conjoin rewrite — reject (as
840
+ // the single-source spine does) rather than silently widen the write.
841
+ if (sel.limit || sel.offset) {
842
+ raiseMutationDiagnostic({
843
+ reason: 'unsupported-limit',
844
+ table: view.name,
845
+ message: `cannot write through view '${view.name}': a LIMIT/OFFSET join body is not decomposable (a mutation would escape the limited window)`,
846
+ });
847
+ }
848
+ if (sel.distinct) {
849
+ raiseMutationDiagnostic({
850
+ reason: 'unsupported-distinct',
851
+ table: view.name,
852
+ message: `cannot write through view '${view.name}': a DISTINCT join body has no 1:1 base-row lineage`,
853
+ });
854
+ }
855
+ const sources = collectJoinSources(view, sel.from);
856
+ // Explicit projections only: a `select *` over a join is rejected here (before
857
+ // the shared backward read) so it surfaces the join-specific diagnostic rather
858
+ // than the generic projection/attribute arity mismatch (column→base routing
859
+ // relies on a 1:1 projection list).
860
+ if (sel.columns.some(c => c.type === 'all')) {
861
+ raiseMutationDiagnostic({
862
+ reason: 'unsupported-join',
863
+ table: view.name,
864
+ message: `cannot write through view '${view.name}': list the join's output columns explicitly (a 'select *' join body is not yet decomposable)`,
865
+ });
866
+ }
867
+ // Plan the body ONCE and read its threaded `updateLineage` through the shared
868
+ // backward-walk consumer (`analyzeBodyLineage`) — the same n-way reader the
869
+ // decomposition fan-out consumes (§ Round-Trip Laws and the Derived Backward
870
+ // Walk). The raw JoinNode + its column scope and the per-side routing layer on
871
+ // top.
872
+ const { root, tableRefsById, viewColToBaseRef, columns } = analyzeBodyLineage(ctx, view);
873
+ // The raw JoinNode + its combined column scope, captured from the SINGLE plan of
874
+ // the body above. The identity capture and RETURNING re-query build their
875
+ // identifying / projection plan nodes directly on top of these (no AST re-plan of
876
+ // the join body for row identification — § Round-Trip Laws and the Derived
877
+ // Backward Walk). `joinScope` is the exact scope `buildSelectStmt` resolved the
878
+ // body's own predicate/projections against (set into `ctx.outputScopes` during
879
+ // `buildJoin`), so reusing it makes base-term resolution byte-identical to the
880
+ // retired re-plan.
881
+ const joinNode = findJoinNode(view, root);
882
+ const joinScope = ctx.outputScopes.get(joinNode);
883
+ if (!joinScope) {
884
+ raiseMutationDiagnostic({
885
+ reason: 'no-base-lineage',
886
+ table: view.name,
887
+ message: `cannot write through view '${view.name}': the planned join body did not expose a resolvable column scope`,
888
+ });
889
+ }
890
+ // Map each AST source's **alias** to its planned `TableReferenceNode` by resolving
891
+ // the alias-qualified PK column through the join's combined scope (the same scope the
892
+ // body's own projections resolved against) to the producing attribute → its owning
893
+ // `TableReferenceNode`. A by-table-NAME match is ambiguous for a self-join (two
894
+ // sources share one table name); the alias is the discriminator, and each alias is a
895
+ // distinct scan node post-plan, so resolving through the scope pins the right ref.
896
+ // `attrToTableRef` inverts every base ref's attribute ids (which the inner join
897
+ // preserves up to its output) so a resolved column reference identifies its source.
898
+ const attrToTableRef = new Map();
899
+ for (const ref of tableRefsById.values()) {
900
+ for (const attr of ref.getAttributes())
901
+ attrToTableRef.set(attr.id, ref);
902
+ }
903
+ const schemaByTableName = new Map();
904
+ for (const ref of tableRefsById.values())
905
+ schemaByTableName.set(ref.tableSchema.name.toLowerCase(), ref.tableSchema);
906
+ const sides = sources.map((src) => {
907
+ const alias = (src.source.alias ?? src.source.table.name).toLowerCase();
908
+ const schema = schemaByTableName.get(src.source.table.name.toLowerCase());
909
+ if (!schema) {
910
+ raiseMutationDiagnostic({
911
+ reason: 'no-base-lineage',
912
+ table: view.name,
913
+ message: `cannot write through view '${view.name}': base table '${src.source.table.name}' did not resolve in the planned body`,
914
+ });
915
+ }
916
+ const ref = resolveSourceTableRef(ctx, joinScope, schema, alias, attrToTableRef, view);
917
+ return { table: ref, schema: ref.tableSchema, alias, preserved: src.preserved, ...(src.guard ? { guard: src.guard } : {}) };
918
+ });
919
+ const sideByTableId = new Map();
920
+ sides.forEach((s, idx) => sideByTableId.set(Number(s.table.id), idx));
921
+ // The existence flag's `RelationalComponentRef` carries the JoinNode CHILD's
922
+ // plan-node id (best-effort — an *aliased* source wraps the scan in an `AliasNode`,
923
+ // so the child id is the wrapper's, not the scan's). Map every body node id to the
924
+ // SOLE `TableReferenceNode` beneath it so a flag's component id resolves through the
925
+ // wrapper to its scan node, then to its side index (§ Existence columns).
926
+ const nodeToSoleTableRef = buildNodeToSoleTableRef(root);
927
+ // Route each shared backward column onto its owning join side. An inner-join body
928
+ // never null-extends (`nullExtended` always false); an outer-join body marks the
929
+ // non-preserved side's columns `nullExtended: true` (the join-predicate-guarded
930
+ // site `deriveJoinUpdateLineage` wraps) — still base-routed, but read-only on
931
+ // update (the deferred materialization) and insertable as an optional member.
932
+ const outColumns = columns.map((bc) => {
933
+ if (bc.baseTableId !== undefined && bc.baseColumn !== undefined) {
934
+ const sideIndex = sideByTableId.get(bc.baseTableId);
935
+ return {
936
+ name: bc.name,
937
+ displayName: bc.displayName,
938
+ sideIndex,
939
+ baseColumn: bc.baseColumn,
940
+ writable: sideIndex !== undefined && !bc.nullExtended,
941
+ nullExtended: bc.nullExtended,
942
+ ...(bc.inverse ? { inverse: bc.inverse } : {}),
943
+ ...(bc.domain ? { domain: bc.domain } : {}),
944
+ };
945
+ }
946
+ if (bc.existenceComponent) {
947
+ const existenceSide = resolveExistenceSide(bc.existenceComponent, nodeToSoleTableRef, sideByTableId, sides);
948
+ return {
949
+ name: bc.name,
950
+ displayName: bc.displayName,
951
+ // Not a base-column write — its write is an insert/delete effect on the
952
+ // component side, routed by `decomposeUpdate` off `existenceComponent`.
953
+ writable: false,
954
+ nullExtended: false,
955
+ existenceComponent: bc.existenceComponent,
956
+ ...(existenceSide !== undefined ? { existenceSide } : {}),
957
+ };
958
+ }
959
+ // An authored (`with inverse`) column: resolve each put's owning base relation
960
+ // to its join side — the same ownership routing every other put rides. A put
961
+ // whose relation is not a join side (defensive; the lineage routed it through a
962
+ // body TableReferenceNode) degrades the column to read-only.
963
+ if (bc.authored) {
964
+ const puts = [];
965
+ let routable = true;
966
+ for (const p of bc.authored.puts) {
967
+ const sideIndex = sideByTableId.get(p.table);
968
+ if (sideIndex === undefined) {
969
+ routable = false;
970
+ break;
971
+ }
972
+ puts.push({ sideIndex, baseColumn: p.baseColumn, expr: p.expr });
973
+ }
974
+ if (routable) {
975
+ return {
976
+ name: bc.name,
977
+ displayName: bc.displayName,
978
+ writable: true,
979
+ nullExtended: false,
980
+ authored: { puts, newRefIndex: bc.authored.newRefIndex },
981
+ };
982
+ }
983
+ }
984
+ return { name: bc.name, displayName: bc.displayName, writable: false, nullExtended: false };
985
+ });
986
+ return { sel, sides, viewColToBaseRef, outColumns, root, joinNode, joinScope };
987
+ }
988
+ /**
989
+ * Resolve one AST join source (by its `alias`) to its planned `TableReferenceNode`.
990
+ * Probes with the source's first PK column (or first column if keyless) qualified by
991
+ * the alias, resolved through the join's combined `joinScope` — the inner join
992
+ * preserves the producing base scan's attribute id up to its output, so the resolved
993
+ * `ColumnReferenceNode`'s attribute id pins the alias's owning `TableReferenceNode`
994
+ * via `attrToTableRef`. This is what disambiguates a **self-join** (two sources sharing
995
+ * one table name but distinct aliases → distinct scan nodes).
996
+ */
997
+ function resolveSourceTableRef(ctx, joinScope, schema, alias, attrToTableRef, view) {
998
+ const pk = schema.primaryKeyDefinition;
999
+ const probeColName = (pk.length > 0 ? schema.columns[pk[0].index] : schema.columns[0])?.name;
1000
+ if (!probeColName) {
1001
+ raiseMutationDiagnostic({
1002
+ reason: 'no-base-lineage',
1003
+ table: view.name,
1004
+ message: `cannot write through view '${view.name}': base table '${schema.name}' (alias '${alias}') has no columns to resolve its join side`,
1005
+ });
1006
+ }
1007
+ const probe = buildExpression({ ...ctx, scope: joinScope }, { type: 'column', name: probeColName, table: alias });
1008
+ if (!(probe instanceof ColumnReferenceNode)) {
1009
+ raiseMutationDiagnostic({
1010
+ reason: 'no-base-lineage',
1011
+ table: view.name,
1012
+ message: `cannot write through view '${view.name}': join source alias '${alias}' did not resolve to a base column reference in the planned body`,
1013
+ });
1014
+ }
1015
+ const ref = attrToTableRef.get(probe.attributeId);
1016
+ if (!ref) {
1017
+ raiseMutationDiagnostic({
1018
+ reason: 'no-base-lineage',
1019
+ table: view.name,
1020
+ message: `cannot write through view '${view.name}': join source alias '${alias}' did not resolve to a base table in the planned body`,
1021
+ });
1022
+ }
1023
+ return ref;
1024
+ }
1025
+ /**
1026
+ * The single `JoinNode` inside a planned n-way inner-join body — the outermost
1027
+ * `JoinNode` reached from the root (the body's plan is `Project(Filter?(Join…))`; for
1028
+ * an n-way join the outermost JoinNode transitively contains the nested ones). Reused
1029
+ * (not re-planned) as the source the identifying-capture / RETURNING relations build
1030
+ * on; the nested joins ride inside it via its own `getRelations()`.
1031
+ */
1032
+ function findJoinNode(view, root) {
1033
+ let found;
1034
+ const visit = (n) => {
1035
+ if (found)
1036
+ return;
1037
+ if (n instanceof JoinNode) {
1038
+ found = n;
1039
+ return;
1040
+ }
1041
+ for (const child of n.getRelations())
1042
+ visit(child);
1043
+ };
1044
+ visit(root);
1045
+ if (!found) {
1046
+ raiseMutationDiagnostic({
1047
+ reason: 'no-base-lineage',
1048
+ table: view.name,
1049
+ message: `cannot write through view '${view.name}': the planned body did not contain a join node`,
1050
+ });
1051
+ }
1052
+ return found;
1053
+ }
1054
+ /**
1055
+ * Map every node in a planned body to the SOLE `TableReferenceNode` reachable beneath
1056
+ * it. Resolves an existence flag's `RelationalComponentRef` — which names the JoinNode
1057
+ * child's plan-node id, an `AliasNode` wrapper for an *aliased* source — back to its
1058
+ * scan node. A node spanning two or more base tables is left unmapped (size ≠ 1).
1059
+ */
1060
+ function buildNodeToSoleTableRef(root) {
1061
+ const out = new Map();
1062
+ const visit = (n) => {
1063
+ if (n instanceof TableReferenceNode) {
1064
+ out.set(Number(n.id), n);
1065
+ return [n];
1066
+ }
1067
+ const refs = [];
1068
+ for (const child of n.getRelations())
1069
+ refs.push(...visit(child));
1070
+ const uniqueIds = new Set(refs.map(r => Number(r.id)));
1071
+ if (uniqueIds.size === 1)
1072
+ out.set(Number(n.id), refs[0]);
1073
+ return refs;
1074
+ };
1075
+ visit(root);
1076
+ return out;
1077
+ }
1078
+ /**
1079
+ * Resolve an existence flag's component to the non-preserved join side it drives. The
1080
+ * component names the JoinNode child's plan-node id; resolve it through any wrapper to
1081
+ * its scan node ({@link buildNodeToSoleTableRef}) → side index. Falls back to the unique
1082
+ * non-preserved side when the id does not resolve (v1: a single LEFT join has exactly
1083
+ * one such side). Returns `undefined` only when the side is genuinely ambiguous (≠1
1084
+ * non-preserved side — e.g. a parser-rejected FULL); the write router then defers.
1085
+ */
1086
+ function resolveExistenceSide(component, nodeToSoleTableRef, sideByTableId, sides) {
1087
+ if (component.kind === 'join-side') {
1088
+ const ref = nodeToSoleTableRef.get(component.table);
1089
+ const direct = ref ? sideByTableId.get(Number(ref.id)) : undefined;
1090
+ if (direct !== undefined)
1091
+ return direct;
1092
+ }
1093
+ const nonPreserved = sides.flatMap((s, i) => s.preserved ? [] : [i]);
1094
+ return nonPreserved.length === 1 ? nonPreserved[0] : undefined;
1095
+ }
1096
+ /**
1097
+ * Collect the join's base-table sources (in AST declaration order), validating the body
1098
+ * is an **n-way (>=2) equi-join** over plain base tables — `inner`, `left`, `right`, or
1099
+ * `full` (RIGHT is the mirror of LEFT; FULL has no preserved side, so it self-
1100
+ * conservatizes downstream; no comma/implicit cross join, no subquery or function
1101
+ * sources). A **self-join**
1102
+ * — the same base table under distinct aliases — is accepted (routing is alias-keyed
1103
+ * downstream); USING joins are accepted alongside ON joins. The declaration order is the
1104
+ * alias-declaration order the substrate serializes per-side ops in (§ Cycles, Self-Joins).
1105
+ *
1106
+ * Each source is tagged **preserved** / **non-preserved** by walking the join tree and
1107
+ * tracking which branch each table sits on: the right of a `left`, the left of a `right`,
1108
+ * and both sides of a `full` are non-preserved (potentially null-extended), carrying the
1109
+ * enclosing outer join's ON predicate as their guard (§ Outer Joins). An inner join
1110
+ * propagates its parents' classification unchanged. This is the AST-shape dual of the
1111
+ * planned body's `null-extended` lineage (which `analyzeJoinView` cross-checks per column).
1112
+ */
1113
+ function collectJoinSources(view, from) {
1114
+ if (from.length !== 1 || from[0].type !== 'join') {
1115
+ raiseMutationDiagnostic({
1116
+ reason: 'unsupported-join',
1117
+ table: view.name,
1118
+ message: `cannot write through view '${view.name}': only an explicit 'JOIN ... ON/USING' body is decomposable (a comma/implicit cross join is not)`,
1119
+ });
1120
+ }
1121
+ const out = [];
1122
+ // `nonPreserved` is true when an enclosing outer join can null-extend the subtree;
1123
+ // `guards` are those outer joins' ON predicates (conjoined onto each leaf's guard).
1124
+ const visit = (fc, nonPreserved, guards) => {
1125
+ switch (fc.type) {
1126
+ case 'table':
1127
+ out.push({
1128
+ source: fc,
1129
+ preserved: !nonPreserved,
1130
+ guard: guards.reduce((acc, g) => combineAnd(acc, g), undefined),
1131
+ });
1132
+ return;
1133
+ case 'join': {
1134
+ const hasPredicate = !!fc.condition || (!!fc.columns && fc.columns.length > 0);
1135
+ // RIGHT is **admitted** (`view-write-right-join-readmit`): the runtime reads a
1136
+ // RIGHT join and its preserved/non-preserved classification is the exact mirror of
1137
+ // LEFT (the right operand of a `right` is preserved, the left null-extended — see
1138
+ // the per-side recursion below), so the substrate routes it symmetrically (it keys
1139
+ // off `JoinSide.preserved`, not source order). FULL is accepted only to carry
1140
+ // through to its precise conservative diagnostics (it has no preserved side, so it
1141
+ // never falsely advertises); FULL write-through is a separable future concern.
1142
+ const acceptedType = fc.joinType === 'inner' || fc.joinType === 'left' || fc.joinType === 'right' || fc.joinType === 'full';
1143
+ if (!acceptedType || !hasPredicate) {
1144
+ raiseMutationDiagnostic({
1145
+ reason: 'unsupported-join',
1146
+ table: view.name,
1147
+ message: `cannot write through view '${view.name}': only INNER/LEFT/RIGHT/FULL joins with an ON/USING predicate are decomposable (got '${fc.joinType}'${hasPredicate ? '' : ' without ON/USING'})`,
1148
+ });
1149
+ }
1150
+ // USING joins carry no AST `Expression` guard — only an explicit ON predicate
1151
+ // is surfaced as the null-extension guard (v1 routing does not consume it).
1152
+ const guardsWith = fc.condition ? [...guards, fc.condition] : guards;
1153
+ switch (fc.joinType) {
1154
+ case 'inner':
1155
+ visit(fc.left, nonPreserved, guards);
1156
+ visit(fc.right, nonPreserved, guards);
1157
+ break;
1158
+ case 'left':
1159
+ visit(fc.left, nonPreserved, guards);
1160
+ visit(fc.right, true, guardsWith);
1161
+ break;
1162
+ case 'right':
1163
+ // Mirror of `left`: the left operand of a RIGHT join is null-extended
1164
+ // (non-preserved), the right operand is preserved.
1165
+ visit(fc.left, true, guardsWith);
1166
+ visit(fc.right, nonPreserved, guards);
1167
+ break;
1168
+ case 'full':
1169
+ visit(fc.left, true, guardsWith);
1170
+ visit(fc.right, true, guardsWith);
1171
+ break;
1172
+ }
1173
+ return;
1174
+ }
1175
+ default:
1176
+ raiseMutationDiagnostic({
1177
+ reason: 'nested-view',
1178
+ table: view.name,
1179
+ message: `cannot write through view '${view.name}': join sources must be plain base tables (a subquery / function source in the join is not yet supported)`,
1180
+ });
1181
+ }
1182
+ };
1183
+ visit(from[0], false, []);
1184
+ if (out.length < 2) {
1185
+ raiseMutationDiagnostic({
1186
+ reason: 'unsupported-join',
1187
+ table: view.name,
1188
+ message: `cannot write through view '${view.name}': a decomposable join needs at least two base tables (found ${out.length})`,
1189
+ });
1190
+ }
1191
+ return out;
1192
+ }
1193
+ /**
1194
+ * Scope guard for the multi-source path — parity with the single-source spine
1195
+ * (`single-source.ts` § `assertTopLevelViewColumns`). A top-level `where` / `set`
1196
+ * reference that is not a join-view output column would otherwise pass through
1197
+ * `substituteViewColumns` unmapped and re-bind against a base table in the
1198
+ * identifying subquery's join body (the same encapsulation leak). `outColumns`
1199
+ * already enumerates every projected view column (computed ones included, marked
1200
+ * non-writable), so it is the view's exposed column set.
1201
+ */
1202
+ function guardTopLevelScope(expr, analysis, view) {
1203
+ assertTopLevelViewColumns(expr, new Set(analysis.outColumns.map(c => c.name)), analysis.outColumns.map(c => c.displayName), view);
1204
+ }
1205
+ export function decomposeUpdate(ctx, view, analysis, stmt, sourceValues, captureRelationName = MS_UPDATE_KEYS_CTE) {
1206
+ // RETURNING through a multi-source update is supported, but the rows are not
1207
+ // recoverable from the per-side base ops (the view row spans both tables), so
1208
+ // the builder (`view-mutation-builder.ts`) supplies them via a re-query of the
1209
+ // planned join body; the base ops themselves carry no RETURNING.
1210
+ // Each assignment routes to its owning base side by lineage, unconditionally —
1211
+ // there is no statement-level base-set override (the routing tags were removed;
1212
+ // a per-row presence/membership column expresses any non-default routing).
1213
+ // Scope guard: top-level `where` references must name view columns (parity with
1214
+ // the single-source spine — a base-only name must not leak through the join body).
1215
+ if (stmt.where)
1216
+ guardTopLevelScope(stmt.where, analysis, view);
1217
+ // Cross-source SET values (`set a.x = b.y`) ride the same `__vmupd_keys` capture:
1218
+ // each partner-side base column the SET reads is projected into the capture under a
1219
+ // stable `srcN` alias, and the reference is rewritten to a correlated scalar read of
1220
+ // it (keyed by the owning side's PK). The carrier is the `sourceValues` out-param the
1221
+ // builder threads into `buildMultiSourceKeyCapture`; absent it (the legacy
1222
+ // `propagateMultiSource` path, unreachable from build) cross-source values stay
1223
+ // rejected by `stripSideQualifier`'s throw.
1224
+ const srcDedup = new Map();
1225
+ // Project an arbitrary base-term expression into the up-front `__vmupd_keys` capture
1226
+ // under a stable `srcN` alias (deduped by `key`), returning that alias. The carrier is
1227
+ // the `sourceValues` out-param the builder threads into the capture, so each projection
1228
+ // is materialized pre-mutation over the join body. Backs both the cross-source SET reads
1229
+ // and the outer-join non-preserved materialization (the captured assigned value + the EC
1230
+ // join key). Absent ⇒ the legacy non-build path, which keeps deferring those shapes.
1231
+ const registerCapturedExpr = sourceValues
1232
+ ? (key, expr) => {
1233
+ const existing = srcDedup.get(key);
1234
+ if (existing)
1235
+ return existing;
1236
+ const alias = `src${sourceValues.length}`;
1237
+ srcDedup.set(key, alias);
1238
+ sourceValues.push({ alias, expr });
1239
+ return alias;
1240
+ }
1241
+ : undefined;
1242
+ const registerCrossSource = sourceValues
1243
+ ? (col) => {
1244
+ const key = `${(col.table ?? '').toLowerCase()}.${col.name.toLowerCase()}`;
1245
+ const existing = srcDedup.get(key);
1246
+ if (existing)
1247
+ return existing;
1248
+ const alias = `src${sourceValues.length}`;
1249
+ srcDedup.set(key, alias);
1250
+ sourceValues.push({ alias, expr: { type: 'column', name: col.name, table: col.table } });
1251
+ return alias;
1252
+ }
1253
+ : undefined;
1254
+ // Route each assignment to its owning base side (one entry per side, index 0..n-1).
1255
+ const perSide = analysis.sides.map(() => []);
1256
+ // Non-preserved (outer-join null-extended) assignments, keyed by their owning side: each
1257
+ // rides a matched-UPDATE (pushed into `perSide`, its captured PK non-null) plus a single
1258
+ // null-extended-INSERT op (built after the per-side loop, one per non-preserved side,
1259
+ // carrying every column assigned on that side). § Outer Joins — Updates.
1260
+ const nullExtendedBySide = new Map();
1261
+ // Existence-flag writes (§ Existence columns): writing an `exists … as` flag drives the
1262
+ // non-preserved side's existence. `set hasB = true` materializes the side for the
1263
+ // null-extended partition (the matched-update path's null-extended INSERT with no
1264
+ // assigned columns — the EC join key + base defaults); `set hasB = false` deletes the
1265
+ // matched partition. Tracked by the non-preserved side they drive.
1266
+ const existenceInsertSides = new Set();
1267
+ const existenceDeleteSides = new Set();
1268
+ // `new.<x>` in an authored put binds the WRITTEN view row: when `x` is also
1269
+ // assigned in this statement, that assignment's value (every embedded RHS reads
1270
+ // the pre-update join row, so cross-references are order-independent); otherwise
1271
+ // the column's forward read image. First occurrence wins on a duplicate target —
1272
+ // the base builder's duplicate-assignment backstop rejects the statement anyway.
1273
+ // Keyed by out-column index (the `newRefIndex` domain).
1274
+ const assignedValueByIdx = new Map();
1275
+ stmt.assignments.forEach(a => {
1276
+ const i = analysis.outColumns.findIndex(c => c.name === a.column.toLowerCase());
1277
+ if (i >= 0 && !assignedValueByIdx.has(i))
1278
+ assignedValueByIdx.set(i, a.value);
1279
+ });
1280
+ for (const asg of stmt.assignments) {
1281
+ const out = analysis.outColumns.find(c => c.name === asg.column.toLowerCase());
1282
+ if (!out) {
1283
+ // Not a view column at all — the same encapsulation-leak guard as the
1284
+ // top-level `where` scan (distinct from a computed view column below).
1285
+ raiseUnknownViewColumn(asg.column, view, analysis.outColumns.map(c => c.displayName));
1286
+ }
1287
+ // An `exists … as` existence flag (no base column): writing it is the explicit
1288
+ // insert/delete-of-the-component effect. `true` ⇒ insert the non-preserved side for
1289
+ // the null-extended partition; `false` ⇒ delete the matched partition. Reuses the
1290
+ // non-preserved-column update substrate (capture + null-extended INSERT / captured-key
1291
+ // DELETE), so the runtime is reused, not extended (§ Existence columns).
1292
+ if (out.existenceComponent) {
1293
+ if (!registerCapturedExpr) {
1294
+ raiseMutationDiagnostic({
1295
+ reason: 'unsupported-outer-join-update',
1296
+ column: asg.column,
1297
+ table: view.name,
1298
+ message: `cannot write through view '${view.name}': the existence column '${asg.column}' drives a per-row insert/delete of the non-preserved side, which needs the capture carrier`,
1299
+ });
1300
+ }
1301
+ if (out.existenceSide === undefined) {
1302
+ raiseMutationDiagnostic({
1303
+ reason: 'unsupported-outer-join-update',
1304
+ column: asg.column,
1305
+ table: view.name,
1306
+ message: `cannot write through view '${view.name}': the existence column '${asg.column}' does not resolve to a single non-preserved side (an ambiguous / full-outer existence shape is deferred)`,
1307
+ });
1308
+ }
1309
+ const npSideIndex = out.existenceSide;
1310
+ // RETURNING is not recoverable through an existence-flip (the post-mutation
1311
+ // re-query identifies by the captured non-preserved PK — null for a freshly
1312
+ // materialized row, deleted for a removed one), so reject it (parity with the
1313
+ // non-preserved-column update).
1314
+ if (stmt.returning && stmt.returning.length > 0) {
1315
+ raiseMutationDiagnostic({
1316
+ reason: 'returning-through-view',
1317
+ column: asg.column,
1318
+ table: view.name,
1319
+ message: `cannot write through view '${view.name}': RETURNING is not supported on an existence-flag write '${asg.column}' — the materialized/deleted non-preserved row is not recoverable by the captured-identity re-query`,
1320
+ });
1321
+ }
1322
+ const flag = asBooleanLiteral(asg.value);
1323
+ if (flag === undefined) {
1324
+ raiseMutationDiagnostic({
1325
+ reason: 'unsupported-outer-join-update',
1326
+ column: asg.column,
1327
+ table: view.name,
1328
+ message: `cannot write through view '${view.name}': the existence column '${asg.column}' must be assigned a boolean literal (true/false); a per-row branch on a non-literal value is deferred`,
1329
+ });
1330
+ }
1331
+ if (flag) {
1332
+ // `true`: materialize the non-preserved side for the null-extended partition.
1333
+ // Ensure a (possibly empty) `nullExtendedBySide` entry so the post-loop emits
1334
+ // the materialization INSERT; a same-side `set` folds its columns into it.
1335
+ existenceInsertSides.add(npSideIndex);
1336
+ if (!nullExtendedBySide.has(npSideIndex))
1337
+ nullExtendedBySide.set(npSideIndex, []);
1338
+ }
1339
+ else {
1340
+ // `false`: delete the matched partition (captured non-preserved PK non-null).
1341
+ existenceDeleteSides.add(npSideIndex);
1342
+ }
1343
+ continue;
1344
+ }
1345
+ // A non-preserved (outer-join null-extended) base column splits per row (§ Outer
1346
+ // Joins — Updates on a non-preserved-side column): where the non-preserved side
1347
+ // matched it is an ordinary base update; where the row is null-extended (no match) it
1348
+ // is rewritten as an insert on that side. Both ride the up-front `__vmupd_keys`
1349
+ // capture, materialized pre-mutation over the join body: the matched op reads its
1350
+ // captured PK (non-null for a matched row); the null-extended op fires for the rows
1351
+ // whose captured PK is null. The assigned value is captured ONCE (so both branches
1352
+ // read the identical pre-mutation value), and the matched op reads it back keyed on
1353
+ // the non-preserved PK. Needs the capture carrier; the legacy `propagateMultiSource`
1354
+ // path (no carrier) keeps deferring with `unsupported-outer-join-update`.
1355
+ if (out.nullExtended && out.sideIndex !== undefined && out.baseColumn) {
1356
+ if (!registerCapturedExpr) {
1357
+ raiseMutationDiagnostic({
1358
+ reason: 'unsupported-outer-join-update',
1359
+ column: asg.column,
1360
+ table: view.name,
1361
+ message: `cannot write through view '${view.name}': column '${asg.column}' is backed by the non-preserved side of an outer join (base table '${analysis.sides[out.sideIndex].schema.name}'); the per-row matched-update / null-extended-insert materialization needs the capture carrier`,
1362
+ });
1363
+ }
1364
+ // RETURNING through a non-preserved-side update IS supported: the post-mutation
1365
+ // re-query (`buildMultiSourceUpdateReturning`) re-keys its identity EXISTS off the
1366
+ // stable preserved-side PK (a per-non-preserved-side matched-OR-null disjunction),
1367
+ // so a freshly-materialized null-extended row — whose non-preserved PK was captured
1368
+ // NULL — surfaces via its preserved-side equalities instead of being dropped by a
1369
+ // `NULL = <minted pk>` match (`view-write-outer-join-nonpreserved-returning`). The
1370
+ // existence-flag RETURNING reject above stays — `set hasB = false` deletes the
1371
+ // matched partition, which neither disjunction branch recovers.
1372
+ // The assigned value's top-level references must name view columns (parity with
1373
+ // the preserved path); the value is then lowered to base terms over the join body
1374
+ // and captured pre-mutation, so a same- or cross-side read resolves uniformly.
1375
+ guardTopLevelScope(asg.value, analysis, view);
1376
+ const npSide = analysis.sides[out.sideIndex];
1377
+ const baseValue = substituteViewColumns(ctx, asg.value, analysis.viewColToBaseRef, view, analysis.sides);
1378
+ const valAlias = registerCapturedExpr(`neval:${out.sideIndex}:${out.baseColumn.toLowerCase()}`, baseValue);
1379
+ // Matched rows: a per-side UPDATE reading the captured value back, correlated by
1380
+ // the non-preserved side's PK (`buildCapturedKeyPredicate` already filters to
1381
+ // matched rows — a null captured PK never equals a real one). The read-back is
1382
+ // `min`-de-duped per non-preserved partner: when N preserved rows share one
1383
+ // existing partner, that partner's PK matches all N capture rows, so a bare scalar
1384
+ // read would error `Scalar subquery returned more than one row` — `min` collapses
1385
+ // the shared-partner group to one value (a no-op for a constant / np-only SET).
1386
+ perSide[out.sideIndex].push({ column: out.baseColumn, value: capturedValueSubquery(valAlias, out.sideIndex, requireKeyColumns(view, npSide), 'min', SELF_ALIAS, captureRelationName) });
1387
+ // Null-extended rows: accumulate the (column, captured value) for this side's
1388
+ // single materialization insert, built after the loop.
1389
+ let list = nullExtendedBySide.get(out.sideIndex);
1390
+ if (!list) {
1391
+ list = [];
1392
+ nullExtendedBySide.set(out.sideIndex, list);
1393
+ }
1394
+ list.push({ baseColumn: out.baseColumn, valAlias });
1395
+ continue;
1396
+ }
1397
+ // Lower a view-term value expression onto one owning side: gate cross-source
1398
+ // reads + 1:many cardinality, substitute view columns to base terms, then strip
1399
+ // the owning side's qualifier (a partner-side read becomes a correlated read of
1400
+ // its captured pre-mutation value). Shared by the plain per-column route and the
1401
+ // authored put fan-out below.
1402
+ const lowerValueOntoSide = (valueViewTerms, owningSideIndex, assignedCol) => {
1403
+ // Gate cross-source reads: a value that reads a partner-side view column is
1404
+ // admitted only when that column has `base` lineage (its value is recoverable
1405
+ // from a captured base column). A computed (non-base) partner column stays
1406
+ // rejected (`no-inverse`); a same-side read keeps the qualifier-strip path. Run
1407
+ // only when a capture carrier is threaded — the legacy path rejects wholesale.
1408
+ if (registerCrossSource)
1409
+ gateCrossSourceReads(valueViewTerms, owningSideIndex, analysis, view);
1410
+ const side = analysis.sides[owningSideIndex];
1411
+ // Cross-source cardinality gate (§ Inner Join, cross-source `set`): a cross-source
1412
+ // value `set owner.x = partner.y` is well-defined only when the owning side joins AT
1413
+ // MOST ONE partner row — else the capture's correlated read-back is multi-valued and
1414
+ // the runtime would error `Scalar subquery returned more than one row`. Reject the
1415
+ // 1:many direction at plan time, naming the cross-source ambiguity. Bound to this
1416
+ // assignment's owning side; memoized per partner side so the join equalities are
1417
+ // collected once. Threaded only on the capture-carrier path (symmetric with
1418
+ // `registerCrossSource`); the legacy path rejects cross-source wholesale before this.
1419
+ const cardinalityProven = new Map();
1420
+ const gateCrossSourceCardinality = registerCrossSource
1421
+ ? (partnerCol) => {
1422
+ const partnerIdx = resolveColumnSide(partnerCol, analysis.sides);
1423
+ if (partnerIdx === undefined || partnerIdx === owningSideIndex)
1424
+ return;
1425
+ let proven = cardinalityProven.get(partnerIdx);
1426
+ if (proven === undefined) {
1427
+ proven = ownerJoinsAtMostOnePartner(owningSideIndex, partnerIdx, analysis.sel, analysis.sides);
1428
+ cardinalityProven.set(partnerIdx, proven);
1429
+ }
1430
+ if (!proven) {
1431
+ const partnerTable = analysis.sides[partnerIdx].schema.name;
1432
+ raiseMutationDiagnostic({
1433
+ reason: 'cross-source-ambiguous-cardinality',
1434
+ column: assignedCol,
1435
+ table: view.name,
1436
+ message: `cannot write through view '${view.name}': the cross-source assignment of column '${assignedCol}' reads column '${partnerCol.name}' on base table '${partnerTable}', but the assigned side joins more than one '${partnerTable}' row (the join does not constrain '${partnerTable}' to a unique key), so the partner value is ambiguous — a cross-source \`set\` value is well-defined only when the assigned side joins at most one partner row`,
1437
+ });
1438
+ }
1439
+ }
1440
+ : undefined;
1441
+ // Rewrite the assigned value into base terms, then strip the owning side's
1442
+ // qualifier (the base UPDATE targets that table directly). A reference to a
1443
+ // partner side is rewritten to a correlated read of its captured pre-mutation
1444
+ // value (`registerCrossSource`); absent the carrier it is rejected.
1445
+ return stripSideQualifier(substituteViewColumns(ctx, valueViewTerms, analysis.viewColToBaseRef, view, analysis.sides), view, side, owningSideIndex, analysis.sides, registerCrossSource, gateCrossSourceCardinality);
1446
+ };
1447
+ // An authored (`with inverse`) column lowers to one base assignment per put,
1448
+ // each routed to its owning join side — a two-sided target set yields two child
1449
+ // ops, atomic, FK-parent-first ordered by the shared `orderSides` below. Inside
1450
+ // each put, `new.<x>` binds the WRITTEN view row: the assigned value when `x`
1451
+ // is assigned in this statement (including this column itself), that view
1452
+ // column's name otherwise — still in VIEW terms — then the standard lowering
1453
+ // maps everything onto the put's side (the forward read image for non-assigned
1454
+ // columns; a cross-side read rides the same captured-read machinery as a
1455
+ // cross-source SET value). docs/view-updateability.md § Authored inverses.
1456
+ if (out.authored) {
1457
+ const authored = out.authored;
1458
+ // The assigned VALUE's top-level references must name view columns (parity
1459
+ // with the plain route below).
1460
+ guardTopLevelScope(asg.value, analysis, view);
1461
+ for (const put of authored.puts) {
1462
+ const viewTermExpr = substituteNewRefs(put.expr, name => {
1463
+ const idx = requireValidatedNewRefIndex(authored.newRefIndex, name, asg.column);
1464
+ return assignedValueByIdx.get(idx)
1465
+ ?? { type: 'column', name: analysis.outColumns[idx].displayName };
1466
+ });
1467
+ perSide[put.sideIndex].push({
1468
+ column: put.baseColumn,
1469
+ value: lowerValueOntoSide(viewTermExpr, put.sideIndex, out.displayName),
1470
+ });
1471
+ }
1472
+ continue;
1473
+ }
1474
+ if (!out.writable || out.sideIndex === undefined || !out.baseColumn) {
1475
+ raiseMutationDiagnostic({
1476
+ reason: 'no-inverse',
1477
+ column: asg.column,
1478
+ table: view.name,
1479
+ message: `cannot write through view '${view.name}': column '${asg.column}' is a computed (non-invertible) expression and is read-only`,
1480
+ });
1481
+ }
1482
+ // The assigned VALUE's top-level references must name view columns too (parity
1483
+ // with the single-source spine). On a single-table side a base-only name would
1484
+ // otherwise re-bind in that table; across sides it would fail to resolve with a
1485
+ // generic error — the structured guard makes the diagnostic uniform either way.
1486
+ guardTopLevelScope(asg.value, analysis, view);
1487
+ const baseValue = lowerValueOntoSide(asg.value, out.sideIndex, out.displayName);
1488
+ // For an `inverse`-profile column the assigned value is in the VIEW domain;
1489
+ // apply the site's inverse to recover the BASE value (`cv1 = cv + 1` ⇒ the
1490
+ // write `cv1 = w` stores `cv = w - 1`). The base-term substitution + side-
1491
+ // qualifier strip above already produced the written value in base terms; the
1492
+ // inverse wraps that last (it expects a value already in base terms).
1493
+ const written = out.inverse ? out.inverse(baseValue) : baseValue;
1494
+ perSide[out.sideIndex].push({ column: out.baseColumn, value: written });
1495
+ // NB: a present `out.domain` (an `inverse`-profile restriction) would conjoin
1496
+ // into the identifying predicate. No shipped invertibility profile produces a
1497
+ // domain (`x ± k` is unrestricted over integers), and the capture path that now
1498
+ // backs EVERY side's identification (`__vmupd_keys`) does not yet thread
1499
+ // per-assignment domains — deferred uniformly until a domain-bearing profile
1500
+ // lands (§ Scalar Invertibility).
1501
+ }
1502
+ // Existence-flip contradiction (§ Existence columns): `set <npCol> = …, hasB = false`
1503
+ // cannot both delete the non-preserved side and write one of its columns; an np-column
1504
+ // write always emits a matched per-side UPDATE, so a non-empty `perSide[side]` on a
1505
+ // delete side is the contradiction. `hasB = true, hasB = false` (insert+delete the same
1506
+ // side) is the same conflict. Reject rather than silently picking one effect.
1507
+ for (const side of existenceDeleteSides) {
1508
+ if (perSide[side].length > 0 || existenceInsertSides.has(side)) {
1509
+ raiseMutationDiagnostic({
1510
+ reason: 'conflicting-assignment',
1511
+ table: view.name,
1512
+ message: `cannot write through view '${view.name}': an existence-flag write deletes base table '${analysis.sides[side].schema.name}' (the non-preserved side) while the same statement also writes one of its columns — the two effects contradict`,
1513
+ });
1514
+ }
1515
+ }
1516
+ // Every affected view row's base-PK identities are captured ONCE up-front (before
1517
+ // any base op fires) and each per-side op reads its identifying values back from
1518
+ // that captured set via a correlated EXISTS over `__vmupd_keys` (matching all of the
1519
+ // side's PK columns — composite keys included), a mutation-order-independent identity
1520
+ // built from the ALREADY-planned join body (the builder materializes the capture; see
1521
+ // `buildMultiSourceKeyCapture`). This unifies the single-side and both-sides paths
1522
+ // onto the same identity (no live re-query of a re-planned AST body): a both-sides
1523
+ // update's FK-parent op can no longer rewrite a predicate column out from under the
1524
+ // FK-child op, and a single-side op — having no ordering hazard — reads the same
1525
+ // pre-mutation set it would have re-queried live.
1526
+ // Order FK-parent before FK-child by the n-way FK topological sort (matches insert
1527
+ // ordering intent and avoids surprising mid-statement FK states); source order within
1528
+ // an FK-equivalence class (and for self-joins, whose mutual edges fall back to
1529
+ // alias-declaration order — § Cycles, Self-Joins).
1530
+ const order = orderSides(analysis.sides);
1531
+ const ops = [];
1532
+ for (const sideIndex of order) {
1533
+ const assignments = perSide[sideIndex];
1534
+ if (assignments.length === 0)
1535
+ continue;
1536
+ const side = analysis.sides[sideIndex];
1537
+ const where = buildCapturedKeyPredicate(view, side, sideIndex, captureRelationName);
1538
+ const statement = {
1539
+ type: 'update',
1540
+ table: tableIdentifier(side.schema),
1541
+ // Synthesised collision-proof correlation name on the lowered per-side target
1542
+ // (mirrors the single-source spine): the base builder registers it as the
1543
+ // target's AliasedScope alias, so a `__vm_self.col` operand emitted by the
1544
+ // capture read-back / owning-strip qualifications above binds the outer target
1545
+ // row regardless of a user value subquery's own FROM.
1546
+ alias: SELF_ALIAS,
1547
+ assignments,
1548
+ where,
1549
+ contextValues: stmt.contextValues,
1550
+ schemaPath: stmt.schemaPath,
1551
+ loc: stmt.loc,
1552
+ };
1553
+ ops.push({ table: side.table, op: 'update', statement });
1554
+ }
1555
+ // Materialize the null-extended rows: one insert per non-preserved side over the
1556
+ // captured partition (the affected rows whose non-preserved PK was captured null). The
1557
+ // matched UPDATE for the same side was already emitted by the per-side loop above (its
1558
+ // tag-allowance enforced there), so this only adds the create branch. § Outer Joins.
1559
+ for (const [sideIndex, cols] of nullExtendedBySide) {
1560
+ ops.push(buildNullExtendedInsert(ctx, view, analysis, sideIndex, cols, registerCapturedExpr, stmt, captureRelationName));
1561
+ }
1562
+ // Existence-flip deletes (§ Existence columns): `set hasB = false` removes the matched
1563
+ // non-preserved rows (their captured PK is non-null; a null-extended row's captured PK
1564
+ // is null, so the same captured-key EXISTS naturally excludes it). The preserved side is
1565
+ // untouched, so a deleted row reads back null-extended (`hasB` now false).
1566
+ for (const sideIndex of existenceDeleteSides) {
1567
+ const side = analysis.sides[sideIndex];
1568
+ const where = buildCapturedKeyPredicate(view, side, sideIndex, captureRelationName);
1569
+ const statement = {
1570
+ type: 'delete',
1571
+ table: tableIdentifier(side.schema),
1572
+ where,
1573
+ contextValues: stmt.contextValues,
1574
+ schemaPath: stmt.schemaPath,
1575
+ loc: stmt.loc,
1576
+ };
1577
+ ops.push({ table: side.table, op: 'delete', statement });
1578
+ }
1579
+ if (ops.length === 0) {
1580
+ // No assignment routed (e.g. only computed columns) — caught above as
1581
+ // no-inverse, so this is unreachable; guard for safety.
1582
+ raiseMutationDiagnostic({
1583
+ reason: 'no-inverse',
1584
+ table: view.name,
1585
+ message: `cannot write through view '${view.name}': no writable base column targeted by the update`,
1586
+ });
1587
+ }
1588
+ return ops;
1589
+ }
1590
+ /**
1591
+ * Build the null-extended materialization INSERT for a non-preserved outer-join side:
1592
+ * `insert into <np> (<joinKey>, <set cols…>) select k.<jk>, min(k.<val…>) from __vmupd_keys k
1593
+ * where <every np PK k col> is null and k.<jk> is not null group by k.<jk>` (§ Outer Joins —
1594
+ * Updates). The `group by k.<jk>` de-dups per dangling join key so a shared missing partner
1595
+ * materializes exactly once (a fan-out of N preserved rows would otherwise double-insert the
1596
+ * partner PK); the value projections are `min` so each is single-valued per group. It
1597
+ * fires only for the affected rows the join null-extended (the non-preserved PK captured
1598
+ * null) whose preserved-side join key is non-null (a null key cannot seed a joinable row).
1599
+ * The new row carries the EC join key (so the preserved row joins it), the assigned
1600
+ * value(s) read from the same pre-mutation `__vmupd_keys` capture the matched UPDATE reads,
1601
+ * and base defaults for everything else; a NOT NULL base column without a default that no
1602
+ * value covers raises `null-extended-create-conflict`.
1603
+ *
1604
+ * Built as a pure AST `BaseOp` (an insert-from-select over `__vmupd_keys`, resolved by the
1605
+ * builder's `cteNodes` injection) — no new plan-node substrate: the existing
1606
+ * capture-materialize-then-drain machinery already supplies the pre-mutation partition.
1607
+ */
1608
+ function buildNullExtendedInsert(_ctx, view, analysis, npSideIndex, cols, registerCapturedExpr, stmt, captureRelationName = MS_UPDATE_KEYS_CTE) {
1609
+ const npSide = analysis.sides[npSideIndex];
1610
+ const { npJoinColumn, preservedExpr } = outerJoinInsertKey(view, analysis, npSideIndex);
1611
+ const jkAlias = registerCapturedExpr(`nejk:${npSideIndex}`, preservedExpr);
1612
+ // Insert columns: the non-preserved join column (= the captured preserved-side join
1613
+ // value, so the preserved row joins the freshly materialized row) followed by each
1614
+ // assigned base column (= its captured value). The join column is threaded once.
1615
+ //
1616
+ // De-dup per dangling join key: a `group by k.<jkAlias>` collapses the N preserved rows
1617
+ // that share one missing partner to a single materialized row (else N rows projecting the
1618
+ // same join key would each insert the partner PK → `UNIQUE constraint failed`). The join
1619
+ // column projection IS the GROUP BY key (bare); each value column is wrapped in `min` so
1620
+ // it is single-valued per group — a no-op for a constant / np-only SET, a deterministic
1621
+ // pick for a value that differs per preserved row (mirrors the matched read-back's `min`).
1622
+ const targetColumns = [npJoinColumn];
1623
+ const projections = [
1624
+ { type: 'column', expr: { type: 'column', name: jkAlias, table: 'k' }, alias: npJoinColumn },
1625
+ ];
1626
+ const joinColLower = npJoinColumn.toLowerCase();
1627
+ for (const c of cols) {
1628
+ if (c.baseColumn.toLowerCase() === joinColLower)
1629
+ continue; // join column already threaded
1630
+ targetColumns.push(c.baseColumn);
1631
+ projections.push({ type: 'column', expr: { type: 'function', name: 'min', args: [{ type: 'column', name: c.valAlias, table: 'k' }] }, alias: c.baseColumn });
1632
+ }
1633
+ assertNullExtendedInsertCovered(view, npSide.schema, targetColumns);
1634
+ // Restrict to the null-extended partition: every captured PK column of the non-preserved
1635
+ // side is null (no join match), and the preserved join key is non-null (a null key has
1636
+ // no joinable row to create).
1637
+ const conds = requireKeyColumns(view, npSide).map((_pk, j) => ({ type: 'unary', operator: 'IS NULL', expr: { type: 'column', name: keyColumnName(npSideIndex, j), table: 'k' } }));
1638
+ conds.push({ type: 'unary', operator: 'IS NOT NULL', expr: { type: 'column', name: jkAlias, table: 'k' } });
1639
+ const where = conds.reduce((acc, c) => combineAnd(acc, c));
1640
+ const select = {
1641
+ type: 'select',
1642
+ columns: projections,
1643
+ from: [{ type: 'table', table: { type: 'identifier', name: captureRelationName }, alias: 'k' }],
1644
+ where,
1645
+ groupBy: [{ type: 'column', name: jkAlias, table: 'k' }],
1646
+ };
1647
+ const statement = {
1648
+ type: 'insert',
1649
+ table: tableIdentifier(npSide.schema),
1650
+ columns: targetColumns,
1651
+ source: select,
1652
+ contextValues: stmt.contextValues,
1653
+ schemaPath: stmt.schemaPath,
1654
+ loc: stmt.loc,
1655
+ };
1656
+ return { table: npSide.table, op: 'insert', statement };
1657
+ }
1658
+ /**
1659
+ * The non-preserved side's join column + the preserved partner's join value for the
1660
+ * null-extended materialization insert. Walks the join's cross-side equalities
1661
+ * ({@link collectCrossSideEqualities}) for one connecting the non-preserved side to a
1662
+ * PRESERVED side: the non-preserved column is set to the preserved value, so the
1663
+ * materialized row joins back to the preserved row. A non-preserved side related to no
1664
+ * preserved side by an equi-join key cannot be materialized with a joinable key — rejected
1665
+ * `unsupported-outer-join-update`.
1666
+ *
1667
+ * Only a **single-column** join key is materializable: the insert threads exactly one
1668
+ * non-preserved join column, so a composite key (the non-preserved side equated on more
1669
+ * than one distinct column) would leave the extra predicate(s) unsatisfied — the freshly
1670
+ * inserted row would NOT join back to the preserved row (a silent non-join leaving a stray
1671
+ * unreachable row), so it is rejected `unsupported-outer-join-update`. Mirrors the
1672
+ * inner-join insert envelope's single-column shared-key restriction
1673
+ * ({@link extractJoinKeyColumns}); the matched-update branch (keyed on the full np PK) is
1674
+ * unaffected, but the whole non-preserved update rejects at plan time since the create
1675
+ * branch cannot be expressed (the conservative, data-independent precedent of
1676
+ * {@link assertNullExtendedInsertCovered}).
1677
+ */
1678
+ function outerJoinInsertKey(view, analysis, npSideIndex) {
1679
+ const eqs = collectCrossSideEqualities(analysis.sel.from, analysis.sides);
1680
+ // Every cross-side equality the non-preserved side participates in (its own column +
1681
+ // the partner side/column it is equated to).
1682
+ const npEqs = eqs.flatMap(eq => {
1683
+ if (eq.sideA === npSideIndex)
1684
+ return [{ npCol: eq.colA, partnerSide: eq.sideB, partnerCol: eq.colB }];
1685
+ if (eq.sideB === npSideIndex)
1686
+ return [{ npCol: eq.colB, partnerSide: eq.sideA, partnerCol: eq.colA }];
1687
+ return [];
1688
+ });
1689
+ // Reject a composite join key (the np side equated on >1 distinct column): the
1690
+ // single-column materialization insert cannot satisfy the extra predicate(s), so the
1691
+ // new row would not join back (a silent non-join). Distinct by np column name — the
1692
+ // same np column equated to several partners (a 3-way shared key) still threads once.
1693
+ const distinctNpCols = new Set(npEqs.map(e => e.npCol.toLowerCase()));
1694
+ if (distinctNpCols.size > 1) {
1695
+ raiseMutationDiagnostic({
1696
+ reason: 'unsupported-outer-join-update',
1697
+ table: view.name,
1698
+ message: `cannot write through view '${view.name}': the non-preserved side (base table '${analysis.sides[npSideIndex].schema.name}') is related to the join by a composite key (${[...distinctNpCols].join(', ')}); a null-extended row can only be materialized through a single-column join key`,
1699
+ });
1700
+ }
1701
+ const match = npEqs.find(e => analysis.sides[e.partnerSide].preserved);
1702
+ if (match) {
1703
+ return {
1704
+ npJoinColumn: match.npCol,
1705
+ preservedExpr: { type: 'column', name: match.partnerCol, table: analysis.sides[match.partnerSide].alias },
1706
+ };
1707
+ }
1708
+ return raiseMutationDiagnostic({
1709
+ reason: 'unsupported-outer-join-update',
1710
+ table: view.name,
1711
+ message: `cannot write through view '${view.name}': the non-preserved side (base table '${analysis.sides[npSideIndex].schema.name}') is not related to a preserved side by an equi-join key, so a null-extended row cannot be materialized with a joinable key`,
1712
+ });
1713
+ }
1714
+ /**
1715
+ * Reject a NOT NULL base column on the non-preserved side that the null-extended
1716
+ * materialization insert leaves unset (no default, no covering value) — the row cannot be
1717
+ * created. Mirrors {@link assertNoMissingNotNull} but raises `null-extended-create-conflict`
1718
+ * (the outer-join create-side diagnostic), distinguishing a missing materialization value
1719
+ * from an ordinary insert's missing column.
1720
+ */
1721
+ function assertNullExtendedInsertCovered(view, schema, covered) {
1722
+ const set = new Set(covered.map(c => c.toLowerCase()));
1723
+ for (const col of schema.columns) {
1724
+ if (col.generated || !col.notNull || col.defaultValue !== null)
1725
+ continue;
1726
+ if (set.has(col.name.toLowerCase()))
1727
+ continue;
1728
+ raiseMutationDiagnostic({
1729
+ reason: 'null-extended-create-conflict',
1730
+ column: col.name,
1731
+ table: view.name,
1732
+ message: `cannot update through view '${view.name}': materializing a null-extended row on base table '${schema.name}' would leave NOT NULL column '${col.name}' (no default) unset, so the non-preserved-side row cannot be created`,
1733
+ });
1734
+ }
1735
+ }
1736
+ /**
1737
+ * The **preserved** side indices — the DELETE candidate set (§ Outer Joins —
1738
+ * Deletes: "deleting from the preserved side is the only way for the joined row to
1739
+ * disappear from the view"). For an inner join every side is preserved, so this is the
1740
+ * full set `0..n-1`. For a LEFT/RIGHT outer join it is the single
1741
+ * preserved side. A `full` outer join has *no* preserved side; the caller falls back to
1742
+ * the full candidate set there (every side is both preserved and non-preserved).
1743
+ */
1744
+ function preservedSideIndices(sides) {
1745
+ return sides.flatMap((s, i) => s.preserved ? [i] : []);
1746
+ }
1747
+ /**
1748
+ * The correlated EXISTS identifying predicate a per-side base op routes on:
1749
+ * `exists (select 1 from __vmupd_keys k where k.k<side>_0 = <pk0> [and k.k<side>_1 =
1750
+ * <pk1> …])` — matching ALL of the side's PK columns (composite keys included) against
1751
+ * the up-front materialized key set (the both-sides-assigned UPDATE path, every
1752
+ * single-side update/delete, and the multi-side DELETE fan-out; see
1753
+ * {@link MS_UPDATE_KEYS_CTE}). The right-hand `<pk_j>` are unqualified, so they bind to
1754
+ * the base op's own target table; `k.k<side>_<j>` reads the captured column. The builder
1755
+ * injects `__vmupd_keys` into the base op's planning `cteNodes` (resolving to the
1756
+ * context-backed key relation), so this is read by descriptor rather than re-querying
1757
+ * the join body. (EXISTS — not a row-value `IN` — to reuse the UPDATE RETURNING
1758
+ * re-query's correlation shape; one pattern.)
1759
+ */
1760
+ function buildCapturedKeyPredicate(view, side, sideIndex, captureRelationName = MS_UPDATE_KEYS_CTE) {
1761
+ const keyCols = requireKeyColumns(view, side);
1762
+ const conds = keyCols.map((pkCol, j) => ({
1763
+ type: 'binary',
1764
+ operator: '=',
1765
+ left: { type: 'column', name: keyColumnName(sideIndex, j), table: 'k' },
1766
+ right: { type: 'column', name: pkCol },
1767
+ }));
1768
+ return {
1769
+ type: 'exists',
1770
+ subquery: {
1771
+ type: 'select',
1772
+ columns: [{ type: 'column', expr: { type: 'literal', value: 1 } }],
1773
+ from: [{ type: 'table', table: { type: 'identifier', name: captureRelationName }, alias: 'k' }],
1774
+ where: conds.reduce((acc, c) => combineAnd(acc, c)),
1775
+ },
1776
+ };
1777
+ }
1778
+ /**
1779
+ * Mint a context-backed key relation (`InternalRecursiveCTERefNode`) over a
1780
+ * capture's descriptor — what a multi-side base op's identifying `in`-subquery or
1781
+ * the RETURNING re-query's EXISTS scans `__vmupd_keys` through. Fresh attribute ids
1782
+ * per call (each ref lives in its own subtree); the **descriptor** identity is what
1783
+ * ties it to the rows the emitter materializes.
1784
+ *
1785
+ * `captureRelationName` is used for BOTH the node's display CTE name and every
1786
+ * attribute's `sourceRelation`, so a half-updated node can never keep the constant as
1787
+ * `sourceRelation` while the caller injects under a fresh name. It defaults to the
1788
+ * capture's own {@link MultiSourceKeyCapture.relationName} (falling back to
1789
+ * {@link MS_UPDATE_KEYS_CTE}), so a ref minted from a fresh-named capture is
1790
+ * self-consistent without the caller re-passing the name.
1791
+ */
1792
+ export function makeMultiSourceKeyRef(scope, capture, captureRelationName = capture.relationName ?? MS_UPDATE_KEYS_CTE) {
1793
+ const keyAttrs = capture.keyColumns.map(c => ({
1794
+ id: PlanNode.nextAttrId(),
1795
+ name: c.name,
1796
+ type: c.type,
1797
+ sourceRelation: captureRelationName,
1798
+ }));
1799
+ const keyRelType = {
1800
+ typeClass: 'relation',
1801
+ isReadOnly: true,
1802
+ isSet: false,
1803
+ columns: capture.keyColumns.map(c => ({ name: c.name, type: c.type })),
1804
+ keys: [],
1805
+ rowConstraints: [],
1806
+ };
1807
+ return new InternalRecursiveCTERefNode(scope, captureRelationName, keyAttrs, keyRelType, capture.descriptor);
1808
+ }
1809
+ /**
1810
+ * A planning context with `capture` injected into `cteNodes` under its relation name (the
1811
+ * capture's {@link MultiSourceKeyCapture.relationName}, defaulting to {@link MS_UPDATE_KEYS_CTE}),
1812
+ * so a base op's / inner capture's `from <relationName> k` reference resolves to the capture's
1813
+ * context-backed key relation (over the shared capture descriptor). The injected ref's own
1814
+ * name is pinned to the SAME `relationName` so the map key and the ref agree — a nested capture
1815
+ * injects under its fresh `__vmupd_keys$N` name while shadowing nothing under the default. A
1816
+ * fresh ref per call keeps each consumer's subtree from sharing a node instance.
1817
+ *
1818
+ * Shared by the multi-source UPDATE/DELETE builder (`view-mutation-builder.ts`) and the set-op
1819
+ * join-leg compose (`set-op.ts`, which injects the OUTER set-op capture so an inner per-branch
1820
+ * capture's `memberExists` filter resolves against it).
1821
+ */
1822
+ export function withKeyCapture(ctx, capture) {
1823
+ const cteNodes = new Map(ctx.cteNodes ?? []);
1824
+ const relationName = capture.relationName ?? MS_UPDATE_KEYS_CTE;
1825
+ cteNodes.set(relationName, makeMultiSourceKeyRef(ctx.scope, capture, relationName));
1826
+ return { ...ctx, cteNodes };
1827
+ }
1828
+ /**
1829
+ * The distinct join-side indices the emitted base ops target (each base op carries the
1830
+ * planned side's `TableReferenceNode`), sorted ascending — the sides whose PKs the capture
1831
+ * must project so each op's `select k<side> from <relationName>` resolves. Shared by the
1832
+ * standalone multi-source UPDATE/DELETE builder (`view-mutation-builder.ts`) and the set-op
1833
+ * join-leg compose (`set-op.ts`), which each pass it straight to
1834
+ * {@link buildMultiSourceKeyCapture}.
1835
+ */
1836
+ export function capturedSideIndices(baseOps, analysis) {
1837
+ const set = new Set();
1838
+ for (const op of baseOps) {
1839
+ const i = analysis.sides.findIndex(s => s.table.id === op.table.id);
1840
+ if (i >= 0)
1841
+ set.add(i);
1842
+ }
1843
+ return [...set].sort((a, b) => a - b);
1844
+ }
1845
+ /**
1846
+ * Build the up-front identity capture: each affected view row's base-PK identities,
1847
+ * by the same identifying predicate the base ops route on (user WHERE → base ∧ body
1848
+ * WHERE). Built as **plan nodes directly over the ALREADY-planned join body**
1849
+ * (`analysis.joinNode` + `analysis.joinScope`) — `Project_{k<side>_<j>}(Filter_{idPred}
1850
+ * (joinNode))` — instead of re-planning a cloned AST FROM, so the body is planned
1851
+ * ONCE (§ Round-Trip Laws and the Derived Backward Walk). `preserveInputColumns=false`
1852
+ * ⇒ the materialized rows are exactly the requested key columns, positionally aligned
1853
+ * to the `k<side>_<j>` columns every reader scans back (`keyColumns` and the projection
1854
+ * derive from the same `sideIndices` order; a composite-PK side contributes one column
1855
+ * per PK column).
1856
+ *
1857
+ * `sideIndices` selects which sides' PKs to capture (each requires ≥1 PK column via
1858
+ * {@link requireKeyColumns}; a keyless side is rejected with `unsupported-join`). The
1859
+ * builder passes exactly the sides whose base ops read the capture (plus all sides, for
1860
+ * an UPDATE with RETURNING whose EXISTS correlates the full joined row), so a single-
1861
+ * side write never forces a PK on an untouched side.
1862
+ *
1863
+ * Op-agnostic: takes the user `where` directly (an UPDATE's or a DELETE's) — the
1864
+ * identifying predicate is the same either way.
1865
+ *
1866
+ * `captureRelationName` is the AST relation name the resulting capture is injected
1867
+ * under (and that the base-op predicates `decomposeUpdate`/`decomposeDelete` emit read
1868
+ * back); it is stamped onto the returned {@link MultiSourceKeyCapture.relationName} so
1869
+ * downstream injection/RETURNING read it from the capture object rather than
1870
+ * re-deriving the literal. Defaults to {@link MS_UPDATE_KEYS_CTE} — the standalone
1871
+ * multi-source path is byte-identical; a nested capture passes a fresh name.
1872
+ */
1873
+ export function buildMultiSourceKeyCapture(ctx, view, where, analysis, sideIndices, sourceValues, captureRelationName = MS_UPDATE_KEYS_CTE) {
1874
+ // The identifying predicate (user WHERE → base terms ∧ the body's own WHERE), built
1875
+ // as a ScalarPlanNode over the planned join body's own scope — the exact scope
1876
+ // `buildSelectStmt` resolved the body against. The body WHERE is conjoined
1877
+ // explicitly (the source is the raw `joinNode`, before the body's σ), so the
1878
+ // captured set is byte-identical to the retired re-plan over the cloned FROM.
1879
+ const idPredicateAst = buildIdentifyingPredicate(ctx, analysis, where, view);
1880
+ const predicate = idPredicateAst
1881
+ ? buildExpression({ ...ctx, scope: analysis.joinScope }, idPredicateAst)
1882
+ : undefined;
1883
+ const filtered = predicate
1884
+ ? new FilterNode(analysis.joinScope, analysis.joinNode, predicate)
1885
+ : analysis.joinNode;
1886
+ // One capture column per requested side per PK column: `k<side>_<j>`. A composite-PK
1887
+ // side projects all its PK columns; the readers' EXISTS correlate on the same set.
1888
+ const keyColumns = [];
1889
+ const projections = [];
1890
+ for (const i of sideIndices) {
1891
+ const side = analysis.sides[i];
1892
+ const pkCols = requireKeyColumns(view, side);
1893
+ pkCols.forEach((pk, j) => {
1894
+ const name = keyColumnName(i, j);
1895
+ keyColumns.push({ name, type: columnSchemaToScalarType(columnByName(side.schema, pk)) });
1896
+ projections.push({
1897
+ node: buildExpression({ ...ctx, scope: analysis.joinScope }, { type: 'column', name: pk, table: side.alias }),
1898
+ alias: name,
1899
+ });
1900
+ });
1901
+ }
1902
+ // Cross-source SET read values: project each partner base column the SET reads under
1903
+ // its stable `srcN` alias (over the same join-body scope), so every per-side base op's
1904
+ // correlated `select srcN from __vmupd_keys k where …` reads the captured pre-mutation
1905
+ // value. Appended AFTER the per-side PK columns, positionally aligned with the readers'
1906
+ // `keyColumns` (which are pushed in the same order).
1907
+ for (const sv of sourceValues ?? []) {
1908
+ const node = buildExpression({ ...ctx, scope: analysis.joinScope }, sv.expr);
1909
+ keyColumns.push({ name: sv.alias, type: node.getType() });
1910
+ projections.push({ node, alias: sv.alias });
1911
+ }
1912
+ const source = new ProjectNode(analysis.joinScope, filtered, projections, undefined, undefined, false);
1913
+ return { source, descriptor: {}, keyColumns, relationName: captureRelationName };
1914
+ }
1915
+ /**
1916
+ * Build the post-mutation RETURNING re-query for a multi-source UPDATE
1917
+ * (docs/view-updateability.md § `returning`). A re-query that matched by the *user
1918
+ * predicate* cannot recapture a row whose predicate column the update itself
1919
+ * rewrote (the changed row no longer matches), so this matches by the captured
1920
+ * **identity** instead: project the view-spelled, base-term RETURNING columns over
1921
+ * the post-mutation join body, restricted to the captured identities by a correlated
1922
+ * `exists (select 1 from __vmupd_keys k where <per-side identity>)` — so a row the
1923
+ * update pushed *out* of the view's filter (or whose predicate column it rewrote) is
1924
+ * still returned (single-source NEW semantics). It keeps only the structural join
1925
+ * ON-condition; the body/user WHERE is intentionally NOT re-applied.
1926
+ *
1927
+ * The per-side identity is **preserved-keyed**: a preserved side matches by exact
1928
+ * per-PK-column equality (`k.k<p>_<j> = s<p>.pk<j>`), while a non-preserved (outer-join
1929
+ * null-extended) side uses a matched-OR-null disjunction `(AND_j k.k<np>_<j> =
1930
+ * s<np>.pk<j>) OR (AND_j k.k<np>_<j> is null)`. This re-keys the re-query off the
1931
+ * **stable preserved-side identity** so a freshly-materialized null-extended row (whose
1932
+ * non-preserved PK was captured NULL) surfaces via its preserved-side equalities alone,
1933
+ * rather than being silently dropped by a `NULL = <minted pk>` match. For an all-
1934
+ * preserved (inner) join every side is exact equality — byte-identical to the prior
1935
+ * behavior, so inner-join RETURNING is unchanged.
1936
+ *
1937
+ * Reads the shared {@link MultiSourceKeyCapture} the builder materializes
1938
+ * before the base ops fire (via its own freshly-minted key ref over the same
1939
+ * descriptor). The capture covers ALL sides for an UPDATE with RETURNING, so this
1940
+ * correlates the full joined row's identity.
1941
+ */
1942
+ export function buildMultiSourceUpdateReturning(ctx, view, stmt, capture, analysis) {
1943
+ const returningCols = stmt.returning;
1944
+ // Restrict the POST-mutation join body to the captured identities, built as plan
1945
+ // nodes over the ALREADY-planned `joinNode` (its structural ON-condition only —
1946
+ // the body/user WHERE is intentionally NOT re-applied) — no AST re-plan of the
1947
+ // body. The EXISTS subquery resolves `__vmupd_keys` via `cteNodes` to a fresh key
1948
+ // ref over the shared capture descriptor; `s<side>.pk<j>` correlate to the outer
1949
+ // join row through `joinScope`.
1950
+ //
1951
+ // The per-side identity predicate is AND'd over all sides:
1952
+ // - a **preserved** side keys by exact per-PK-column equality (`AND_j k.k<side>_<j>
1953
+ // = s<side>.pk<j>`) — its PK is stable across the mutation and uniquely identifies
1954
+ // the view row (the premise that makes a non-preserved column updatable at all);
1955
+ // - a **non-preserved** (outer-join null-extended) side keys by a matched-OR-null
1956
+ // disjunction `(AND_j k.k<np>_<j> = s<np>.pk<j>) OR (AND_j k.k<np>_<j> is null)`.
1957
+ //
1958
+ // A *matched* capture row (np PK non-null) takes the matched branch and finds the
1959
+ // stable np row; the null branch is false (the np PK is non-null). A *materialized
1960
+ // null-extended* capture row (np PK captured NULL — it had no pre-mutation partner)
1961
+ // fails the matched branch (`null = …` is not-true) and takes the null branch, so it
1962
+ // is identified by the preserved-side equalities ALONE — surfacing the freshly-minted
1963
+ // partner row (and a preserved-side update touching a still-null-extended row, the
1964
+ // latent partial-set bug #2). SQL three-valued comparison keeps the two branches
1965
+ // disjoint, so no explicit `is not null` guard is needed.
1966
+ const sideConds = analysis.sides.map((side, sideIndex) => {
1967
+ const pkCols = requireKeyColumns(view, side);
1968
+ const exact = pkCols.map((pk, j) => ({
1969
+ type: 'binary',
1970
+ operator: '=',
1971
+ left: { type: 'column', name: keyColumnName(sideIndex, j), table: 'k' },
1972
+ right: { type: 'column', name: pk, table: side.alias },
1973
+ })).reduce((acc, c) => combineAnd(acc, c));
1974
+ if (side.preserved)
1975
+ return exact;
1976
+ // Null-extended branch: every captured PK column of this non-preserved side is null
1977
+ // (no pre-mutation join partner), so the row is identified by the preserved sides'
1978
+ // exact equalities alone.
1979
+ const allNull = pkCols.map((_pk, j) => ({ type: 'unary', operator: 'IS NULL', expr: { type: 'column', name: keyColumnName(sideIndex, j), table: 'k' } }))
1980
+ .reduce((acc, c) => combineAnd(acc, c));
1981
+ return { type: 'binary', operator: 'OR', left: exact, right: allNull };
1982
+ });
1983
+ // Read the capture's own relation name so a fresh-named capture's RETURNING re-query
1984
+ // reads back from (and injects under) the same relation its base ops do — the constant
1985
+ // is only the default the standalone path stamps.
1986
+ const captureRelationName = capture.relationName ?? MS_UPDATE_KEYS_CTE;
1987
+ const keyRef = makeMultiSourceKeyRef(ctx.scope, capture, captureRelationName);
1988
+ const existsPredicateAst = {
1989
+ type: 'exists',
1990
+ subquery: {
1991
+ type: 'select',
1992
+ columns: [{ type: 'column', expr: { type: 'literal', value: 1 } }],
1993
+ from: [{ type: 'table', table: { type: 'identifier', name: captureRelationName }, alias: 'k' }],
1994
+ where: sideConds.reduce((acc, c) => combineAnd(acc, c)),
1995
+ },
1996
+ };
1997
+ const cteNodes = new Map(ctx.cteNodes ?? []);
1998
+ cteNodes.set(captureRelationName, keyRef);
1999
+ const existsNode = buildExpression({ ...ctx, scope: analysis.joinScope, cteNodes }, existsPredicateAst);
2000
+ const filtered = new FilterNode(analysis.joinScope, analysis.joinNode, existsNode);
2001
+ // Project the view-spelled, base-term RETURNING columns over the filtered join.
2002
+ return buildMultiSourceReturningProjection(ctx, view, analysis, filtered, returningCols);
2003
+ }
2004
+ /**
2005
+ * Build the multi-source RETURNING projection over a `filtered` join relation: lower
2006
+ * each RETURNING result column to its view-spelled, base-term form
2007
+ * ({@link buildReturningProjection}) and project it as a `ScalarPlanNode` over the
2008
+ * input through `analysis.joinScope`. `preserveInputColumns=false` ⇒ the output is
2009
+ * exactly the RETURNING columns. Shared by the UPDATE re-query (filter = the
2010
+ * post-mutation EXISTS-over-capture join) and the DELETE re-query (filter = the
2011
+ * pre-mutation identifying predicate over the raw join), which differ only in the
2012
+ * `filtered` input relation they pass.
2013
+ */
2014
+ function buildMultiSourceReturningProjection(ctx, view, analysis, filtered, returningCols) {
2015
+ const projections = buildReturningProjection(ctx, view, analysis, returningCols).map((rc) => {
2016
+ const col = rc;
2017
+ return {
2018
+ node: buildExpression({ ...ctx, scope: analysis.joinScope }, col.expr),
2019
+ alias: col.alias,
2020
+ };
2021
+ });
2022
+ return new ProjectNode(analysis.joinScope, filtered, projections, undefined, undefined, false);
2023
+ }
2024
+ /**
2025
+ * Lower a multi-source UPDATE's RETURNING result columns to base terms over the
2026
+ * join body, preserving the **view spelling** of each output column. A bare view-
2027
+ * column ref substitutes to its base term aliased to the column's written spelling
2028
+ * (so a renamed view col `eid`→base `id` still surfaces as `eid`); a computed
2029
+ * RETURNING expression has its nested view-column refs substituted; `returning *`
2030
+ * expands to every view output column's base term aliased to its display name.
2031
+ */
2032
+ function buildReturningProjection(ctx, view, analysis, returningCols) {
2033
+ const out = [];
2034
+ for (const rc of returningCols) {
2035
+ if (rc.type === 'all') {
2036
+ assertReturningStarQualifier(rc.table, view.name);
2037
+ for (const col of analysis.outColumns) {
2038
+ const baseExpr = analysis.viewColToBaseRef.get(col.name);
2039
+ if (!baseExpr) {
2040
+ raiseMutationDiagnostic({
2041
+ reason: 'returning-through-view',
2042
+ table: view.name,
2043
+ message: `cannot expand 'returning *' through view '${view.name}': no base term for column '${col.displayName}'`,
2044
+ });
2045
+ }
2046
+ out.push({ type: 'column', expr: cloneExpr(baseExpr), alias: col.displayName });
2047
+ }
2048
+ }
2049
+ else {
2050
+ // Scope guard: a top-level RETURNING reference must name a view column —
2051
+ // otherwise it would pass through `substituteViewColumns` unmapped and
2052
+ // re-bind against a base table in the re-query's join body (the same leak
2053
+ // the where/set guard closes). Parity with the single-source RETURNING guard.
2054
+ guardTopLevelScope(rc.expr, analysis, view);
2055
+ const substituted = substituteViewColumns(ctx, rc.expr, analysis.viewColToBaseRef, view, analysis.sides);
2056
+ // Preserve the user's view spelling as the output name: an explicit alias
2057
+ // wins; a bare column ref keeps its own name; otherwise leave it unnamed.
2058
+ const alias = rc.alias ?? (rc.expr.type === 'column' ? rc.expr.name : undefined);
2059
+ out.push({ type: 'column', expr: substituted, alias });
2060
+ }
2061
+ }
2062
+ return out;
2063
+ }
2064
+ // --- DELETE ---------------------------------------------------------------
2065
+ /**
2066
+ * Build the pre-mutation RETURNING re-query for a multi-source DELETE
2067
+ * (docs/view-updateability.md § `returning`). The OLD view image of the rows about
2068
+ * to vanish: project the view-spelled, base-term RETURNING columns over the raw
2069
+ * `analysis.joinNode` restricted to the identifying predicate (user WHERE → base ∧
2070
+ * body WHERE — the same predicate the key capture and base ops route on). Captured
2071
+ * `pre` (before the base ops fire) so it reads the live base tables through the join.
2072
+ *
2073
+ * Building the projection in **base terms** (rather than referencing the planned
2074
+ * body `root`'s output attribute ids) is what fixes a computed view column: a
2075
+ * computed column has no surviving intermediate attribute id after project-merge, so
2076
+ * a by-id reference dangles — recomputing from base columns has nothing fragile to
2077
+ * reference. Mirrors {@link buildMultiSourceUpdateReturning}; they differ only in the
2078
+ * filter + timing.
2079
+ */
2080
+ export function buildMultiSourceDeleteReturning(ctx, view, stmt, analysis) {
2081
+ const idPredicateAst = buildIdentifyingPredicate(ctx, analysis, stmt.where, view);
2082
+ const predicate = idPredicateAst
2083
+ ? buildExpression({ ...ctx, scope: analysis.joinScope }, idPredicateAst)
2084
+ : undefined;
2085
+ const filtered = predicate
2086
+ ? new FilterNode(analysis.joinScope, analysis.joinNode, predicate)
2087
+ : analysis.joinNode;
2088
+ return buildMultiSourceReturningProjection(ctx, view, analysis, filtered, stmt.returning);
2089
+ }
2090
+ export function decomposeDelete(_ctx, view, analysis, stmt, captureRelationName = MS_UPDATE_KEYS_CTE) {
2091
+ // RETURNING through a multi-source delete is supported via a re-query of the
2092
+ // planned view body captured *before* the base delete fires (the builder); the
2093
+ // base op itself carries no RETURNING.
2094
+ // Scope guard: top-level `where` references must name view columns (parity with
2095
+ // the single-source spine).
2096
+ if (stmt.where)
2097
+ guardTopLevelScope(stmt.where, analysis, view);
2098
+ const sides = chooseDeleteSides(view, analysis);
2099
+ // Every base delete (single-side and multi-side fan-out alike) reads its
2100
+ // identifying values from the up-front identity capture the builder materializes
2101
+ // ONCE before any base op fires (a correlated EXISTS over `__vmupd_keys` matching
2102
+ // the side's PK columns), a mutation-order-independent set built from the
2103
+ // ALREADY-planned join body. So the first side's delete cannot empty the join out
2104
+ // from under a later side's identifying set (the predicate-honest multi-side
2105
+ // fan-out), and a single-side delete — having no ordering hazard — reads the same
2106
+ // pre-mutation set it would have re-queried live. (No live re-query of a re-planned
2107
+ // AST body.)
2108
+ // Order the base deletes. The **two-side fan-out over a 2-table join** orders by ON
2109
+ // DELETE action so the side whose removal clears the other's reference runs first
2110
+ // (`orderDeleteFanout`); a mutual FK whose actions no side order can satisfy under
2111
+ // immediate enforcement raises `mutual-fk-restrict-delete` at plan time — but ONLY
2112
+ // when the join provably correlates a mutual FK edge (the joined rows necessarily
2113
+ // cross-reference, so a RESTRICT necessarily fires). When the join correlates neither
2114
+ // edge (e.g. a join on non-FK columns), the schema-only reject is a data-independent
2115
+ // over-rejection: fall back to the fixed `[0, 1]` fan-out and defer to the runtime
2116
+ // RESTRICT pre-check (`runtime/foreign-key-actions.ts`) on the actual data.
2117
+ //
2118
+ // This plan-time mutual-FK analysis is **deliberately NOT generalized past two
2119
+ // sides** (§ Out of scope): an n-way (>2) delete uses the **reverse** FK-topological
2120
+ // order (FK-child before FK-parent) over the chosen sides — the FK-safe delete
2121
+ // direction — and defers any mutual-FK cycle wholesale to the runtime RESTRICT
2122
+ // pre-check. A single-side delete has no ordering hazard, so it keeps its trivial
2123
+ // order.
2124
+ let order;
2125
+ if (analysis.sides.length === 2 && sides.length === 2) {
2126
+ const fanoutOrder = orderDeleteFanout(analysis.sides);
2127
+ if (fanoutOrder === undefined) {
2128
+ if (joinCorrelatesMutualFk(analysis)) {
2129
+ const [a, b] = analysis.sides;
2130
+ raiseMutationDiagnostic({
2131
+ reason: 'mutual-fk-restrict-delete',
2132
+ table: view.name,
2133
+ message: `cannot delete through view '${view.name}': the joined row spans a mutual foreign key ('${a.schema.name}'↔'${b.schema.name}') whose ON DELETE actions cannot be satisfied in either order under immediate FK enforcement (deleting either side trips the other's RESTRICT, directly or transitively through a cascade); break the cycle outside the view — null out the referencing column(s) first, or restructure the offending ON DELETE action — before deleting through the view (a 'deferrable initially deferred' declaration does not help: RESTRICT is enforced immediately regardless)`,
2134
+ });
2135
+ }
2136
+ // No mutual FK edge is correlated by the join — defer to the runtime
2137
+ // RESTRICT pre-check on the real data via the fixed fan-out order.
2138
+ order = [0, 1];
2139
+ }
2140
+ else {
2141
+ order = fanoutOrder;
2142
+ }
2143
+ }
2144
+ else {
2145
+ // Single side, or an n-way (>2) fan-out. Delete in **reverse** FK-topological
2146
+ // order — FK-CHILD before FK-parent — so a child's referencing row is gone before
2147
+ // its parent row is deleted (the canonical columnar-split shape: each member's PK
2148
+ // references the anchor's). The forward (parent-first) order trips the parent's
2149
+ // inbound RESTRICT/NO-ACTION under immediate FK enforcement and aborts the whole
2150
+ // statement. Child-first is unconditionally FK-safe (deleting a referencing row
2151
+ // never trips a constraint on the referenced row, for any ON DELETE action), so it
2152
+ // is the right default for both RESTRICT and CASCADE; a mutual-FK cycle still
2153
+ // defers wholesale to the runtime RESTRICT pre-check. A single-side delete reverses
2154
+ // a one-element order (a no-op). The eager up-front key capture makes the order
2155
+ // purely an FK-enforcement concern — identity is fixed before any op fires.
2156
+ order = orderSides(analysis.sides).filter(i => sides.includes(i)).reverse();
2157
+ }
2158
+ const ops = [];
2159
+ for (const sideIndex of order) {
2160
+ const side = analysis.sides[sideIndex];
2161
+ const where = buildCapturedKeyPredicate(view, side, sideIndex, captureRelationName);
2162
+ const statement = {
2163
+ type: 'delete',
2164
+ table: tableIdentifier(side.schema),
2165
+ where,
2166
+ contextValues: stmt.contextValues,
2167
+ schemaPath: stmt.schemaPath,
2168
+ loc: stmt.loc,
2169
+ };
2170
+ ops.push({ table: side.table, op: 'delete', statement });
2171
+ }
2172
+ return ops;
2173
+ }
2174
+ /**
2175
+ * Pick the base side(s) a join delete routes to (§ Inner Join — Deletes). Deleting
2176
+ * one side of an inner equi-join already removes the joined row from the view, so the
2177
+ * common case resolves to a single side; the maximal-lenient case fans out to every
2178
+ * candidate side ("make this joined row not exist"). Returns 1 or more sides.
2179
+ *
2180
+ * The routing is **predicate/FK truth only** — there is no tag override (the routing
2181
+ * tags were removed; a per-row presence/membership column, e.g. the outer-join
2182
+ * existence column, expresses any non-default side explicitly):
2183
+ * 1. The candidate set is the **preserved** side(s) — deleting a preserved side is the
2184
+ * only way the joined row leaves the view (§ Outer Joins — Deletes). An inner join
2185
+ * is all-preserved; a `full` outer join has no preserved side, so its delete defers.
2186
+ * 2. If a foreign key proves the FK-many (child) side, that single side (deleting the
2187
+ * child leaves the parent — the documented FK-style default; the FK resolves the
2188
+ * ambiguity, so it is NOT a fan-out).
2189
+ * 3. Otherwise the deletion side is ambiguous and the (hardwired) lenient default
2190
+ * **fans out to every candidate side** — the predicate-honest multi-side delete
2191
+ * (see {@link decomposeDelete}'s eager key capture).
2192
+ */
2193
+ function chooseDeleteSides(view, analysis) {
2194
+ // The candidate set is the **preserved** side(s) — deleting the preserved side is
2195
+ // the only way the joined row leaves the view (§ Outer Joins — Deletes). Inner
2196
+ // joins are all-preserved (⇒ the full set). A `full` outer join has no preserved
2197
+ // side (each side is both preserved and non-preserved per row), so a
2198
+ // statically-routed delete is not expressible — defer it.
2199
+ const candidates = preservedSideIndices(analysis.sides);
2200
+ if (candidates.length === 0) {
2201
+ raiseMutationDiagnostic({
2202
+ reason: 'unsupported-join',
2203
+ table: view.name,
2204
+ message: `cannot delete through view '${view.name}': a FULL outer join has no preserved side to route the delete to (each side is both preserved and non-preserved per row); deleting through a full-outer view is deferred`,
2205
+ });
2206
+ }
2207
+ if (candidates.length === 1)
2208
+ return candidates;
2209
+ // A provable FK-child side resolves the ambiguity to that single side (deleting the
2210
+ // child leaves the parent). `fkChildIndex` is binary, so for an n-way (>2) join it
2211
+ // is undefined and the delete fans out.
2212
+ const childIndex = fkChildIndex(analysis.sides);
2213
+ if (childIndex !== undefined && candidates.includes(childIndex))
2214
+ return [childIndex];
2215
+ // ≥2 residual candidates + no provable single-direction FK-child default: fan out to
2216
+ // every candidate side (the hardwired lenient, predicate-honest multi-side delete).
2217
+ return candidates;
2218
+ }
2219
+ // --- predicate / subquery construction ------------------------------------
2220
+ /**
2221
+ * The combined base-term identifying predicate: the user's WHERE (rewritten from
2222
+ * view columns to base terms) conjoined with the view body's own WHERE (already
2223
+ * in base terms). Either may be absent.
2224
+ */
2225
+ function buildIdentifyingPredicate(ctx, analysis, userWhere, view) {
2226
+ const userBase = userWhere ? substituteViewColumns(ctx, userWhere, analysis.viewColToBaseRef, view, analysis.sides) : undefined;
2227
+ const bodyWhere = analysis.sel.where ? cloneExpr(analysis.sel.where) : undefined;
2228
+ return combineAnd(userBase, bodyWhere);
2229
+ }
2230
+ /**
2231
+ * Substitute references to view columns (unqualified, or qualified by the view's
2232
+ * own name) with their base-term replacement expression. References already
2233
+ * qualified by a base alias are left untouched. A view-column reference nested
2234
+ * inside a `subquery` / `exists` / `in`-subquery operand is rewritten too, via
2235
+ * the scope-aware {@link makeViewColumnDescend} descent.
2236
+ *
2237
+ * Every injected replacement is **side-alias-qualified** ({@link
2238
+ * makeSideQualifyScope}, threaded both into the top-level substitution and as the
2239
+ * descent's `baseQualify`). A body may legally project a partner column BARE
2240
+ * (`select c.cid as cid, cval, pv from c join p …` — `pv` unambiguous across the
2241
+ * sides), so its lineage leaf arrives unqualified, and an unqualified leaf emitted
2242
+ * inside a subquery operand would re-bind, by innermost-scope SQL rules, to a
2243
+ * same-named column of that subquery's own FROM instead of the join body — the
2244
+ * multi-source analog of the single-source correlation-qualification of
2245
+ * substituted terms (`makeBaseQualifier`). Qualifying at injection keeps every
2246
+ * scope decision in this walk: downstream, the qualifier strip
2247
+ * ({@link stripSideQualifier}) is qualifier-driven, and a bare leaf reaching it is
2248
+ * only ever a user-authored local/unknown name. The strip is **alias-scope-aware**
2249
+ * for the converse case this pass does not cover — a *user-authored*
2250
+ * alias-qualified ref whose qualifier collides with a side alias/table name but is
2251
+ * shadowed by an inner value-subquery's own FROM alias is left subquery-local there
2252
+ * (these injected lineage leaves carry side aliases a user subquery would not reuse,
2253
+ * so they are never the shadowed ones).
2254
+ */
2255
+ function substituteViewColumns(ctx, expr, viewColToBaseRef, view, sides) {
2256
+ const viewName = view.name.toLowerCase();
2257
+ const sideQualifyScope = makeSideQualifyScope(sides, view);
2258
+ const sideQualify = (repl) => transformScopedExpr(ctx, sideQualifyScope, repl);
2259
+ const descend = makeViewColumnDescend(ctx, viewColToBaseRef, view.name, view, sideQualify);
2260
+ return transformExpr(expr, (col) => {
2261
+ if (col.table && col.table.toLowerCase() !== viewName)
2262
+ return undefined;
2263
+ const repl = viewColToBaseRef.get(col.name.toLowerCase());
2264
+ return repl ? sideQualify(repl) : undefined;
2265
+ }, descend);
2266
+ }
2267
+ /**
2268
+ * The {@link ScopeContext} that side-alias-qualifies a substituted base-term
2269
+ * lineage expression at injection time — the multi-source analog of the
2270
+ * single-source `makeBaseQualifyScope` (docs/view-updateability.md § Inner Join,
2271
+ * cross-source `set`). A bare, non-shadowed leaf is resolved by **unique column
2272
+ * ownership** across the join sides ({@link resolveColumnSide}, the exact rule
2273
+ * join-condition operands use) and qualified with the owning side's **alias** —
2274
+ * never the table name, so a self-join's distinct aliases stay distinct. A name
2275
+ * on NO side is a lineage-internal correlated/local name and stays bare; a name
2276
+ * on 2+ sides resolves to `undefined`, but such a bare body projection
2277
+ * (`select av …` with `av` on both sides) is already rejected as ambiguous at
2278
+ * body planning (analyzeBodyLineage → buildSelectStmt), so for genuine lineage
2279
+ * leaves that branch is unreachable — the bare pass-through serves only the
2280
+ * no-side case. Shadowing within the lineage's own nested subqueries is handled
2281
+ * by the shared scoped descent (a lineage term `(select x from oth where fk =
2282
+ * cid)` qualifies only its correlation ref `cid`; `x`/`fk`, shadowed by `oth`,
2283
+ * stay local).
2284
+ *
2285
+ * An unresolvable nested scope is **rejected** rather than tainted (matching
2286
+ * `makeBaseQualifyScope`): shadowing cannot be proven, so the term could over- or
2287
+ * under-qualify into a silent wrong write.
2288
+ */
2289
+ function makeSideQualifyScope(sides, view) {
2290
+ return {
2291
+ makeSubstitute: (shadowed) => (col) => {
2292
+ if (col.table)
2293
+ return undefined;
2294
+ if (shadowed.has(col.name.toLowerCase()))
2295
+ return undefined;
2296
+ const side = resolveColumnSide(col, sides);
2297
+ if (side === undefined)
2298
+ return undefined;
2299
+ return { ...col, table: sides[side].alias };
2300
+ },
2301
+ unresolvableScope: 'reject',
2302
+ rejectUnresolvableScope: () => raiseMutationDiagnostic({
2303
+ reason: 'unsupported-subquery-correlation',
2304
+ table: view.name,
2305
+ message: `cannot write through view '${view.name}': a view column's base-term lineage contains a correlated subquery whose source columns are not statically resolvable (a 'select *' / table-valued function / unresolved source), so its correlation cannot be proven; restructure the view body`,
2306
+ }),
2307
+ rejectDmlSubquery: () => raiseMutationDiagnostic({
2308
+ reason: 'unsupported-subquery-correlation',
2309
+ table: view.name,
2310
+ message: `cannot write through view '${view.name}': a data-modifying subquery (INSERT/UPDATE/DELETE) within a view column's base-term lineage cannot be analysed`,
2311
+ }),
2312
+ };
2313
+ }
2314
+ /**
2315
+ * Rewrite the owning side's alias qualifier on a base-term assignment value to the lowered
2316
+ * UPDATE's `__vm_self` correlation alias ({@link SELF_ALIAS}), so it binds the single-table
2317
+ * UPDATE's target row directly even when nested in a user value subquery whose own FROM
2318
+ * carries a same-named column. A reference to **any other side** cannot
2319
+ * be expressed as a single-table SET, so it is either captured-and-rewritten (when a
2320
+ * `registerCrossSource` carrier is supplied — § Inner Join, cross-source `set`) or
2321
+ * rejected (`cross-source-assignment`, the legacy path). The strip is qualifier-driven
2322
+ * but **alias-scope-aware**: the route/strip decision reads a column's own table
2323
+ * qualifier, yet a qualifier shadowed by an inner value-subquery's FROM **alias** binds
2324
+ * to that inner source (innermost-scope SQL rules), so it is left local. The alias-shadow
2325
+ * set accumulates per nesting depth through the {@link transformAliasScopedExpr} descent
2326
+ * (the alias-only analog of the view-column descent's column-name shadowing); a nested
2327
+ * *owning*-side reference whose qualifier is NOT shadowed is still correlated to the
2328
+ * target row of the lowered UPDATE just like a top-level one.
2329
+ *
2330
+ * The **alias-shadow check fires first** (before the owning/other qualifier sets), so a
2331
+ * user-authored alias-qualified ref colliding with a side alias OR a side's table name
2332
+ * (`from things c` shadowing owning alias `c`; `from aux parent` shadowing a side's table
2333
+ * name `parent`) is left subquery-local — never stripped to bare, never mis-routed
2334
+ * through the capture. Injected base-term lineage leaves carry side aliases a user
2335
+ * subquery would not intentionally reuse, so they are never shadowed; the narrowing
2336
+ * affects only genuine user collisions.
2337
+ *
2338
+ * The owning-side qualifier set is checked **before** the other-side set, so a self-join
2339
+ * (where an `other` side shares the owning side's table name) still strips an owning-alias
2340
+ * reference; only a reference qualified by a *different alias* is the cross-source case.
2341
+ *
2342
+ * A **bare** leaf is left untouched — binding locally (a nested subquery's own FROM, or
2343
+ * the lowered single-table UPDATE's target) or failing loudly at build. The strip never
2344
+ * resolves a bare name against the view sides: every base-term lineage leaf was
2345
+ * side-alias-qualified when `substituteViewColumns` injected it
2346
+ * ({@link makeSideQualifyScope} — including a partner column the body projected bare, so
2347
+ * that read rides the qualified routing below at ANY non-shadowed nesting depth), and
2348
+ * resolving a bare name here would mis-route an inner-scope column whose name merely collides with a
2349
+ * partner base column (e.g. `(select psecret from t)` where the partner side also has a
2350
+ * `psecret`) to the partner's captured value.
2351
+ *
2352
+ * A cross-source read is rewritten to `(select <srcN> from __vmupd_keys k where
2353
+ * k.k<owningSide>_0 = __vm_self.<pk0> [and …])`: `registerCrossSource` projects the partner
2354
+ * column into the capture under `srcN` and returns the alias; the `<pk_j>` (qualified with
2355
+ * the lowered UPDATE's `__vm_self` correlation alias — {@link SELF_ALIAS}) bind to its own
2356
+ * target row, so each row reads the captured pre-mutation
2357
+ * partner value of its joined row. The cross-source gate (`gateCrossSourceReads`) has
2358
+ * already proved every reached partner column has `base` lineage.
2359
+ *
2360
+ * Before the rewrite, `gateCrossSourceCardinality` (when supplied) rejects the **1:many**
2361
+ * direction at plan time: the capture carries one `srcN` row per joined owner/partner pair,
2362
+ * so the correlated read-back is well-defined only when the owning side joins **at most one**
2363
+ * partner row ({@link ownerJoinsAtMostOnePartner}). Placed here — at the rewrite site — so it
2364
+ * covers a partner ref nested in a value subquery as well as a top-level one (both lower to
2365
+ * {@link capturedValueSubquery}).
2366
+ */
2367
+ function stripSideQualifier(expr, view, owning, owningSideIndex, allSides, registerCrossSource, gateCrossSourceCardinality) {
2368
+ const owningQuals = new Set([owning.alias, owning.schema.name.toLowerCase()]);
2369
+ const otherQuals = new Set();
2370
+ allSides.forEach((s, i) => {
2371
+ if (i === owningSideIndex)
2372
+ return;
2373
+ otherQuals.add(s.alias);
2374
+ otherQuals.add(s.schema.name.toLowerCase());
2375
+ });
2376
+ // The owning side's PK — the correlation a captured cross-source read binds on.
2377
+ // Resolved lazily (only a cross-source rewrite needs it).
2378
+ let owningPk;
2379
+ // Route a partner-side base-column read through the up-front capture: project it into
2380
+ // `__vmupd_keys` under a stable `srcN` alias and rewrite the reference to a correlated
2381
+ // scalar read of it, keyed by the owning side's PK. Shared by the qualified-other branch
2382
+ // and the unqualified-partner branch (both lower identically; the `srcN` dedup key is
2383
+ // `<table>.<col>`, so a body mixing `a.av` and a partner-resolved bare `av` — qualified
2384
+ // here with the same alias — mints ONE capture column). Absent a capture carrier (the
2385
+ // legacy non-build path) reject `cross-source-assignment`.
2386
+ const routePartnerRead = (col) => {
2387
+ if (!registerCrossSource) {
2388
+ raiseMutationDiagnostic({
2389
+ reason: 'cross-source-assignment',
2390
+ column: col.name,
2391
+ table: view.name,
2392
+ message: `cannot write through view '${view.name}': an update value references column '${col.name}' on a different base table than the column it assigns; cross-source assignment is not supported`,
2393
+ });
2394
+ }
2395
+ // Reject the 1:many direction at plan time before lowering to a (multi-valued)
2396
+ // correlated read of the capture (§ Inner Join, cross-source `set`).
2397
+ gateCrossSourceCardinality?.(col);
2398
+ const srcAlias = registerCrossSource(col);
2399
+ owningPk ??= requireKeyColumns(view, owning);
2400
+ // Qualify the owning-PK operands with the per-side UPDATE's collision-proof alias so
2401
+ // the read-back correlates to the target row even when this subquery nests inside a
2402
+ // user value subquery whose FROM has a same-named column (the bug-1 site).
2403
+ return capturedValueSubquery(srcAlias, owningSideIndex, owningPk, undefined, SELF_ALIAS);
2404
+ };
2405
+ // QUALIFIED-only substitution: an owning-alias ref is re-qualified to the lowered
2406
+ // target's `__vm_self` correlation alias; a partner-alias ref
2407
+ // routes through the capture; a BARE ref is left untouched (only ever a user-authored
2408
+ // local/unknown name — every lineage leaf arrives side-alias-qualified; see the
2409
+ // docstring). The route/strip decision is qualifier-driven but ALIAS-SCOPE-AWARE: a
2410
+ // qualifier shadowed by an inner value-subquery FROM alias (`aliasShadow`) binds to that
2411
+ // inner source by innermost-scope SQL rules, so it is left local — checked BEFORE the
2412
+ // side-qualifier sets, so an owning-/partner-/table-name collision with an inner alias
2413
+ // never strips or routes. Injected lineage leaves carry side aliases a user subquery
2414
+ // would not reuse, so they are never shadowed; only a user-authored alias-qualified ref
2415
+ // colliding with a side alias/table name is affected. The alias set accumulates per
2416
+ // nesting depth via `transformAliasScopedExpr` (mirrors the view-column descent's
2417
+ // column-name shadowing); at the top level it is empty, so behaviour is byte-identical
2418
+ // for every non-colliding statement.
2419
+ const substitute = (col, aliasShadow) => {
2420
+ if (!col.table)
2421
+ return undefined;
2422
+ const t = col.table.toLowerCase();
2423
+ if (aliasShadow.has(t))
2424
+ return undefined;
2425
+ // Qualify the stripped owning ref with the per-side UPDATE's collision-proof alias
2426
+ // rather than emitting a bare column: a bare base-name ref nested in a user value
2427
+ // subquery whose FROM carries that base name would re-bind locally (the bug-2 site).
2428
+ if (owningQuals.has(t))
2429
+ return { type: 'column', name: col.name, table: SELF_ALIAS };
2430
+ if (otherQuals.has(t))
2431
+ return routePartnerRead(col);
2432
+ return undefined;
2433
+ };
2434
+ return transformAliasScopedExpr(expr, substitute);
2435
+ }
2436
+ /**
2437
+ * The correlated scalar read a cross-source SET value lowers to:
2438
+ * `(select <srcAlias> from __vmupd_keys k where k.k<owningSide>_0 = <pk0> [and …])`
2439
+ * — `<srcAlias>` is the capture projection of the partner base column; the unqualified
2440
+ * `<pk_j>` bind to the lowered UPDATE's own target row (the owning side), matching the
2441
+ * per-side identifying EXISTS so each target row reads the captured pre-mutation partner
2442
+ * value of its joined row. Composite owning keys conjoin one equality per PK column.
2443
+ *
2444
+ * `dedupAggregate` wraps the projection in that aggregate (`min(k.<srcAlias>)`) so the
2445
+ * correlated read is single-valued even when the owning PK matches MORE THAN ONE capture
2446
+ * row — the non-preserved-side fan-out case, where N preserved rows share one non-preserved
2447
+ * partner so its PK matches all N captures (§ Outer Joins). For a constant / np-only SET the
2448
+ * captured value is identical across the group so `min` is an exact no-op de-dup; for a
2449
+ * value that genuinely differs per preserved row it resolves the ambiguity deterministically
2450
+ * rather than erroring at runtime. The cross-source `set` callers leave it off (their gate
2451
+ * already proves at-most-one partner), keeping the bare-column form byte-identical.
2452
+ *
2453
+ * `correlationAlias` qualifies each owning-PK right operand (`<pk_j>` → `<alias>.<pk_j>`).
2454
+ * When this read-back nests inside a user value subquery whose own FROM introduces a
2455
+ * column named like the owning PK, a **bare** `<pk_j>` would re-bind to that inner column
2456
+ * by innermost-scope SQL rules (keying the read-back on the wrong value); qualifying it with
2457
+ * the lowered per-side UPDATE's collision-proof alias ({@link SELF_ALIAS}) binds the outer
2458
+ * target row instead. The multi-source per-side callers pass `SELF_ALIAS`; `decomposition.ts`
2459
+ * and any caller that omits it keep the bare form (byte-identical — composite keys still
2460
+ * qualify every conjunct only when supplied).
2461
+ *
2462
+ * `captureRelationName` is the relation the correlated read scans (`from <name> k`); it
2463
+ * defaults to {@link MS_UPDATE_KEYS_CTE} so the standalone multi-source and decomposition
2464
+ * callers are byte-identical, while a nested multi-source capture threads its fresh name.
2465
+ */
2466
+ export function capturedValueSubquery(srcAlias, owningSideIndex, owningPk, dedupAggregate, correlationAlias, captureRelationName = MS_UPDATE_KEYS_CTE) {
2467
+ const conds = owningPk.map((pk, j) => ({
2468
+ type: 'binary',
2469
+ operator: '=',
2470
+ left: { type: 'column', name: keyColumnName(owningSideIndex, j), table: 'k' },
2471
+ right: correlationAlias ? { type: 'column', name: pk, table: correlationAlias } : { type: 'column', name: pk },
2472
+ }));
2473
+ const colRef = { type: 'column', name: srcAlias, table: 'k' };
2474
+ const projection = dedupAggregate
2475
+ ? { type: 'function', name: dedupAggregate, args: [colRef] }
2476
+ : colRef;
2477
+ return {
2478
+ type: 'subquery',
2479
+ query: {
2480
+ type: 'select',
2481
+ columns: [{ type: 'column', expr: projection }],
2482
+ from: [{ type: 'table', table: { type: 'identifier', name: captureRelationName }, alias: 'k' }],
2483
+ where: conds.reduce((acc, c) => combineAnd(acc, c)),
2484
+ },
2485
+ };
2486
+ }
2487
+ /**
2488
+ * Reject a cross-source value read whose partner-side view column is **not** `base`
2489
+ * (computed / non-invertible) — its value is not recoverable from a captured base
2490
+ * column, so the cross-source rewrite cannot carry it (`no-inverse`; an outer-join
2491
+ * `null-extended` partner is already rejected wholesale upstream). A same-side read (the
2492
+ * column reads only the assigned side) is left to the qualifier strip; a `base` partner
2493
+ * column is admitted and captured. Walks only the value's top-level column references
2494
+ * (the scope `guardTopLevelScope` already proved are view columns); a reference nested in
2495
+ * a value subquery is left to the qualifier strip's per-leaf handling.
2496
+ *
2497
+ * Known asymmetry (deliberate, conservative): because this walks top level only, a
2498
+ * computed partner read nested in a value subquery is *admitted* via the per-leaf
2499
+ * capture — which is value-correct (leaves captured pre-mutation, scalar applied on
2500
+ * read) — while the same read at the top level is rejected here. `no-inverse` is only
2501
+ * a hard requirement for a computed column as an assignment *target*; admitting the
2502
+ * top-level read through the same capture is the intended unification, pending an
2503
+ * audit of mixed owning/partner leaves under an owning-site inverse. See
2504
+ * docs/view-updateability.md § cross-source `set` values.
2505
+ */
2506
+ function gateCrossSourceReads(value, owningSideIndex, analysis, view) {
2507
+ forEachTopLevelColumnRef(value, (col) => {
2508
+ const vco = analysis.outColumns.find(c => c.name === col.name.toLowerCase());
2509
+ if (!vco)
2510
+ return; // guardTopLevelScope already proved top-level refs are view columns
2511
+ const readSides = viewColumnReadSides(vco, analysis);
2512
+ const crossSource = [...readSides].some(s => s !== owningSideIndex);
2513
+ if (crossSource && !vco.writable) {
2514
+ raiseMutationDiagnostic({
2515
+ reason: 'no-inverse',
2516
+ column: vco.displayName,
2517
+ table: view.name,
2518
+ message: `cannot write through view '${view.name}': the update value reads computed column '${vco.displayName}' on a different base table than the column it assigns; a cross-source read requires the partner column to have base lineage`,
2519
+ });
2520
+ }
2521
+ });
2522
+ }
2523
+ /**
2524
+ * The set of join-side indices a view column's value reads. A `base` site reads only
2525
+ * its owning side; a computed site reads every side its base-term expression's column
2526
+ * leaves resolve to (so a same-side computed read stays admissible while a cross-source
2527
+ * computed read is rejected).
2528
+ */
2529
+ function viewColumnReadSides(vco, analysis) {
2530
+ if (vco.writable && vco.sideIndex !== undefined)
2531
+ return new Set([vco.sideIndex]);
2532
+ const sides = new Set();
2533
+ const expr = analysis.viewColToBaseRef.get(vco.name);
2534
+ if (expr) {
2535
+ forEachColumnRefDeep(expr, (col) => {
2536
+ const s = resolveColumnSide(col, analysis.sides);
2537
+ if (s !== undefined)
2538
+ sides.add(s);
2539
+ });
2540
+ }
2541
+ return sides;
2542
+ }
2543
+ /** Observe every TOP-LEVEL column reference in an expression (no subquery descent). */
2544
+ function forEachTopLevelColumnRef(expr, fn) {
2545
+ transformExpr(expr, (col) => { fn(col); return undefined; });
2546
+ }
2547
+ /** Observe every column reference in an expression, descending into subqueries. */
2548
+ function forEachColumnRefDeep(expr, fn) {
2549
+ const observe = (col) => { fn(col); return undefined; };
2550
+ transformExpr(expr, observe, (q) => mapQueryExprUniform(q, observe));
2551
+ }
2552
+ // --- helpers --------------------------------------------------------------
2553
+ function tableIdentifier(table) {
2554
+ return { type: 'identifier', name: table.name, schema: table.schemaName };
2555
+ }
2556
+ /**
2557
+ * The boolean value of a literal existence-flag assignment (`set hasB = true|false`), or
2558
+ * `undefined` for any non-literal / non-boolean value — a per-row branch on the *written*
2559
+ * value is deferred (§ Existence columns, v1). Accepts the boolean literals `true`/`false`
2560
+ * and the numeric `1`/`0` spellings (integers lower to `bigint` here).
2561
+ */
2562
+ function asBooleanLiteral(expr) {
2563
+ if (expr.type !== 'literal')
2564
+ return undefined;
2565
+ const v = expr.value;
2566
+ if (v === true || v === false)
2567
+ return v;
2568
+ if (v === 1 || v === 1n)
2569
+ return true;
2570
+ if (v === 0 || v === 0n)
2571
+ return false;
2572
+ return undefined;
2573
+ }
2574
+ /**
2575
+ * The side's primary-key column names (≥1), in declaration order — the per-side
2576
+ * identifying key the capture projects and the base ops' EXISTS correlates on.
2577
+ * Composite keys are admitted (each PK column contributes a `k<side>_<j>` capture
2578
+ * column); a keyless table is the only reject (`unsupported-join`).
2579
+ */
2580
+ function requireKeyColumns(view, side) {
2581
+ const pk = side.schema.primaryKeyDefinition;
2582
+ if (pk.length === 0) {
2583
+ raiseMutationDiagnostic({
2584
+ reason: 'unsupported-join',
2585
+ table: view.name,
2586
+ message: `cannot write through view '${view.name}': base table '${side.schema.name}' has no primary key; multi-source identifying predicates need a key`,
2587
+ });
2588
+ }
2589
+ return pk.map(def => side.schema.columns[def.index].name);
2590
+ }
2591
+ /**
2592
+ * True when `fk` (declared on `child`) targets `parent` — the shared FK-match
2593
+ * predicate: case-insensitive `referencedTable` against the parent's name, with an
2594
+ * absent `referencedSchema` defaulting to the child's own schema. The single source
2595
+ * of truth for "does this declared FK reference that side", reused by
2596
+ * {@link fkChildIndex}, {@link inboundDeleteAction}, and {@link edgeCorrelated}.
2597
+ */
2598
+ function fkTargetsSide(fk, child, parent) {
2599
+ return fk.referencedTable.toLowerCase() === parent.schema.name.toLowerCase()
2600
+ && (fk.referencedSchema ?? child.schema.schemaName).toLowerCase() === parent.schema.schemaName.toLowerCase();
2601
+ }
2602
+ /** True when `child` declares any foreign key onto `parent`. */
2603
+ function sideDeclaresFkOnto(child, parent) {
2604
+ return (child.schema.foreignKeys ?? []).some(fk => fkTargetsSide(fk, child, parent));
2605
+ }
2606
+ /**
2607
+ * Index of the FK-child (many) side of a **two-side** join: the side declaring a
2608
+ * foreign key onto the other. `undefined` when no FK is provable, both sides reference
2609
+ * each other (mutual), or the join is not two-sided (the binary FK-child concept does
2610
+ * not generalize past two sides — the n-way delete fan-out / `orderSides` topo sort
2611
+ * handle >2). Used by the two-side delete routing ({@link chooseDeleteSides}).
2612
+ */
2613
+ function fkChildIndex(sides) {
2614
+ if (sides.length !== 2)
2615
+ return undefined;
2616
+ const zeroRefsOne = sideDeclaresFkOnto(sides[0], sides[1]);
2617
+ const oneRefsZero = sideDeclaresFkOnto(sides[1], sides[0]);
2618
+ if (zeroRefsOne && !oneRefsZero)
2619
+ return 0;
2620
+ if (oneRefsZero && !zeroRefsOne)
2621
+ return 1;
2622
+ return undefined;
2623
+ }
2624
+ /**
2625
+ * Side execution order: an FK **topological sort** over the n sides — every FK-parent
2626
+ * precedes its FK-child — stable by source order within an FK-equivalence class. A
2627
+ * mutual FK (each side referencing the other, e.g. a self-join's two aliases of one
2628
+ * self-referencing table) forms a cycle with no zero-in-degree head; it is broken by
2629
+ * lowest source index, i.e. it falls back to **alias-declaration order** (§ Cycles,
2630
+ * Self-Joins). The two-side binary order (`[parent, child]` / `[0, 1]`) is the n=2
2631
+ * specialization of this.
2632
+ */
2633
+ function orderSides(sides) {
2634
+ const n = sides.length;
2635
+ // parents[child] = set of side indices the child must follow (its declared FK parents).
2636
+ const parents = sides.map(() => new Set());
2637
+ for (let child = 0; child < n; child++) {
2638
+ for (let parent = 0; parent < n; parent++) {
2639
+ if (child !== parent && sideDeclaresFkOnto(sides[child], sides[parent]))
2640
+ parents[child].add(parent);
2641
+ }
2642
+ }
2643
+ const placed = new Set();
2644
+ const order = [];
2645
+ while (order.length < n) {
2646
+ // Lowest-index unplaced side all of whose (non-self, unplaced-cycle-aside) parents
2647
+ // are already placed.
2648
+ let pick = -1;
2649
+ for (let i = 0; i < n; i++) {
2650
+ if (placed.has(i))
2651
+ continue;
2652
+ if ([...parents[i]].every(p => placed.has(p))) {
2653
+ pick = i;
2654
+ break;
2655
+ }
2656
+ }
2657
+ // A cycle (mutual FK) leaves no ready node — break it by lowest unplaced index
2658
+ // (source / alias-declaration order).
2659
+ if (pick === -1) {
2660
+ for (let i = 0; i < n; i++)
2661
+ if (!placed.has(i)) {
2662
+ pick = i;
2663
+ break;
2664
+ }
2665
+ }
2666
+ order.push(pick);
2667
+ placed.add(pick);
2668
+ }
2669
+ return order;
2670
+ }
2671
+ /**
2672
+ * The governing ON DELETE action of FK(s) declared on `child` that reference
2673
+ * `parent` — i.e. the action that fires when a `parent` row is deleted. When more
2674
+ * than one such FK runs between the same ordered pair, the **most-blocking** action
2675
+ * governs (immediate enforcement fires every referencing FK): `restrict` over
2676
+ * `cascade` over `setNull`/`setDefault` over absent (`undefined` — no FK on `child`
2677
+ * references `parent`). Mirrors the FK-match predicate in {@link fkChildIndex} (same
2678
+ * `referencedTable` / `referencedSchema` comparison).
2679
+ */
2680
+ function inboundDeleteAction(child, parent) {
2681
+ let governing;
2682
+ for (const fk of child.schema.foreignKeys ?? []) {
2683
+ if (!fkTargetsSide(fk, child, parent))
2684
+ continue;
2685
+ if (fk.onDelete === 'restrict')
2686
+ return 'restrict'; // most-blocking — governs outright
2687
+ if (fk.onDelete === 'cascade')
2688
+ governing = 'cascade';
2689
+ else if (governing !== 'cascade')
2690
+ governing = fk.onDelete; // setNull / setDefault, unless a cascade already won
2691
+ }
2692
+ return governing;
2693
+ }
2694
+ /**
2695
+ * True when deleting side `X` first (then the other side `Y`) does **not** abort
2696
+ * under immediate FK enforcement + the transitive RESTRICT pre-walk
2697
+ * (`runtime/foreign-key-actions.ts` `assertTransitiveRestrictsForParentMutation`).
2698
+ * `inboundX` governs deleting X (the action of the FK referencing X); `inboundY`
2699
+ * governs deleting Y. Deleting X first is feasible iff X carries no inbound
2700
+ * reference, or its inbound action *clears* Y's reference without tripping a RESTRICT:
2701
+ * - `inboundX` absent — nothing references X, so its delete is unconstrained;
2702
+ * - `inboundX ∈ {setNull, setDefault}` — Y's reference is cleared (no cascade, no RESTRICT);
2703
+ * - `inboundX === cascade` **and** `inboundY !== restrict` — the cascade into Y does
2704
+ * not recurse into a RESTRICT (Y's only inbound child here is X, the root, via `inboundY`).
2705
+ * `inboundX === restrict` ⇒ Y still references X ⇒ NOT deletable-first.
2706
+ */
2707
+ function deletableFirst(inboundX, inboundY) {
2708
+ if (inboundX === undefined)
2709
+ return true;
2710
+ if (inboundX === 'setNull' || inboundX === 'setDefault')
2711
+ return true;
2712
+ return inboundX === 'cascade' && inboundY !== 'restrict';
2713
+ }
2714
+ /**
2715
+ * The feasible base-delete order for the two-side DELETE fan-out, or `undefined`
2716
+ * when a mutual FK's ON DELETE actions cannot be satisfied in *any* order under
2717
+ * immediate enforcement (the caller raises `mutual-fk-restrict-delete`). Reached
2718
+ * only at `fkChildIndex(sides) === undefined` (no FK either way, or a mutual FK): a
2719
+ * no-FK pair has both inbound actions absent ⇒ side0 deletable-first ⇒ `[0, 1]`
2720
+ * (unchanged); a both-cascade mutual FK likewise keeps `[0, 1]`. Prefers `[0, 1]`
2721
+ * when side0 is deletable-first so the no-FK / symmetric paths stay order-stable.
2722
+ */
2723
+ function orderDeleteFanout(sides) {
2724
+ const inbound0 = inboundDeleteAction(sides[1], sides[0]); // governs deleting side0
2725
+ const inbound1 = inboundDeleteAction(sides[0], sides[1]); // governs deleting side1
2726
+ if (deletableFirst(inbound0, inbound1))
2727
+ return [0, 1];
2728
+ if (deletableFirst(inbound1, inbound0))
2729
+ return [1, 0];
2730
+ return undefined;
2731
+ }
2732
+ /**
2733
+ * Canonical (order-independent) key for a cross-side column equality, so a join
2734
+ * conjunct written either way (`b.aref = a.aid` or `a.aid = b.aref`) hashes the
2735
+ * same and an edge lookup need not know which operand the join named first.
2736
+ */
2737
+ function crossEqualityKey(sideA, colA, sideB, colB) {
2738
+ const a = `${sideA}:${colA.toLowerCase()}`;
2739
+ const b = `${sideB}:${colB.toLowerCase()}`;
2740
+ return a < b ? `${a}|${b}` : `${b}|${a}`;
2741
+ }
2742
+ /**
2743
+ * Resolve a join-condition column operand to its owning side index (`0..n-1`), or
2744
+ * `undefined` when the reference cannot be pinned to exactly one side. An explicit
2745
+ * `.table` qualifier matches a side's `alias` (already lowercased) or `schema.name`
2746
+ * (alias preferred, so a self-join's distinct aliases resolve unambiguously even
2747
+ * though the table names collide); an unqualified ref resolves by **unique** ownership
2748
+ * of `col.name` across the sides' columns. An ambiguous / unresolved ref returns
2749
+ * `undefined` (conservative — a term that cannot be placed cannot prove correlation).
2750
+ */
2751
+ function resolveColumnSide(col, sides) {
2752
+ const qualifier = col.table?.toLowerCase();
2753
+ if (qualifier !== undefined) {
2754
+ const idx = sides.findIndex(s => s.alias === qualifier || s.schema.name.toLowerCase() === qualifier);
2755
+ return idx < 0 ? undefined : idx;
2756
+ }
2757
+ const colName = col.name.toLowerCase();
2758
+ const owners = sides.flatMap((s, i) => s.schema.columns.some(c => c.name.toLowerCase() === colName) ? [i] : []);
2759
+ return owners.length === 1 ? owners[0] : undefined;
2760
+ }
2761
+ /**
2762
+ * True when the **owning** side (the side a cross-source `set` assigns) provably joins
2763
+ * **at most one** row of the **partner** side (the side the SET value reads), across the
2764
+ * view's join — the cardinality proof that makes a cross-source `set` value well-defined
2765
+ * (§ Inner Join, cross-source `set`). The up-front `__vmupd_keys` capture carries one
2766
+ * `srcN` row per joined owner/partner pair, so the per-row correlated read-back
2767
+ * ({@link capturedValueSubquery}) is single-valued only when the owning side joins at most
2768
+ * one partner. The **reverse** (1:many) direction returns multiple `srcN` rows for a fixed
2769
+ * owner PK and would fail at runtime with the generic `Scalar subquery returned more than
2770
+ * one row`; the caller rejects it at plan time instead with a diagnostic that names the
2771
+ * cross-source ambiguity.
2772
+ *
2773
+ * The proof: collect the join's **direct** owner↔partner `column = column` equalities
2774
+ * ({@link collectCrossSideEqualities} already walks every nested ON predicate and USING
2775
+ * list across the n-way tree), gather the **partner-side** columns they pin, and check
2776
+ * whether some **unique key** of the partner table is a subset of that pinned set — fixing
2777
+ * each column of a unique key to a per-owner-row value admits ≤1 partner row. Partner
2778
+ * unique keys considered: the PRIMARY KEY; every **non-partial** UNIQUE constraint; every
2779
+ * **non-partial** UNIQUE index. A **partial** unique key (one carrying a `predicate`) does
2780
+ * not bound the rows outside its predicate scope, so it does not prove global at-most-one
2781
+ * and is NOT counted. NULL semantics need no special handling — a `=` join only matches
2782
+ * non-null equal values and a unique key bounds each non-null value to ≤1 row (PK columns
2783
+ * are NOT NULL regardless).
2784
+ *
2785
+ * This is the inverse of the FK-correlation reasoning {@link edgeCorrelated} the delete
2786
+ * path uses, but **FK is not required** — the proof is purely partner-side uniqueness (the
2787
+ * canonical FK-child-reads-parent case is subsumed: the FK references the parent's PK and
2788
+ * the join equates the child's FK column to it, so the parent's PK ⊆ the pinned set).
2789
+ * **Multi-hop / transitive** cross-source (owner and partner not directly joined) pins no
2790
+ * partner column ⇒ NOT proven ⇒ the caller rejects (conservative: this only over-rejects,
2791
+ * never falsely accepts; a transitive value-determinacy proof is a possible follow-up).
2792
+ */
2793
+ function ownerJoinsAtMostOnePartner(ownerIdx, partnerIdx, sel, sides) {
2794
+ const partner = sides[partnerIdx];
2795
+ // The partner-side columns the join pins equal to an owner-side value (lowercased).
2796
+ const partnerEquatedCols = new Set();
2797
+ for (const eq of collectCrossSideEqualities(sel.from, sides)) {
2798
+ if (eq.sideA === ownerIdx && eq.sideB === partnerIdx)
2799
+ partnerEquatedCols.add(eq.colB.toLowerCase());
2800
+ else if (eq.sideB === ownerIdx && eq.sideA === partnerIdx)
2801
+ partnerEquatedCols.add(eq.colA.toLowerCase());
2802
+ }
2803
+ if (partnerEquatedCols.size === 0)
2804
+ return false; // no direct owner↔partner equality — not proven (e.g. multi-hop)
2805
+ // A non-empty unique-key column set all of whose columns the join pins ⇒ ≤1 partner row.
2806
+ const provesAtMostOne = (cols) => cols.length > 0 && cols.every(c => partnerEquatedCols.has(c.toLowerCase()));
2807
+ // The partner's PRIMARY KEY.
2808
+ const pkNames = partner.schema.primaryKeyDefinition.map(def => partner.schema.columns[def.index].name);
2809
+ if (provesAtMostOne(pkNames))
2810
+ return true;
2811
+ // Non-partial UNIQUE constraints (a partial UNIQUE bounds uniqueness only within its predicate scope).
2812
+ for (const uc of partner.schema.uniqueConstraints ?? []) {
2813
+ if (uc.predicate)
2814
+ continue;
2815
+ if (provesAtMostOne(uc.columns.map(idx => partner.schema.columns[idx].name)))
2816
+ return true;
2817
+ }
2818
+ // Non-partial UNIQUE indexes (e.g. a CREATE UNIQUE INDEX not mirrored as a constraint).
2819
+ for (const idx of partner.schema.indexes ?? []) {
2820
+ if (!idx.unique || idx.predicate)
2821
+ continue;
2822
+ if (provesAtMostOne(idx.columns.map(c => partner.schema.columns[c.index].name)))
2823
+ return true;
2824
+ }
2825
+ return false;
2826
+ }
2827
+ /**
2828
+ * True when the FK on side `childIdx` referencing side `parentIdx` is **correlated**
2829
+ * by the join — i.e. the join's cross-side equalities force the child's FK column(s)
2830
+ * equal to the parent's referenced column(s) for *every* `(childCol, refCol)` pair,
2831
+ * so a joined partner necessarily references the deleted row (a RESTRICT necessarily
2832
+ * fires). Matches the same `referencedTable` / `referencedSchema` predicate as
2833
+ * {@link fkChildIndex}; any one matching FK whose whole column pairing is equated
2834
+ * makes the edge correlated.
2835
+ */
2836
+ function edgeCorrelated(childIdx, parentIdx, crossEqualities, sides) {
2837
+ const child = sides[childIdx];
2838
+ const parent = sides[parentIdx];
2839
+ return (child.schema.foreignKeys ?? []).some(fk => {
2840
+ if (!fkTargetsSide(fk, child, parent))
2841
+ return false;
2842
+ const refIndices = resolveReferencedColumns(fk, parent.schema);
2843
+ if (refIndices.length !== fk.columns.length)
2844
+ return false;
2845
+ return fk.columns.every((childColIdx, i) => {
2846
+ const childCol = child.schema.columns[childColIdx].name;
2847
+ const refCol = parent.schema.columns[refIndices[i]].name;
2848
+ return crossEqualities.has(crossEqualityKey(childIdx, childCol, parentIdx, refCol));
2849
+ });
2850
+ });
2851
+ }
2852
+ /**
2853
+ * Whether the view's join **provably correlates at least one mutual FK edge** — the
2854
+ * gate on the plan-time `mutual-fk-restrict-delete` reject (§ Inner Join — Deletes).
2855
+ * Reached only when {@link orderDeleteFanout} found no feasible order (a mutual FK
2856
+ * whose actions no order can satisfy). The two mutual edges mirror the
2857
+ * {@link fkChildIndex} match: edgeA = the FK on side0 referencing side1, edgeB = the
2858
+ * FK on side1 referencing side0. An edge is *correlated* when the join's cross-side
2859
+ * column equalities force that FK's child column(s) equal to the parent's referenced
2860
+ * column(s) — so the joined partner necessarily references the deleted row and a
2861
+ * RESTRICT necessarily fires.
2862
+ *
2863
+ * Cross-side equalities are collected from the join ON condition (`sel.from[0]` is the
2864
+ * single `join`) **and** the body WHERE, flattened on `AND`, keeping each conjunct
2865
+ * that is `column = column` with both operands resolving to *different* sides
2866
+ * ({@link resolveColumnSide}; an unresolved/ambiguous/same-side term is skipped —
2867
+ * conservatively, it cannot prove correlation).
2868
+ *
2869
+ * Returns `true` iff **at least one** edge is correlated. A non-FK join (or a join on
2870
+ * non-FK columns) correlates neither edge ⇒ `false`, and the caller falls back to the
2871
+ * fixed-order fan-out, deferring to the runtime RESTRICT pre-check on the real data.
2872
+ * This is a strict *reduction* of over-rejection, not perfect precision: a join that
2873
+ * correlates one edge whose *other* edge's FK columns happen to be NULL at delete time
2874
+ * is still rejected (indistinguishable at plan time from the (fo-h) data-referencing
2875
+ * shape — accepted residual conservatism).
2876
+ */
2877
+ function joinCorrelatesMutualFk(analysis) {
2878
+ const conjuncts = [];
2879
+ const join = analysis.sel.from?.[0];
2880
+ if (join && join.type === 'join' && join.condition)
2881
+ conjuncts.push(...flattenAnd(join.condition));
2882
+ if (analysis.sel.where)
2883
+ conjuncts.push(...flattenAnd(analysis.sel.where));
2884
+ const crossEqualities = new Set();
2885
+ for (const conj of conjuncts) {
2886
+ if (conj.type !== 'binary' || conj.operator !== '=')
2887
+ continue;
2888
+ if (conj.left.type !== 'column' || conj.right.type !== 'column')
2889
+ continue;
2890
+ const leftSide = resolveColumnSide(conj.left, analysis.sides);
2891
+ const rightSide = resolveColumnSide(conj.right, analysis.sides);
2892
+ if (leftSide === undefined || rightSide === undefined || leftSide === rightSide)
2893
+ continue;
2894
+ crossEqualities.add(crossEqualityKey(leftSide, conj.left.name, rightSide, conj.right.name));
2895
+ }
2896
+ return edgeCorrelated(0, 1, crossEqualities, analysis.sides)
2897
+ || edgeCorrelated(1, 0, crossEqualities, analysis.sides);
2898
+ }
2899
+ /**
2900
+ * RETURNING through a multi-source **insert** is not yet supported: it would need
2901
+ * the per-row minted shared surrogate threaded into the projected rows, which the
2902
+ * envelope materialization does not yet expose to a RETURNING projection. Reject
2903
+ * with a structured diagnostic (single- and multi-source update/delete RETURNING
2904
+ * are supported; see the builder).
2905
+ */
2906
+ function rejectReturning(view, returning) {
2907
+ if (returning && returning.length > 0) {
2908
+ raiseMutationDiagnostic({
2909
+ reason: 'returning-through-view',
2910
+ table: view.name,
2911
+ message: `RETURNING through a multi-source (join) insert into view '${view.name}' is not yet supported`,
2912
+ });
2913
+ }
2914
+ }
2915
+ //# sourceMappingURL=multi-source.js.map