@quereus/quereus 3.3.0 → 4.1.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 +204 -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 +716 -27
  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 +13 -0
  80. package/dist/src/func/registration.d.ts.map +1 -1
  81. package/dist/src/func/registration.js +5 -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 +20 -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 +17 -16
@@ -1,7 +1,13 @@
1
1
  import { Schema } from './schema.js';
2
+ import { normalizeCollationName } from '../util/comparison.js';
2
3
  import { quereusError, QuereusError } from '../common/errors.js';
3
4
  import { StatusCode } from '../common/types.js';
4
- import { buildColumnIndexMap, columnDefToSchema, findPKDefinition, opsToMask, mutationContextVarToSchema, extractGeneratedColumnDependencies, topoSortGeneratedColumns } from './table.js';
5
+ import { buildColumnIndexMap, columnDefToSchema, findPKDefinition, opsToMask, mutationContextVarToSchema, extractGeneratedColumnDependencies, topoSortGeneratedColumns, requireVtabModule, resolveNamedConstraintClass, appendIndexToTableSchema } from './table.js';
6
+ import { buildUniqueConstraintSchema, buildForeignKeyConstraintSchema, validateForeignKeyCollations } from './constraint-builder.js';
7
+ import { normalizeBackingModule } from './view.js';
8
+ import { isMaintainedTable } from './derivation.js';
9
+ import { isHiddenImplicitIndex, findExposedImplicitConstraintIndex } from './catalog.js';
10
+ import { buildLensBasisFkGate } from './lens-fk-discovery.js';
5
11
  import { createLogger } from '../common/logger.js';
6
12
  import { Parser } from '../parser/parser.js';
7
13
  import { traverseAst } from '../parser/visitor.js';
@@ -13,10 +19,49 @@ import { BuildTimeDependencyTracker } from '../planner/planning-context.js';
13
19
  import { GlobalScope } from '../planner/scopes/global.js';
14
20
  import { ParameterScope } from '../planner/scopes/param.js';
15
21
  import { hasNativeEventSupport } from '../util/event-support.js';
16
- import { quoteIdentifier } from '../emit/ast-stringify.js';
22
+ import { quoteIdentifier, createViewToString, astToString } from '../emit/ast-stringify.js';
23
+ import { materializeView, adoptMaterializedView, deriveBackingShape, backingShapeMatches, assertDeclaredColumnArity } from '../runtime/emit/materialized-view-helpers.js';
17
24
  const log = createLogger('schema:manager');
18
25
  const warnLog = log.extend('warn');
19
26
  const errorLog = log.extend('error');
27
+ /**
28
+ * Shared frozen empty result for the reverse-FK lookup miss (the O(1)
29
+ * unreferenced-table gate), so the hot path allocates nothing per call.
30
+ */
31
+ const EMPTY_REFERENCING_FKS = Object.freeze([]);
32
+ /** Build the import spec from the `create materialized view` sugar form. */
33
+ function maintainedImportFromMvStmt(stmt) {
34
+ return {
35
+ schemaName: stmt.view.schema,
36
+ name: stmt.view.name,
37
+ select: stmt.select,
38
+ columns: stmt.columns,
39
+ moduleName: stmt.moduleName,
40
+ moduleArgs: stmt.moduleArgs,
41
+ tags: stmt.tags,
42
+ };
43
+ }
44
+ /**
45
+ * Build the import spec from the canonical `create table … maintained as …` form
46
+ * (the unified-model persistence / export form). The rename list is read from the
47
+ * `maintained [(columns)]` clause, NOT the declared column list: its presence is
48
+ * the lossless flag that distinguishes an explicit MV-sugar rename (arity-locks —
49
+ * a widened source is a sited error) from an implicit `select *` body (reshapes to
50
+ * follow its source on reopen). `generateMaintainedTableDDL` emits the clause iff
51
+ * the derivation carries a rename, so this restores `derivation.columns` faithfully.
52
+ */
53
+ function maintainedImportFromTableStmt(stmt) {
54
+ const maintained = stmt.maintained;
55
+ return {
56
+ schemaName: stmt.table.schema,
57
+ name: stmt.table.name,
58
+ select: maintained.select,
59
+ columns: maintained.columns,
60
+ moduleName: stmt.moduleName,
61
+ moduleArgs: stmt.moduleArgs,
62
+ tags: stmt.tags,
63
+ };
64
+ }
20
65
  /**
21
66
  * Manages all schemas associated with a database connection (main, temp, attached).
22
67
  * Handles lookup resolution according to SQLite's rules.
@@ -38,6 +83,44 @@ export class SchemaManager {
38
83
  * See `assertion-hoist-cache.ts` and `core/database-assertions.ts`.
39
84
  */
40
85
  assertionHoistSuppressed = 0;
86
+ /**
87
+ * Re-entrancy guard: when truthy, the read-side materialized-view query-rewrite
88
+ * rule (`rule-materialized-view-rewrite.ts`) is suppressed. Set while planning a
89
+ * materialized view's own body for the purpose of (re)computing or maintaining
90
+ * its backing table (create / refresh / row-time-maintenance compile). Without
91
+ * it, the rewrite rule would recognize the MV's body as "answered from" the MV
92
+ * itself and rewrite it to scan the backing table being populated — reading a
93
+ * stale/empty snapshot instead of recomputing from the source.
94
+ */
95
+ mvRewriteSuppressed = 0;
96
+ /**
97
+ * Catalog-level reverse foreign-key index: referenced `schema.table`
98
+ * (lowercased) → the FKs that reference it. `null` ⇒ needs a (re)build from
99
+ * the live catalog on next access — a pure derived cache, nulled on every
100
+ * mutation that can add/drop/retarget an FK or add/remove a schema (see
101
+ * {@link invalidateReverseFkIndex}). See `getReferencingForeignKeys`.
102
+ */
103
+ reverseFkIndex = null;
104
+ /**
105
+ * Lens basis-FK gate: the set of basis `schema.table` keys (lowercased) that
106
+ * back ≥1 logical parent slot referenced by ≥1 logical FK — the logical-FK
107
+ * analogue of {@link reverseFkIndex}. `null` ⇒ rebuild from the live catalog on
108
+ * next access. A pure derived cache (built by {@link buildLensBasisFkGate}),
109
+ * nulled on every event that can change the underlying slot scan: lens deploy
110
+ * (no `SchemaChangeEvent` fires — `lens-compiler` calls
111
+ * {@link invalidateLensFkGate} directly), any `table_added`/`_modified`/`_removed`
112
+ * (basis-table catalog change, via the constructor listener), and schema
113
+ * attach/detach/reset (no event — invalidated directly).
114
+ *
115
+ * Soundness invariant (load-bearing): a stale gate that **under-reports** would
116
+ * silently drop logical FK enforcement (cascade not propagated / RESTRICT not
117
+ * enforced / divergent basis action not suppressed) — the fatal direction — so
118
+ * invalidation must be exhaustive. Built from, and reset alongside, the same
119
+ * catalog state the three lens FK paths scan, it never under-reports for the
120
+ * current catalog; over-reporting (a stray key ⇒ an on-hit scan that finds
121
+ * nothing) is harmless. See {@link basisTableBacksLogicalParentFk}.
122
+ */
123
+ lensFkGate = null;
41
124
  /**
42
125
  * Creates a new schema manager
43
126
  *
@@ -48,6 +131,27 @@ export class SchemaManager {
48
131
  // Ensure 'main' and 'temp' schemas always exist
49
132
  this.schemas.set('main', new Schema('main'));
50
133
  this.schemas.set('temp', new Schema('temp'));
134
+ // Self-subscribe so any table FK-lifecycle event invalidates the reverse FK
135
+ // index. An FK is declared on a table, and a table enters/leaves/changes the
136
+ // catalog ONLY through one of these events: `create table … references`
137
+ // (table_added), `alter table add/drop constraint` and FK retargets from a
138
+ // parent/column rename (table_modified), and `drop table` (table_removed) —
139
+ // so this is exhaustive. The body just nulls the cache (rebuild happens on
140
+ // next access, never inside the listener), so subscribing to our own notifier
141
+ // is order-independent and safe. Schema ATTACH/DETACH fire no event, so those
142
+ // invalidate directly in addSchema/getOrCreateSchema/removeSchema. The
143
+ // listener lifetime is the SchemaManager's (no disposal path to unsubscribe).
144
+ // The same table events also change basis-table resolution for the lens
145
+ // basis-FK gate (a basis table created after the gate was built is the
146
+ // under-report vector; a drop / column-rename can change which slot resolves
147
+ // to which basis), so reset it on the same events. Lens-slot lifecycle fires
148
+ // no event — `lens-compiler.deployLogicalSchema` invalidates the gate directly.
149
+ this.changeNotifier.addListener(event => {
150
+ if (event.type === 'table_added' || event.type === 'table_modified' || event.type === 'table_removed') {
151
+ this.invalidateReverseFkIndex();
152
+ this.invalidateLensFkGate();
153
+ }
154
+ });
51
155
  }
52
156
  /**
53
157
  * Sets the current default schema for unqualified names
@@ -224,7 +328,7 @@ export class SchemaManager {
224
328
  if (existing) {
225
329
  this.changeNotifier.notifyChange({
226
330
  type: 'assertion_modified',
227
- schemaName: schemaName,
331
+ schemaName: schema.name,
228
332
  objectName: assertion.name,
229
333
  oldObject: existing,
230
334
  newObject: assertion,
@@ -233,7 +337,7 @@ export class SchemaManager {
233
337
  else {
234
338
  this.changeNotifier.notifyChange({
235
339
  type: 'assertion_added',
236
- schemaName: schemaName,
340
+ schemaName: schema.name,
237
341
  objectName: assertion.name,
238
342
  newObject: assertion,
239
343
  });
@@ -254,7 +358,7 @@ export class SchemaManager {
254
358
  if (removed) {
255
359
  this.changeNotifier.notifyChange({
256
360
  type: 'assertion_removed',
257
- schemaName: schemaName,
361
+ schemaName: schema.name,
258
362
  objectName: name,
259
363
  oldObject: existing,
260
364
  });
@@ -289,21 +393,80 @@ export class SchemaManager {
289
393
  this.assertionHoistSuppressed--;
290
394
  }
291
395
  }
396
+ /**
397
+ * True when the read-side materialized-view query-rewrite rule must be
398
+ * suppressed (the caller is currently planning an MV's own body to recompute or
399
+ * maintain its backing). Read by `rule-materialized-view-rewrite.ts`.
400
+ */
401
+ isMaterializedViewRewriteSuppressed() {
402
+ return this.mvRewriteSuppressed > 0;
403
+ }
404
+ /**
405
+ * Run a synchronous `fn` with the materialized-view query-rewrite rule
406
+ * suppressed. Re-entrant via a depth counter; always restores state, even on
407
+ * throw. Wrap every place that plans an MV body to (re)compute its backing.
408
+ */
409
+ withSuppressedMaterializedViewRewrite(fn) {
410
+ this.mvRewriteSuppressed++;
411
+ try {
412
+ return fn();
413
+ }
414
+ finally {
415
+ this.mvRewriteSuppressed--;
416
+ }
417
+ }
418
+ /** Async counterpart of {@link withSuppressedMaterializedViewRewrite}. */
419
+ async withSuppressedMaterializedViewRewriteAsync(fn) {
420
+ this.mvRewriteSuppressed++;
421
+ try {
422
+ return await fn();
423
+ }
424
+ finally {
425
+ this.mvRewriteSuppressed--;
426
+ }
427
+ }
292
428
  /**
293
429
  * Adds a new schema (e.g., for ATTACH)
294
430
  *
295
431
  * @param name Name of the schema to add
432
+ * @param kind Whether the schema is module-backed (`physical`, default) or
433
+ * design-only (`logical`). See `docs/lens.md` § Schema Kinds.
296
434
  * @returns The newly created schema
297
435
  * @throws QuereusError if the name conflicts with an existing schema
298
436
  */
299
- addSchema(name) {
437
+ addSchema(name, kind = 'physical') {
300
438
  const lowerName = name.toLowerCase();
301
439
  if (this.schemas.has(lowerName)) {
302
440
  throw new QuereusError(`Schema '${name}' already exists`, StatusCode.ERROR);
303
441
  }
304
- const schema = new Schema(lowerName);
442
+ const schema = new Schema(lowerName, kind);
305
443
  this.schemas.set(lowerName, schema);
306
- log(`Added schema '%s'`, lowerName);
444
+ // ATTACH can bring (or, via later import, an attached schema can hold) a
445
+ // cross-schema FK target; this method fires no change event, so reset directly.
446
+ // A logical-schema ATTACH likewise brings lens slots, so reset the lens gate too.
447
+ this.invalidateReverseFkIndex();
448
+ this.invalidateLensFkGate();
449
+ log(`Added schema '%s' (kind=%s)`, lowerName, kind);
450
+ return schema;
451
+ }
452
+ /**
453
+ * Returns the named schema, lazily creating an empty (physical) one if absent.
454
+ * Used by the catalog-import paths ({@link importTable}/{@link importView}) so an
455
+ * object can rehydrate into a schema that holds no tables yet — making import
456
+ * order-independent. Unlike {@link addSchema} this never throws on an existing
457
+ * schema.
458
+ */
459
+ getOrCreateSchema(name) {
460
+ const lowerName = name.toLowerCase();
461
+ let schema = this.schemas.get(lowerName);
462
+ if (!schema) {
463
+ schema = new Schema(lowerName);
464
+ this.schemas.set(lowerName, schema);
465
+ // Fires no change event; a cross-schema FK can land in/under this schema, and
466
+ // a lens slot can later resolve a basis under it — reset both derived caches.
467
+ this.invalidateReverseFkIndex();
468
+ this.invalidateLensFkGate();
469
+ }
307
470
  return schema;
308
471
  }
309
472
  /**
@@ -324,7 +487,14 @@ export class SchemaManager {
324
487
  schema.clearTables();
325
488
  schema.clearViews();
326
489
  schema.clearAssertions();
490
+ schema.clearLensSlots();
327
491
  this.schemas.delete(lowerName);
492
+ // DETACH fires no change event, yet a cross-schema FK may have keyed
493
+ // under (or referenced from) this schema; reset directly. The schema's lens
494
+ // slots (cleared above) also leave the gate stale — a basis re-attach after a
495
+ // stale-but-non-null gate could under-report — so reset it too.
496
+ this.invalidateReverseFkIndex();
497
+ this.invalidateLensFkGate();
328
498
  log(`Removed schema '%s'`, name);
329
499
  return true;
330
500
  }
@@ -416,6 +586,49 @@ export class SchemaManager {
416
586
  const schema = this.schemas.get(targetSchemaName);
417
587
  return schema?.getView(viewName);
418
588
  }
589
+ /**
590
+ * Retrieves a maintained table (a table carrying a derivation — what
591
+ * `create materialized view` produces) by name.
592
+ *
593
+ * @param schemaName The schema name ('main', etc.). Defaults to current schema
594
+ * @param name The maintained table's name
595
+ */
596
+ getMaintainedTable(schemaName, name) {
597
+ const table = this.getTable(schemaName ?? undefined, name);
598
+ return isMaintainedTable(table) ? table : undefined;
599
+ }
600
+ /**
601
+ * Returns all maintained tables (derivation-bearing tables) across all schemas.
602
+ */
603
+ getAllMaintainedTables() {
604
+ const result = [];
605
+ for (const schema of this.schemas.values()) {
606
+ for (const table of schema.getAllTables()) {
607
+ if (isMaintainedTable(table))
608
+ result.push(table);
609
+ }
610
+ }
611
+ return result;
612
+ }
613
+ /**
614
+ * Attaches (or replaces) a derivation on an already-registered table,
615
+ * swapping the registered record for `{...table, derivation}`. Fires no
616
+ * event — callers own the event discipline (create fires
617
+ * `materialized_view_added`; import stays silent). Returns the swapped
618
+ * maintained table.
619
+ *
620
+ * @throws QuereusError if the table is not registered.
621
+ */
622
+ attachDerivation(schemaName, tableName, derivation) {
623
+ const schema = this.getSchemaOrFail(schemaName);
624
+ const table = schema.getTable(tableName);
625
+ if (!table) {
626
+ throw new QuereusError(`Cannot attach derivation: table '${schemaName}.${tableName}' not found`, StatusCode.INTERNAL);
627
+ }
628
+ const maintained = { ...table, derivation };
629
+ schema.addTable(maintained);
630
+ return maintained;
631
+ }
419
632
  /**
420
633
  * Retrieves any schema item (table or view) by name. Checks views first
421
634
  *
@@ -446,6 +659,79 @@ export class SchemaManager {
446
659
  const tableSchema = this.getTable(targetSchemaName, tableName);
447
660
  return tableSchema?.tags;
448
661
  }
662
+ /**
663
+ * Freezes a whole-set tag replacement: an empty record stores `undefined`
664
+ * (so `tags IS NULL` and the differ's "no tags" both hold), a non-empty one a
665
+ * frozen copy. Shared by the three catalog-only tag setters.
666
+ */
667
+ freezeTags(tags) {
668
+ return Object.keys(tags).length > 0 ? Object.freeze({ ...tags }) : undefined;
669
+ }
670
+ /**
671
+ * Computes the next frozen tag record from the current one plus a per-key
672
+ * mutation, reusing {@link freezeTags} for the empty→`undefined` collapse.
673
+ *
674
+ * - `merge`: shallow-overlay the new keys onto the current set (overwrite on
675
+ * collision), keeping the rest. A merge of a non-empty payload can never empty
676
+ * the set; an empty merge of an empty set collapses to `undefined`.
677
+ * - `drop`: every listed key must currently be present (atomic). Any missing key
678
+ * raises `NOTFOUND` naming the offenders and mutates nothing; otherwise the keys
679
+ * are deleted and dropping the last key collapses to `undefined`. Key matching
680
+ * is verbatim (case-sensitive), matching how `parseTags` stores keys.
681
+ */
682
+ mutateTagRecord(current, mutation) {
683
+ if (mutation.op === 'merge') {
684
+ return this.freezeTags({ ...(current ?? {}), ...mutation.tags });
685
+ }
686
+ const next = { ...(current ?? {}) };
687
+ const missing = mutation.keys.filter(k => !(k in next));
688
+ if (missing.length > 0) {
689
+ throw new QuereusError(`Tag key(s) not found: ${missing.join(', ')}`, StatusCode.NOTFOUND);
690
+ }
691
+ for (const k of mutation.keys)
692
+ delete next[k];
693
+ return this.freezeTags(next);
694
+ }
695
+ /**
696
+ * Re-registers a tag-only schema swap and fires `table_modified` so optimizer
697
+ * caches invalidate. Tags are excluded from the schema hash, so a tag-only swap
698
+ * is a structural no-op except for the metadata itself.
699
+ */
700
+ commitTagUpdate(targetSchemaName, oldSchema, newSchema) {
701
+ const schema = this.getSchemaOrFail(targetSchemaName);
702
+ schema.addTable(newSchema);
703
+ this.changeNotifier.notifyChange({
704
+ type: 'table_modified',
705
+ // Stored names of the swapped object, not the raw ALTER args — see
706
+ // canonicalSchemaName for the emitter/stored-name invariant. A raw
707
+ // `targetSchemaName` here (e.g. `alter index MAIN.idx set tags`) would
708
+ // miss the table dep's stored `'main'`.
709
+ schemaName: newSchema.schemaName,
710
+ objectName: newSchema.name,
711
+ oldObject: oldSchema,
712
+ newObject: newSchema,
713
+ });
714
+ }
715
+ /**
716
+ * Shared table-tag read-modify-write: fetches the live table (NOTFOUND if
717
+ * absent), computes the next tag record from its current `tags` via `compute`,
718
+ * and commits the swap (firing `table_modified`). `compute` decides
719
+ * replace / merge / drop; it may throw before any mutation (e.g. drop-of-absent
720
+ * NOTFOUND), leaving the catalog untouched. Reads the *live* schema each call so
721
+ * back-to-back ALTERs and prepared-statement reuse see the prior result.
722
+ */
723
+ updateTableTags(tableName, compute, schemaName) {
724
+ const targetSchemaName = schemaName ?? this.getCurrentSchemaName();
725
+ const tableSchema = this.getTable(targetSchemaName, tableName);
726
+ if (!tableSchema) {
727
+ throw new QuereusError(`Table '${tableName}' not found in schema '${targetSchemaName}'`, StatusCode.NOTFOUND);
728
+ }
729
+ const updatedSchema = {
730
+ ...tableSchema,
731
+ tags: compute(tableSchema.tags),
732
+ };
733
+ this.commitTagUpdate(targetSchemaName, tableSchema, updatedSchema);
734
+ }
449
735
  /**
450
736
  * Sets metadata tags on an existing table, replacing any existing tags.
451
737
  *
@@ -454,18 +740,456 @@ export class SchemaManager {
454
740
  * @param schemaName Optional schema name (defaults to current schema)
455
741
  */
456
742
  setTableTags(tableName, tags, schemaName) {
743
+ this.updateTableTags(tableName, () => this.freezeTags(tags), schemaName);
744
+ }
745
+ /**
746
+ * Merges `tags` into an existing table's tags — set/overwrite the listed keys,
747
+ * keep the rest (the `ALTER TABLE … ADD TAGS` primitive). An empty `tags` is a
748
+ * no-op (it does NOT clear). Reads the table's live tags at call time.
749
+ */
750
+ mergeTableTags(tableName, tags, schemaName) {
751
+ this.updateTableTags(tableName, current => this.mutateTagRecord(current, { op: 'merge', tags }), schemaName);
752
+ }
753
+ /**
754
+ * Drops the listed keys from an existing table's tags (the `ALTER TABLE …
755
+ * DROP TAGS` primitive). Atomic: every key must be present, else `NOTFOUND`
756
+ * names the missing key(s) and nothing is dropped. Dropping the last key(s)
757
+ * leaves `tags` undefined. An empty `keys` is a no-op.
758
+ */
759
+ dropTableTags(tableName, keys, schemaName) {
760
+ this.updateTableTags(tableName, current => this.mutateTagRecord(current, { op: 'drop', keys }), schemaName);
761
+ }
762
+ /**
763
+ * Shared column-tag read-modify-write: resolves the table and column (NOTFOUND
764
+ * on either miss), computes the column's next tag record from its current `tags`
765
+ * via `compute`, and commits the swap. Only the column's `tags` field changes;
766
+ * nullability / type / default / PK membership are untouched. `compute` may throw
767
+ * before any mutation (drop-of-absent NOTFOUND), leaving the catalog untouched.
768
+ */
769
+ updateColumnTags(tableName, columnName, compute, schemaName) {
457
770
  const targetSchemaName = schemaName ?? this.getCurrentSchemaName();
458
771
  const tableSchema = this.getTable(targetSchemaName, tableName);
459
772
  if (!tableSchema) {
460
773
  throw new QuereusError(`Table '${tableName}' not found in schema '${targetSchemaName}'`, StatusCode.NOTFOUND);
461
774
  }
462
- const hasTags = Object.keys(tags).length > 0;
775
+ const colIndex = tableSchema.columnIndexMap.get(columnName.toLowerCase());
776
+ if (colIndex === undefined) {
777
+ throw new QuereusError(`Column '${columnName}' not found in table '${tableName}'`, StatusCode.NOTFOUND);
778
+ }
779
+ // Compute before building the new column array so a drop-of-absent NOTFOUND
780
+ // aborts before any swap.
781
+ const nextTags = compute(tableSchema.columns[colIndex].tags);
782
+ const newColumns = tableSchema.columns.map((c, i) => (i === colIndex ? { ...c, tags: nextTags } : c));
463
783
  const updatedSchema = {
464
784
  ...tableSchema,
465
- tags: hasTags ? Object.freeze({ ...tags }) : undefined,
785
+ columns: Object.freeze(newColumns),
466
786
  };
787
+ this.commitTagUpdate(targetSchemaName, tableSchema, updatedSchema);
788
+ }
789
+ /**
790
+ * Sets metadata tags on a column of an existing table, replacing any existing
791
+ * tags on that column (empty record clears). Catalog-only — only the column's
792
+ * `tags` field changes; nullability / type / default / PK membership are
793
+ * untouched.
794
+ *
795
+ * @throws QuereusError(NOTFOUND) if the table or column does not exist.
796
+ */
797
+ setColumnTags(tableName, columnName, tags, schemaName) {
798
+ this.updateColumnTags(tableName, columnName, () => this.freezeTags(tags), schemaName);
799
+ }
800
+ /**
801
+ * Merges `tags` into a column's existing tags — set/overwrite the listed keys,
802
+ * keep the rest (`ALTER TABLE … ALTER COLUMN … ADD TAGS`). Empty `tags` is a
803
+ * no-op (does NOT clear).
804
+ *
805
+ * @throws QuereusError(NOTFOUND) if the table or column does not exist.
806
+ */
807
+ mergeColumnTags(tableName, columnName, tags, schemaName) {
808
+ this.updateColumnTags(tableName, columnName, current => this.mutateTagRecord(current, { op: 'merge', tags }), schemaName);
809
+ }
810
+ /**
811
+ * Drops the listed keys from a column's tags (`ALTER TABLE … ALTER COLUMN …
812
+ * DROP TAGS`). Atomic: every key must be present, else `NOTFOUND` names the
813
+ * missing key(s) and nothing is dropped. Empty `keys` is a no-op.
814
+ *
815
+ * @throws QuereusError(NOTFOUND) if the table or column does not exist, or any
816
+ * listed key is absent.
817
+ */
818
+ dropColumnTags(tableName, columnName, keys, schemaName) {
819
+ this.updateColumnTags(tableName, columnName, current => this.mutateTagRecord(current, { op: 'drop', keys }), schemaName);
820
+ }
821
+ /**
822
+ * Shared named-constraint-tag read-modify-write: resolves the table (NOTFOUND if
823
+ * absent) and the single matching constraint class (check → unique → fk;
824
+ * NOTFOUND / ambiguous via {@link resolveNamedConstraintClass}), computes the
825
+ * matching constraint's next tag record from its current `tags` via `compute`,
826
+ * and commits. `compute` may throw before any mutation (drop-of-absent NOTFOUND);
827
+ * since it runs inside the array rebuild prior to `commitTagUpdate`, a throw
828
+ * leaves the catalog untouched.
829
+ */
830
+ updateConstraintTags(tableName, constraintName, compute, schemaName) {
831
+ const targetSchemaName = schemaName ?? this.getCurrentSchemaName();
832
+ const tableSchema = this.getTable(targetSchemaName, tableName);
833
+ if (!tableSchema) {
834
+ throw new QuereusError(`Table '${tableName}' not found in schema '${targetSchemaName}'`, StatusCode.NOTFOUND);
835
+ }
836
+ const lower = constraintName.toLowerCase();
837
+ // Resolve to exactly one class (check → unique → fk), or throw NOTFOUND/ambiguous.
838
+ const constraintClass = resolveNamedConstraintClass(tableSchema, constraintName);
839
+ const updatedSchema = { ...tableSchema };
840
+ if (constraintClass === 'check') {
841
+ updatedSchema.checkConstraints = Object.freeze(tableSchema.checkConstraints.map(c => (c.name?.toLowerCase() === lower ? { ...c, tags: compute(c.tags) } : c)));
842
+ }
843
+ else if (constraintClass === 'unique') {
844
+ updatedSchema.uniqueConstraints = Object.freeze(tableSchema.uniqueConstraints.map(c => (c.name?.toLowerCase() === lower ? { ...c, tags: compute(c.tags) } : c)));
845
+ }
846
+ else {
847
+ updatedSchema.foreignKeys = Object.freeze(tableSchema.foreignKeys.map(c => (c.name?.toLowerCase() === lower ? { ...c, tags: compute(c.tags) } : c)));
848
+ }
849
+ this.commitTagUpdate(targetSchemaName, tableSchema, updatedSchema);
850
+ }
851
+ /**
852
+ * Sets metadata tags on a NAMED table-level constraint (CHECK / UNIQUE /
853
+ * FOREIGN KEY), replacing any existing tags (empty record clears). Lookup order
854
+ * is checks → unique → foreign keys; a name present in more than one class is
855
+ * rejected as ambiguous. Unnamed constraints are not addressable.
856
+ *
857
+ * @throws QuereusError(NOTFOUND) if no named constraint matches.
858
+ * @throws QuereusError(ERROR) if the name is ambiguous across constraint classes.
859
+ */
860
+ setConstraintTags(tableName, constraintName, tags, schemaName) {
861
+ this.updateConstraintTags(tableName, constraintName, () => this.freezeTags(tags), schemaName);
862
+ }
863
+ /**
864
+ * Merges `tags` into a named constraint's existing tags — set/overwrite the
865
+ * listed keys, keep the rest (`ALTER TABLE … ALTER CONSTRAINT … ADD TAGS`).
866
+ * Empty `tags` is a no-op (does NOT clear).
867
+ *
868
+ * @throws QuereusError(NOTFOUND) if no named constraint matches.
869
+ * @throws QuereusError(ERROR) if the name is ambiguous across constraint classes.
870
+ */
871
+ mergeConstraintTags(tableName, constraintName, tags, schemaName) {
872
+ this.updateConstraintTags(tableName, constraintName, current => this.mutateTagRecord(current, { op: 'merge', tags }), schemaName);
873
+ }
874
+ /**
875
+ * Drops the listed keys from a named constraint's tags (`ALTER TABLE … ALTER
876
+ * CONSTRAINT … DROP TAGS`). Atomic: every key must be present, else `NOTFOUND`
877
+ * names the missing key(s) and nothing is dropped. Empty `keys` is a no-op.
878
+ *
879
+ * @throws QuereusError(NOTFOUND) if no named constraint matches, or any listed
880
+ * key is absent.
881
+ * @throws QuereusError(ERROR) if the name is ambiguous across constraint classes.
882
+ */
883
+ dropConstraintTags(tableName, constraintName, keys, schemaName) {
884
+ this.updateConstraintTags(tableName, constraintName, current => this.mutateTagRecord(current, { op: 'drop', keys }), schemaName);
885
+ }
886
+ /**
887
+ * Shared view-tag read-modify-write: fetches the live view (NOTFOUND if
888
+ * absent), computes its next tag record from its current `tags` via `compute`,
889
+ * re-registers the swapped {@link ViewSchema}, and fires `view_modified` so a
890
+ * cached write-through plan that recorded a `view` dependency (every
891
+ * view-/MV-mediated write does — see `buildViewMutation`) is invalidated when
892
+ * the view's tags change (tag validation re-runs at plan time, so a
893
+ * newly-invalid tag must surface on the next run). This event is distinct
894
+ * from the (non-existent) plain-view create event, so it triggers no maintenance
895
+ * re-registration. `compute` decides replace / merge / drop and may throw before
896
+ * any mutation (drop-of-absent NOTFOUND), leaving the catalog untouched.
897
+ */
898
+ updateViewTags(viewName, compute, schemaName) {
899
+ const targetSchemaName = schemaName ?? this.getCurrentSchemaName();
467
900
  const schema = this.getSchemaOrFail(targetSchemaName);
468
- schema.addTable(updatedSchema);
901
+ const view = schema.getView(viewName);
902
+ if (!view) {
903
+ throw new QuereusError(`View '${viewName}' not found in schema '${targetSchemaName}'`, StatusCode.NOTFOUND);
904
+ }
905
+ const updated = { ...view, tags: compute(view.tags) };
906
+ schema.addView(updated);
907
+ this.changeNotifier.notifyChange({
908
+ type: 'view_modified',
909
+ // Stored names of the swapped object, not the raw ALTER args — see
910
+ // canonicalSchemaName for the emitter/stored-name invariant. A
911
+ // case-differing ALTER (e.g. `alter view MAIN.MYVIEW` on
912
+ // `create view MyView` in `main`) would otherwise miss on either field.
913
+ schemaName: schema.name,
914
+ objectName: updated.name,
915
+ oldObject: view,
916
+ newObject: updated,
917
+ });
918
+ }
919
+ /**
920
+ * Sets metadata tags on an existing view, replacing any existing tags (empty
921
+ * record clears).
922
+ *
923
+ * @throws QuereusError(NOTFOUND) if the view does not exist.
924
+ */
925
+ setViewTags(viewName, tags, schemaName) {
926
+ this.updateViewTags(viewName, () => this.freezeTags(tags), schemaName);
927
+ }
928
+ /**
929
+ * Merges `tags` into an existing view's tags — set/overwrite the listed keys,
930
+ * keep the rest (`ALTER VIEW … ADD TAGS`). Empty `tags` is a no-op (does NOT
931
+ * clear). Reads the view's live tags at call time.
932
+ *
933
+ * @throws QuereusError(NOTFOUND) if the view does not exist.
934
+ */
935
+ mergeViewTags(viewName, tags, schemaName) {
936
+ this.updateViewTags(viewName, current => this.mutateTagRecord(current, { op: 'merge', tags }), schemaName);
937
+ }
938
+ /**
939
+ * Drops the listed keys from an existing view's tags (`ALTER VIEW … DROP TAGS`).
940
+ * Atomic: every key must be present, else `NOTFOUND` names the missing key(s)
941
+ * and nothing is dropped. Dropping the last key(s) leaves `tags` undefined. An
942
+ * empty `keys` is a no-op.
943
+ *
944
+ * @throws QuereusError(NOTFOUND) if the view does not exist, or any listed key
945
+ * is absent.
946
+ */
947
+ dropViewTags(viewName, keys, schemaName) {
948
+ this.updateViewTags(viewName, current => this.mutateTagRecord(current, { op: 'drop', keys }), schemaName);
949
+ }
950
+ /**
951
+ * Shared materialized-view-tag read-modify-write: fetches the live maintained
952
+ * table (NOTFOUND if absent or derivation-less), computes its next tag record
953
+ * via `compute`, re-registers the swapped table (the shared `derivation`
954
+ * object rides the spread), and fires `materialized_view_modified`. The
955
+ * table's contents and the row-time maintenance plan are untouched (tags do
956
+ * not affect maintenance), so this never re-materializes — `_modified` is
957
+ * deliberately distinct from `materialized_view_added` (what create emits):
958
+ * the MV maintenance manager re-registers on `_added` but ignores
959
+ * `_modified`. The event invalidates a cached write-through plan that
960
+ * recorded a `view` dependency when the MV's tags change (tag validation
961
+ * re-runs at plan time). `compute` may throw before any mutation
962
+ * (drop-of-absent NOTFOUND), leaving the catalog untouched.
963
+ */
964
+ updateMaterializedViewTags(name, compute, schemaName) {
965
+ const targetSchemaName = schemaName ?? this.getCurrentSchemaName();
966
+ const schema = this.getSchemaOrFail(targetSchemaName);
967
+ const table = schema.getTable(name);
968
+ if (!isMaintainedTable(table)) {
969
+ throw new QuereusError(`Materialized view '${name}' not found in schema '${targetSchemaName}'`, StatusCode.NOTFOUND);
970
+ }
971
+ const updated = { ...table, tags: compute(table.tags) };
972
+ schema.addTable(updated);
973
+ this.changeNotifier.notifyChange({
974
+ type: 'materialized_view_modified',
975
+ // Stored names of the swapped object, not the raw ALTER args — see
976
+ // canonicalSchemaName for the emitter/stored-name invariant.
977
+ schemaName: schema.name,
978
+ objectName: updated.name,
979
+ oldObject: table,
980
+ newObject: updated,
981
+ });
982
+ }
983
+ /**
984
+ * Sets metadata tags on an existing materialized view, replacing any existing
985
+ * tags (empty record clears). Catalog-only — never re-materializes.
986
+ *
987
+ * @throws QuereusError(NOTFOUND) if the materialized view does not exist.
988
+ */
989
+ setMaterializedViewTags(name, tags, schemaName) {
990
+ this.updateMaterializedViewTags(name, () => this.freezeTags(tags), schemaName);
991
+ }
992
+ /**
993
+ * Merges `tags` into an existing materialized view's tags — set/overwrite the
994
+ * listed keys, keep the rest (`ALTER MATERIALIZED VIEW … ADD TAGS`). Empty
995
+ * `tags` is a no-op (does NOT clear). Catalog-only — never re-materializes.
996
+ *
997
+ * @throws QuereusError(NOTFOUND) if the materialized view does not exist.
998
+ */
999
+ mergeMaterializedViewTags(name, tags, schemaName) {
1000
+ this.updateMaterializedViewTags(name, current => this.mutateTagRecord(current, { op: 'merge', tags }), schemaName);
1001
+ }
1002
+ /**
1003
+ * Drops the listed keys from an existing materialized view's tags (`ALTER
1004
+ * MATERIALIZED VIEW … DROP TAGS`). Atomic: every key must be present, else
1005
+ * `NOTFOUND` names the missing key(s) and nothing is dropped. Empty `keys` is a
1006
+ * no-op. Catalog-only — never re-materializes.
1007
+ *
1008
+ * @throws QuereusError(NOTFOUND) if the materialized view does not exist, or any
1009
+ * listed key is absent.
1010
+ */
1011
+ dropMaterializedViewTags(name, keys, schemaName) {
1012
+ this.updateMaterializedViewTags(name, current => this.mutateTagRecord(current, { op: 'drop', keys }), schemaName);
1013
+ }
1014
+ /**
1015
+ * Shared index-tag read-modify-write. Indexes live on their owning
1016
+ * {@link TableSchema}, so this resolves the owner by index name, computes the
1017
+ * matching {@link IndexSchema}'s next tag record from its current `tags` via
1018
+ * `compute`, swaps it, re-registers the table, and fires `table_modified`
1019
+ * (mirroring create/drop index) so optimizer caches invalidate.
1020
+ *
1021
+ * Hidden implicit covering structures (the auto-built BTree backing a UNIQUE
1022
+ * constraint, not opted into catalog visibility) are not user-addressable and
1023
+ * surface as NOTFOUND — their tags live on the originating constraint. `compute`
1024
+ * runs before the index array is rebuilt, so a drop-of-absent NOTFOUND aborts
1025
+ * before any swap.
1026
+ */
1027
+ updateIndexTags(indexName, compute, schemaName) {
1028
+ const targetSchemaName = schemaName ?? this.getCurrentSchemaName();
1029
+ const { oldSchema, newSchema } = this.resolveIndexTagSwap(targetSchemaName, indexName, compute);
1030
+ this.commitTagUpdate(targetSchemaName, oldSchema, newSchema);
1031
+ }
1032
+ /**
1033
+ * Resolve-and-swap core shared by {@link updateIndexTags} (live ALTER — commits
1034
+ * via {@link commitTagUpdate}, firing `table_modified`) and the catalog-import
1035
+ * path ({@link applyImportedIndexTags} — commits silently). Resolves `indexName`
1036
+ * within `targetSchemaName` and returns the owning table plus its swapped
1037
+ * replacement with the computed tags applied; mutates nothing itself.
1038
+ */
1039
+ resolveIndexTagSwap(targetSchemaName, indexName, compute) {
1040
+ const schema = this.getSchemaOrFail(targetSchemaName);
1041
+ const lower = indexName.toLowerCase();
1042
+ // Primary path: a materialized IndexSchema — every real index, plus the
1043
+ // memory backend's materialized implicit covering index. Tags live on the
1044
+ // matched IndexSchema. A *hidden* implicit index is not user-addressable and
1045
+ // is skipped here; it then fails the exposed-constraint fallback below
1046
+ // (its name is materialized, so it is not "exposed and unmaterialized") and
1047
+ // surfaces as NOTFOUND — preserving Phase 22/37 behavior.
1048
+ for (const table of schema.getAllTables()) {
1049
+ const matched = table.indexes?.find(idx => idx.name.toLowerCase() === lower);
1050
+ if (!matched || isHiddenImplicitIndex(table, matched.name))
1051
+ continue;
1052
+ // Compute before rebuilding the index array so a drop-of-absent NOTFOUND
1053
+ // aborts before any swap.
1054
+ const nextTags = compute(matched.tags);
1055
+ const updatedIndexes = table.indexes.map(idx => (idx.name.toLowerCase() === lower ? { ...idx, tags: nextTags } : idx));
1056
+ return { oldSchema: table, newSchema: { ...table, indexes: Object.freeze(updatedIndexes) } };
1057
+ }
1058
+ // Fallback (store mode): the exposed implicit covering index is not
1059
+ // materialized as an IndexSchema. Route its tags onto the originating UNIQUE
1060
+ // constraint's `exposedIndexTags` — kept separate from `uc.tags`, which holds
1061
+ // the exposure flag, so the flag never leaks into the surfaced index tags.
1062
+ // `findExposedImplicitConstraintIndex` returns -1 for hidden/materialized
1063
+ // implicit indexes, so they fall through to NOTFOUND.
1064
+ for (const table of schema.getAllTables()) {
1065
+ const ucIndex = findExposedImplicitConstraintIndex(table, indexName);
1066
+ if (ucIndex < 0)
1067
+ continue;
1068
+ const constraints = table.uniqueConstraints;
1069
+ // Compute before swapping so a drop-of-absent NOTFOUND aborts untouched.
1070
+ const nextTags = compute(constraints[ucIndex].exposedIndexTags);
1071
+ const updatedConstraints = constraints.map((uc, i) => {
1072
+ if (i !== ucIndex)
1073
+ return uc;
1074
+ const next = { ...uc };
1075
+ if (nextTags)
1076
+ next.exposedIndexTags = nextTags;
1077
+ else
1078
+ delete next.exposedIndexTags;
1079
+ return next;
1080
+ });
1081
+ return { oldSchema: table, newSchema: { ...table, uniqueConstraints: Object.freeze(updatedConstraints) } };
1082
+ }
1083
+ throw new QuereusError(`Index '${indexName}' not found in schema '${targetSchemaName}'`, StatusCode.NOTFOUND);
1084
+ }
1085
+ /**
1086
+ * Sets metadata tags on an existing index, replacing any existing tags (empty
1087
+ * record clears).
1088
+ *
1089
+ * @throws QuereusError(NOTFOUND) if no user-visible index matches.
1090
+ */
1091
+ setIndexTags(indexName, tags, schemaName) {
1092
+ this.updateIndexTags(indexName, () => this.freezeTags(tags), schemaName);
1093
+ }
1094
+ /**
1095
+ * Merges `tags` into an existing index's tags — set/overwrite the listed keys,
1096
+ * keep the rest (`ALTER INDEX … ADD TAGS`). Empty `tags` is a no-op (does NOT
1097
+ * clear).
1098
+ *
1099
+ * @throws QuereusError(NOTFOUND) if no user-visible index matches.
1100
+ */
1101
+ mergeIndexTags(indexName, tags, schemaName) {
1102
+ this.updateIndexTags(indexName, current => this.mutateTagRecord(current, { op: 'merge', tags }), schemaName);
1103
+ }
1104
+ /**
1105
+ * Drops the listed keys from an existing index's tags (`ALTER INDEX … DROP
1106
+ * TAGS`). Atomic: every key must be present, else `NOTFOUND` names the missing
1107
+ * key(s) and nothing is dropped. Empty `keys` is a no-op.
1108
+ *
1109
+ * @throws QuereusError(NOTFOUND) if no user-visible index matches, or any listed
1110
+ * key is absent.
1111
+ */
1112
+ dropIndexTags(indexName, keys, schemaName) {
1113
+ this.updateIndexTags(indexName, current => this.mutateTagRecord(current, { op: 'drop', keys }), schemaName);
1114
+ }
1115
+ /**
1116
+ * Nulls the reverse FK index so it rebuilds from the live catalog on next
1117
+ * access. Pure derived-cache reset — order-independent, called both from the
1118
+ * self-subscribed change listener and from the schema attach/detach methods.
1119
+ */
1120
+ invalidateReverseFkIndex() {
1121
+ this.reverseFkIndex = null;
1122
+ }
1123
+ /**
1124
+ * Builds the reverse FK index from the live catalog, bucketing every FK under
1125
+ * its resolved referenced `schema.table` key (cross-schema FKs key under their
1126
+ * `referencedSchema`). Preserves schema-insertion → table → FK-declaration
1127
+ * order within each bucket so the first-surviving-child RESTRICT pre-check and
1128
+ * any error-message golden tests keep naming the same child.
1129
+ */
1130
+ buildReverseFkIndex() {
1131
+ const index = new Map();
1132
+ for (const schema of this._getAllSchemas()) {
1133
+ for (const childTable of schema.getAllTables()) {
1134
+ if (!childTable.foreignKeys)
1135
+ continue;
1136
+ for (const fk of childTable.foreignKeys) {
1137
+ const refSchema = (fk.referencedSchema ?? childTable.schemaName).toLowerCase();
1138
+ const key = `${refSchema}.${fk.referencedTable.toLowerCase()}`;
1139
+ let bucket = index.get(key);
1140
+ if (!bucket) {
1141
+ bucket = [];
1142
+ index.set(key, bucket);
1143
+ }
1144
+ bucket.push({ childTable, fk });
1145
+ }
1146
+ }
1147
+ }
1148
+ return index;
1149
+ }
1150
+ /**
1151
+ * Returns the FKs that reference `parentSchemaName.parentTableName`
1152
+ * (case-insensitive), the shared primitive every parent-side referential scan
1153
+ * uses to short-circuit. Returns a shared frozen empty array — the O(1) gate —
1154
+ * when nothing references the table; otherwise exactly its referencing FKs.
1155
+ * Lazily (re)builds the whole index from the live catalog on the first access
1156
+ * after any schema mutation; a pure derived cache (over-reporting a since-dropped
1157
+ * FK is harmless — each consumer re-checks arity/target in its per-FK body).
1158
+ *
1159
+ * The returned `fk` objects are the same references held in
1160
+ * `childTable.foreignKeys` (identity preserved).
1161
+ */
1162
+ getReferencingForeignKeys(parentSchemaName, parentTableName) {
1163
+ if (this.reverseFkIndex === null) {
1164
+ this.reverseFkIndex = this.buildReverseFkIndex();
1165
+ }
1166
+ const key = `${parentSchemaName.toLowerCase()}.${parentTableName.toLowerCase()}`;
1167
+ return this.reverseFkIndex.get(key) ?? EMPTY_REFERENCING_FKS;
1168
+ }
1169
+ /**
1170
+ * Nulls the lens basis-FK gate so it rebuilds from the live catalog on next
1171
+ * access. Pure derived-cache reset — order-independent (rebuild happens on next
1172
+ * access, never inside a listener). Public because `lens-compiler` calls it after
1173
+ * a lens (re)deploy, which mutates the slot set without firing a `SchemaChangeEvent`.
1174
+ */
1175
+ invalidateLensFkGate() {
1176
+ this.lensFkGate = null;
1177
+ }
1178
+ /**
1179
+ * O(1) gate for the three basis-keyed lens FK paths: does `schemaName.tableName`
1180
+ * (case-insensitive) back ≥1 logical parent slot referenced by ≥1 logical FK?
1181
+ * When `false`, `executeLensForeignKeyActions`,
1182
+ * `assertLensRestrictsForParentMutation`, and `basisFksOverriddenByDivergentLensFk`
1183
+ * early-return — the reverse-map slot scan they would run finds nothing. Lazily
1184
+ * (re)builds {@link lensFkGate} on the first access after any invalidation, then
1185
+ * does a single `Set.has`. See {@link buildLensBasisFkGate} for the build, and the
1186
+ * {@link lensFkGate} doc-comment for the never-under-report soundness invariant.
1187
+ */
1188
+ basisTableBacksLogicalParentFk(schemaName, tableName) {
1189
+ if (this.lensFkGate === null) {
1190
+ this.lensFkGate = buildLensBasisFkGate(this);
1191
+ }
1192
+ return this.lensFkGate.has(`${schemaName.toLowerCase()}.${tableName.toLowerCase()}`);
469
1193
  }
470
1194
  /**
471
1195
  * Asserts that no other table has FK rows referencing the table being dropped.
@@ -477,42 +1201,34 @@ export class SchemaManager {
477
1201
  return;
478
1202
  const parentSchemaLower = parentSchemaName.toLowerCase();
479
1203
  const parentTableLower = parentTableName.toLowerCase();
480
- for (const schema of this._getAllSchemas()) {
481
- for (const childTable of schema.getAllTables()) {
482
- if (!childTable.foreignKeys)
483
- continue;
484
- // Skip the table being dropped itself — self-FK rows are going away with it.
485
- if (childTable.schemaName.toLowerCase() === parentSchemaLower &&
486
- childTable.name.toLowerCase() === parentTableLower)
487
- continue;
488
- for (const fk of childTable.foreignKeys) {
489
- if (fk.referencedTable.toLowerCase() !== parentTableLower)
490
- continue;
491
- const targetSchema = fk.referencedSchema ?? childTable.schemaName;
492
- if (targetSchema.toLowerCase() !== parentSchemaLower)
493
- continue;
494
- // MATCH SIMPLE: row is referencing iff every FK column is non-NULL.
495
- const childColNames = fk.columns.map(idx => quoteIdentifier(childTable.columns[idx].name));
496
- const whereClause = childColNames.map(c => `${c} IS NOT NULL`).join(' AND ');
497
- const schemaPrefix = childTable.schemaName.toLowerCase() !== 'main'
498
- ? `${quoteIdentifier(childTable.schemaName)}.`
499
- : '';
500
- const sql = `select 1 from ${schemaPrefix}${quoteIdentifier(childTable.name)} where ${whereClause} limit 1`;
501
- const stmt = this.db.prepare(sql);
502
- try {
503
- let referenced = false;
504
- for await (const _row of stmt._iterateRowsRaw()) {
505
- referenced = true;
506
- break;
507
- }
508
- if (referenced) {
509
- throw new QuereusError(`FOREIGN KEY constraint failed: cannot drop table '${parentTableName}' because table '${childTable.name}' still has rows referencing it`, StatusCode.CONSTRAINT);
510
- }
511
- }
512
- finally {
513
- await stmt.finalize();
514
- }
1204
+ // The reverse FK index already keyed on the referenced schema.table, so the
1205
+ // two discovery filters (referencedTable / targetSchema match) are satisfied
1206
+ // by the lookup and drop out; every other line below is unchanged.
1207
+ for (const { childTable, fk } of this.getReferencingForeignKeys(parentSchemaName, parentTableName)) {
1208
+ // Skip the table being dropped itself — self-FK rows are going away with it.
1209
+ if (childTable.schemaName.toLowerCase() === parentSchemaLower &&
1210
+ childTable.name.toLowerCase() === parentTableLower)
1211
+ continue;
1212
+ // MATCH SIMPLE: row is referencing iff every FK column is non-NULL.
1213
+ const childColNames = fk.columns.map(idx => quoteIdentifier(childTable.columns[idx].name));
1214
+ const whereClause = childColNames.map(c => `${c} IS NOT NULL`).join(' AND ');
1215
+ const schemaPrefix = childTable.schemaName.toLowerCase() !== 'main'
1216
+ ? `${quoteIdentifier(childTable.schemaName)}.`
1217
+ : '';
1218
+ const sql = `select 1 from ${schemaPrefix}${quoteIdentifier(childTable.name)} where ${whereClause} limit 1`;
1219
+ const stmt = this.db.prepare(sql);
1220
+ try {
1221
+ let referenced = false;
1222
+ for await (const _row of stmt._iterateRowsRaw()) {
1223
+ referenced = true;
1224
+ break;
515
1225
  }
1226
+ if (referenced) {
1227
+ throw new QuereusError(`FOREIGN KEY constraint failed: cannot drop table '${parentTableName}' because table '${childTable.name}' still has rows referencing it`, StatusCode.CONSTRAINT);
1228
+ }
1229
+ }
1230
+ finally {
1231
+ await stmt.finalize();
516
1232
  }
517
1233
  }
518
1234
  }
@@ -544,56 +1260,64 @@ export class SchemaManager {
544
1260
  // with the table). MATCH SIMPLE: a row is "referencing" iff every FK column
545
1261
  // is non-NULL.
546
1262
  await this.assertNoReferencingChildrenForDrop(schemaName, tableName);
547
- // Remove any active connections for this table before destroying the module.
548
- // Connections become stale once the table is dropped and must not be reused
549
- // if the table is later recreated with the same name.
550
- this.db.removeConnectionsForTable(schemaName, tableName);
551
- let destroyPromise = null;
552
- // Call destroy on the module, providing table details
1263
+ // Call destroy on the module FIRST, awaiting it and PROPAGATING any rejection,
1264
+ // BEFORE any engine-side teardown. A module may veto the drop (e.g. a
1265
+ // schema-level inbound-FK guard that an emptied child cannot satisfy); by
1266
+ // awaiting destroy before mutating connection/schema state we make the veto
1267
+ // abort the statement atomically — on rejection the table stays in our schema
1268
+ // map AND in the module's own catalogue, since neither has been touched yet.
1269
+ // Awaiting here (rather than after removeTable, as before) also preserves the
1270
+ // original "subsequent DDL/DML sees a clean slate" intent: destroy still
1271
+ // completes before dropTable returns, just without swallowing its error.
553
1272
  if (tableSchema.vtabModuleName) { // tableSchema is guaranteed to be defined here
554
1273
  const moduleRegistration = this.getModule(tableSchema.vtabModuleName);
555
1274
  if (moduleRegistration && moduleRegistration.module && moduleRegistration.module.destroy) {
556
1275
  log(`Calling destroy for VTab %s.%s via module %s`, schemaName, tableName, tableSchema.vtabModuleName);
557
- destroyPromise = moduleRegistration.module.destroy(this.db, moduleRegistration.auxData, tableSchema.vtabModuleName, schemaName, tableName).catch(err => {
558
- errorLog(`Error during VTab module destroy for %s.%s: %O`, schemaName, tableName, err);
559
- // Potentially re-throw or handle as a critical error if destroy failure is problematic
560
- });
1276
+ // Module-facing stored-name contract (see canonicalSchemaName): hand the
1277
+ // resolved table's canonical schemaName and stored display casing, never
1278
+ // the raw `drop table T` spelling — so a module keying by the arg finds the
1279
+ // create-time key, and the store's own `drop` schema-change event (which
1280
+ // emits objectName: tableName) fires the stored name.
1281
+ await moduleRegistration.module.destroy(this.db, moduleRegistration.auxData, tableSchema.vtabModuleName, tableSchema.schemaName, tableSchema.name);
1282
+ log(`destroy completed for VTab %s.%s`, schemaName, tableName);
561
1283
  }
562
1284
  else {
563
1285
  warnLog(`VTab module %s (for table %s.%s) or its destroy method not found during dropTable.`, tableSchema.vtabModuleName, schemaName, tableName);
564
1286
  }
565
1287
  }
566
- // Remove from schema map immediately
1288
+ // destroy succeeded (or the module had none) — now tear down engine-side state.
1289
+ // Remove any active connections for this table before removing it from the
1290
+ // schema map. Connections become stale once the table is dropped and must not
1291
+ // be reused if the table is later recreated with the same name.
1292
+ this.db.removeConnectionsForTable(schemaName, tableName);
1293
+ // Remove from schema map
567
1294
  const removed = schema.removeTable(tableName);
568
1295
  if (!removed && !ifExists) {
569
1296
  // This should ideally not be reached if tableSchema was found above.
570
1297
  // But as a safeguard if removeTable could fail for other reasons.
571
1298
  throw new QuereusError(`Failed to remove table ${tableName} from schema ${schemaName}, though it was initially found.`, StatusCode.INTERNAL);
572
1299
  }
573
- // Notify schema change listeners if table was removed
1300
+ // Notify schema change listeners if table was removed. Stored names of the
1301
+ // dropped object, not the raw drop args — see canonicalSchemaName for the
1302
+ // emitter/stored-name invariant.
574
1303
  if (removed) {
575
1304
  this.changeNotifier.notifyChange({
576
1305
  type: 'table_removed',
577
- schemaName: schemaName,
578
- objectName: tableName,
1306
+ schemaName: tableSchema.schemaName,
1307
+ objectName: tableSchema.name,
579
1308
  oldObject: tableSchema
580
1309
  });
581
1310
  // Emit auto schema event for modules without native event support
582
1311
  const moduleReg = tableSchema.vtabModuleName ? this.getModule(tableSchema.vtabModuleName) : undefined;
583
- if (this.db.hasSchemaListeners() && !hasNativeEventSupport(moduleReg?.module)) {
1312
+ if (this.db._needsSchemaEvents() && !hasNativeEventSupport(moduleReg?.module)) {
584
1313
  this.db._getEventEmitter().emitAutoSchemaEvent(tableSchema.vtabModuleName ?? 'memory', {
585
1314
  type: 'drop',
586
1315
  objectType: 'table',
587
- schemaName: schemaName,
588
- objectName: tableName,
1316
+ schemaName: tableSchema.schemaName,
1317
+ objectName: tableSchema.name,
589
1318
  });
590
1319
  }
591
1320
  }
592
- // Await destruction so subsequent DDL/DML sees a clean slate
593
- if (destroyPromise) {
594
- await destroyPromise;
595
- log(`destroy completed for VTab %s.%s`, schemaName, tableName);
596
- }
597
1321
  return removed; // True if removed from schema, false if not found and ifExists was true.
598
1322
  }
599
1323
  /**
@@ -618,7 +1342,12 @@ export class SchemaManager {
618
1342
  schema.clearFunctions();
619
1343
  schema.clearViews();
620
1344
  schema.clearAssertions();
1345
+ schema.clearLensSlots();
621
1346
  });
1347
+ // Wiping every table + lens slot leaves both derived FK caches stale (this
1348
+ // path fires no change event); reset so the next access rebuilds them empty.
1349
+ this.invalidateReverseFkIndex();
1350
+ this.invalidateLensFkGate();
622
1351
  log("Cleared all schemas.");
623
1352
  }
624
1353
  /**
@@ -635,6 +1364,25 @@ export class SchemaManager {
635
1364
  }
636
1365
  return schema;
637
1366
  }
1367
+ /**
1368
+ * Canonical form of a raw (statement-supplied) schema qualifier.
1369
+ *
1370
+ * The invalidation contract this anchors: stored `schemaName` on
1371
+ * tables/views/MVs is canonical, and every schema-change emitter fires the
1372
+ * *stored* names of the object it swapped. `Statement.compile()` compares
1373
+ * recorded dependencies against events exactly, so a raw-cased name on
1374
+ * either side silently misses cached-plan invalidation.
1375
+ *
1376
+ * `Schema.name` is invariantly lowercase (every construction site
1377
+ * lowercases), so an existing schema canonicalizes through its Schema
1378
+ * object; an absent one (a plan-time reference that may resolve by run
1379
+ * time, a catalog import into a not-yet-created schema) folds the way the
1380
+ * Schema constructor would. Existence is NOT validated here — lookup sites
1381
+ * keep their own missing-schema handling.
1382
+ */
1383
+ canonicalSchemaName(raw) {
1384
+ return this.schemas.get(raw.toLowerCase())?.name ?? raw.toLowerCase();
1385
+ }
638
1386
  /**
639
1387
  * Retrieves a table from the specified schema
640
1388
  *
@@ -673,19 +1421,22 @@ export class SchemaManager {
673
1421
  * Builds column schemas from AST column/constraint definitions,
674
1422
  * resolving PK membership and nullability.
675
1423
  */
676
- buildColumnSchemas(astColumns, astConstraints, defaultNotNull) {
677
- const preliminaryColumnSchemas = astColumns.map(colDef => columnDefToSchema(colDef, defaultNotNull));
678
- const { pkDef: pkDefinition, defaultConflict: pkDefaultConflict } = findPKDefinition(preliminaryColumnSchemas, astConstraints);
1424
+ buildColumnSchemas(astColumns, astConstraints, defaultNotNull, defaultCollation = 'BINARY') {
1425
+ const preliminaryColumnSchemas = astColumns.map(colDef => columnDefToSchema(colDef, defaultNotNull, defaultCollation));
1426
+ const { pkDef: pkDefinition, defaultConflict: pkDefaultConflict, synthesized } = findPKDefinition(preliminaryColumnSchemas, astConstraints);
679
1427
  const columns = preliminaryColumnSchemas.map((col, idx) => {
680
1428
  const isPkColumn = pkDefinition.some(pkCol => pkCol.index === idx);
681
1429
  const pkOrder = isPkColumn
682
1430
  ? pkDefinition.findIndex(pkC => pkC.index === idx) + 1
683
1431
  : 0;
1432
+ // Only an explicitly-declared PK forces NOT NULL. A synthesized
1433
+ // all-columns key (the no-PK fallback) leaves each column's declared
1434
+ // nullability intact — see findPKDefinition.
684
1435
  return {
685
1436
  ...col,
686
1437
  primaryKey: isPkColumn,
687
1438
  pkOrder,
688
- notNull: isPkColumn ? true : col.notNull,
1439
+ notNull: (isPkColumn && !synthesized) ? true : col.notNull,
689
1440
  };
690
1441
  });
691
1442
  return { columns, pkDefinition, pkDefaultConflict };
@@ -746,7 +1497,7 @@ export class SchemaManager {
746
1497
  name: con.name ?? `_fk_${tableName}_${colDef.name}`,
747
1498
  columns: Object.freeze([childColIndex]),
748
1499
  referencedTable: fk.table,
749
- referencedSchema: schemaName,
1500
+ referencedSchema: fk.schema ?? schemaName,
750
1501
  referencedColumns: Object.freeze([]), // resolved at enforcement time
751
1502
  referencedColumnNames: fk.columns, // deferred resolution via resolveReferencedColumns
752
1503
  onDelete: fk.onDelete ?? 'restrict',
@@ -757,32 +1508,11 @@ export class SchemaManager {
757
1508
  }
758
1509
  }
759
1510
  }
760
- // Table-level foreign keys
1511
+ // Table-level foreign keys — delegate to the shared builder so the module
1512
+ // `ADD CONSTRAINT` path and CREATE TABLE produce byte-identical schemas.
761
1513
  for (const con of astConstraints ?? []) {
762
1514
  if (con.type === 'foreignKey' && con.foreignKey && con.columns) {
763
- const fk = con.foreignKey;
764
- const childColIndices = con.columns.map(col => {
765
- const idx = columnIndexMap.get(col.name.toLowerCase());
766
- if (idx === undefined) {
767
- throw new QuereusError(`FK column '${col.name}' not found in table '${tableName}'`, StatusCode.ERROR);
768
- }
769
- return idx;
770
- });
771
- if (fk.columns && fk.columns.length !== childColIndices.length) {
772
- throw new QuereusError(`FK constraint '${con.name ?? `_fk_${tableName}_${con.columns.map(c => c.name).join('_')}`}' on table '${tableName}': child column count (${childColIndices.length}) does not match parent column count (${fk.columns.length})`, StatusCode.ERROR);
773
- }
774
- result.push({
775
- name: con.name ?? `_fk_${tableName}_${con.columns.map(c => c.name).join('_')}`,
776
- columns: Object.freeze(childColIndices),
777
- referencedTable: fk.table,
778
- referencedSchema: schemaName,
779
- referencedColumns: Object.freeze([]), // resolved at enforcement time
780
- referencedColumnNames: fk.columns, // deferred resolution via resolveReferencedColumns
781
- onDelete: fk.onDelete ?? 'restrict',
782
- onUpdate: fk.onUpdate ?? 'restrict',
783
- deferred: fk.initiallyDeferred ?? false,
784
- tags: con.tags && Object.keys(con.tags).length > 0 ? Object.freeze({ ...con.tags }) : undefined,
785
- });
1515
+ result.push(buildForeignKeyConstraintSchema(con, columnIndexMap, tableName, schemaName));
786
1516
  }
787
1517
  }
788
1518
  return result;
@@ -809,22 +1539,11 @@ export class SchemaManager {
809
1539
  }
810
1540
  }
811
1541
  }
812
- // Table-level unique constraints
1542
+ // Table-level unique constraints — delegate to the shared builder (DRY with
1543
+ // the module `ADD CONSTRAINT` path).
813
1544
  for (const con of astConstraints ?? []) {
814
1545
  if (con.type === 'unique' && con.columns && con.columns.length > 0) {
815
- const colIndices = con.columns.map(col => {
816
- const idx = columnIndexMap.get(col.name.toLowerCase());
817
- if (idx === undefined) {
818
- throw new QuereusError(`UNIQUE constraint column '${col.name}' not found`, StatusCode.ERROR);
819
- }
820
- return idx;
821
- });
822
- result.push({
823
- name: con.name,
824
- columns: Object.freeze(colIndices),
825
- defaultConflict: con.onConflict,
826
- tags: con.tags && Object.keys(con.tags).length > 0 ? Object.freeze({ ...con.tags }) : undefined,
827
- });
1546
+ result.push(buildUniqueConstraintSchema(con, columnIndexMap));
828
1547
  }
829
1548
  }
830
1549
  return result;
@@ -833,13 +1552,24 @@ export class SchemaManager {
833
1552
  * Builds a base TableSchema from an AST CREATE TABLE statement.
834
1553
  * Shared by both createTable (new storage) and importTable (existing storage).
835
1554
  */
836
- buildTableSchemaFromAST(stmt, moduleName, effectiveModuleArgs, moduleInfo) {
837
- const targetSchemaName = stmt.table.schema || this.getCurrentSchemaName();
1555
+ buildTableSchemaFromAST(stmt, moduleName, effectiveModuleArgs, moduleInfo,
1556
+ /**
1557
+ * Session `default_collation` for columns with no explicit COLLATE. The
1558
+ * caller decides: `createTable` passes the live session option (user-authored
1559
+ * CREATE), `importTable` passes `'BINARY'` (persisted DDL already made any
1560
+ * non-BINARY collation explicit, so an omitted COLLATE is canonical BINARY).
1561
+ */
1562
+ defaultCollation = 'BINARY') {
1563
+ // Stored schemaName is canonical (see canonicalSchemaName); the table name
1564
+ // keeps its declared display casing.
1565
+ const targetSchemaName = stmt.table.schema
1566
+ ? this.canonicalSchemaName(stmt.table.schema)
1567
+ : this.getCurrentSchemaName();
838
1568
  const tableName = stmt.table.name;
839
1569
  const defaultNullability = this.db.options.getStringOption('default_column_nullability');
840
1570
  const defaultNotNull = defaultNullability === 'not_null';
841
1571
  const astColumns = stmt.columns || [];
842
- const { columns, pkDefinition, pkDefaultConflict } = this.buildColumnSchemas(astColumns, stmt.constraints, defaultNotNull);
1572
+ const { columns, pkDefinition, pkDefaultConflict } = this.buildColumnSchemas(astColumns, stmt.constraints, defaultNotNull, defaultCollation);
843
1573
  const checkConstraints = this.extractCheckConstraints(astColumns, stmt.constraints);
844
1574
  const columnIndexMap = buildColumnIndexMap(columns);
845
1575
  const foreignKeys = this.extractForeignKeys(astColumns, stmt.constraints, columnIndexMap, tableName, targetSchemaName);
@@ -867,7 +1597,6 @@ export class SchemaManager {
867
1597
  checkConstraints: Object.freeze(checkConstraints),
868
1598
  foreignKeys: foreignKeys.length > 0 ? Object.freeze(foreignKeys) : undefined,
869
1599
  uniqueConstraints: uniqueConstraints.length > 0 ? Object.freeze(uniqueConstraints) : undefined,
870
- isTemporary: !!stmt.isTemporary,
871
1600
  isView: false,
872
1601
  vtabModuleName: moduleName,
873
1602
  vtabArgs: effectiveModuleArgs,
@@ -880,6 +1609,48 @@ export class SchemaManager {
880
1609
  tags: stmt.tags && Object.keys(stmt.tags).length > 0 ? Object.freeze({ ...stmt.tags }) : undefined,
881
1610
  };
882
1611
  }
1612
+ /**
1613
+ * Builds a **logical** TableSchema spec from a declared CREATE TABLE AST,
1614
+ * for use as the `logicalTable` of a lens slot (see `schema/lens.ts`).
1615
+ *
1616
+ * Reuses the same column / PK / constraint extraction as a physical table
1617
+ * (so the spec is a faithful design), but carries **no** `vtabModule`
1618
+ * (`vtabModuleName: ''`, `isLogical: true`) — a logical table is never
1619
+ * registered or executed; its compiled effective body is registered as a
1620
+ * `ViewSchema`. Module association / indexes / storage are rejected upstream
1621
+ * by the lens compiler before this is called.
1622
+ */
1623
+ buildLogicalTableSchema(stmt, schemaName) {
1624
+ const tableName = stmt.table.name;
1625
+ const defaultNullability = this.db.options.getStringOption('default_column_nullability');
1626
+ const defaultNotNull = defaultNullability === 'not_null';
1627
+ // User-authored declaration shares the CREATE surface, so honor the session default.
1628
+ const defaultCollation = normalizeCollationName(this.db.options.getStringOption('default_collation'));
1629
+ const astColumns = stmt.columns || [];
1630
+ const { columns, pkDefinition, pkDefaultConflict } = this.buildColumnSchemas(astColumns, stmt.constraints, defaultNotNull, defaultCollation);
1631
+ const checkConstraints = this.extractCheckConstraints(astColumns, stmt.constraints);
1632
+ const columnIndexMap = buildColumnIndexMap(columns);
1633
+ const foreignKeys = this.extractForeignKeys(astColumns, stmt.constraints, columnIndexMap, tableName, schemaName);
1634
+ const uniqueConstraints = this.extractUniqueConstraints(astColumns, stmt.constraints, columnIndexMap);
1635
+ return {
1636
+ name: tableName,
1637
+ schemaName,
1638
+ columns: Object.freeze(columns),
1639
+ columnIndexMap,
1640
+ primaryKeyDefinition: pkDefinition,
1641
+ primaryKeyDefaultConflict: pkDefaultConflict,
1642
+ checkConstraints: Object.freeze(checkConstraints),
1643
+ foreignKeys: foreignKeys.length > 0 ? Object.freeze(foreignKeys) : undefined,
1644
+ uniqueConstraints: uniqueConstraints.length > 0 ? Object.freeze(uniqueConstraints) : undefined,
1645
+ isView: false,
1646
+ isLogical: true,
1647
+ // Logical tables carry no module — they are a design, not storage.
1648
+ vtabModule: undefined,
1649
+ vtabModuleName: '',
1650
+ estimatedRows: 0,
1651
+ tags: stmt.tags && Object.keys(stmt.tags).length > 0 ? Object.freeze({ ...stmt.tags }) : undefined,
1652
+ };
1653
+ }
883
1654
  /**
884
1655
  * Walks an expression AST and rejects bind-parameter and (optionally)
885
1656
  * column-reference nodes. Used by DDL-time DEFAULT/CHECK validators where
@@ -890,6 +1661,15 @@ export class SchemaManager {
890
1661
  */
891
1662
  rejectIllegalReferences(expr, options) {
892
1663
  let offendingType;
1664
+ // A column reference nested inside a subquery is scoped to that subquery's own
1665
+ // FROM, not the row being inserted, so it is not an illegal sibling-row
1666
+ // reference — only top-level (depth-0) columns are. This is what lets a DEFAULT
1667
+ // author a self-referencing allocator like
1668
+ // `coalesce((select max(rid) from t), 0) + mutation_ordinal()` (the
1669
+ // shared-key-via-default surrogate recipe — docs/view-updateability.md
1670
+ // § Mutation Context). Parameters stay rejected at any depth.
1671
+ let subqueryDepth = 0;
1672
+ const isQueryBoundary = (node) => node.type === 'select' || node.type === 'subquery' || node.type === 'exists';
893
1673
  traverseAst(expr, {
894
1674
  enterNode: (node) => {
895
1675
  if (offendingType)
@@ -898,10 +1678,22 @@ export class SchemaManager {
898
1678
  offendingType = 'parameter';
899
1679
  return false;
900
1680
  }
901
- if (options.rejectColumns && node.type === 'column') {
902
- offendingType = 'column';
903
- return false;
1681
+ if (options.rejectColumns && node.type === 'column' && subqueryDepth === 0) {
1682
+ // `new.<column>` is an explicit, legal read of a value the INSERT
1683
+ // supplies for a sibling column (resolved against the row scope at
1684
+ // INSERT time); only a bare (unqualified) column is the illegal
1685
+ // sibling reference rejected here.
1686
+ if (node.table?.toLowerCase() !== 'new') {
1687
+ offendingType = 'column';
1688
+ return false;
1689
+ }
904
1690
  }
1691
+ if (isQueryBoundary(node))
1692
+ subqueryDepth += 1;
1693
+ },
1694
+ exitNode: (node) => {
1695
+ if (isQueryBoundary(node))
1696
+ subqueryDepth -= 1;
905
1697
  },
906
1698
  });
907
1699
  if (offendingType === 'parameter') {
@@ -925,10 +1717,45 @@ export class SchemaManager {
925
1717
  * context variable, and the build attempt is permitted to fail —
926
1718
  * scope resolution is deferred to row-time).
927
1719
  */
928
- validateDefaultDeterminism(columns, tableName, hasMutationContext) {
1720
+ /** True when a DEFAULT expression embeds a subquery (scalar subquery / EXISTS / SELECT). */
1721
+ defaultEmbedsSubquery(expr) {
1722
+ let found = false;
1723
+ traverseAst(expr, {
1724
+ enterNode: (node) => {
1725
+ if (node.type === 'select' || node.type === 'subquery' || node.type === 'exists') {
1726
+ found = true;
1727
+ return false;
1728
+ }
1729
+ },
1730
+ });
1731
+ return found;
1732
+ }
1733
+ /** True when a DEFAULT expression reads the row being written via `new.<column>`. */
1734
+ defaultReferencesNewRow(expr) {
1735
+ let found = false;
1736
+ traverseAst(expr, {
1737
+ enterNode: (node) => {
1738
+ if (found)
1739
+ return false;
1740
+ if (node.type === 'column' && node.table?.toLowerCase() === 'new') {
1741
+ found = true;
1742
+ return false;
1743
+ }
1744
+ },
1745
+ });
1746
+ return found;
1747
+ }
1748
+ /**
1749
+ * Build the throwaway planning context (global + parameter scope, no table/row
1750
+ * scope) used to compile a DEFAULT expression for DDL-time validation. The table's
1751
+ * columns are intentionally absent so a bare-column reference fails to build —
1752
+ * which the bare-column pre-walk has already rejected for the strict case, and
1753
+ * which the deferral path tolerates for `new.`/subquery/mutation-context defaults.
1754
+ */
1755
+ makeDdlValidationContext() {
929
1756
  const globalScope = new GlobalScope(this.db.schemaManager);
930
1757
  const parameterScope = new ParameterScope(globalScope);
931
- const planningCtx = {
1758
+ return {
932
1759
  db: this.db,
933
1760
  schemaManager: this.db.schemaManager,
934
1761
  parameters: {},
@@ -939,43 +1766,104 @@ export class SchemaManager {
939
1766
  cteReferenceCache: new Map(),
940
1767
  outputScopes: new Map()
941
1768
  };
942
- for (const col of columns) {
943
- if (!col.defaultValue || typeof col.defaultValue !== 'object' || col.defaultValue === null || !('type' in col.defaultValue)) {
944
- continue;
945
- }
946
- this.rejectIllegalReferences(col.defaultValue, {
947
- rejectColumns: !hasMutationContext,
948
- formatParamError: () => `DEFAULT for column '${col.name}' in table '${tableName}' may not reference bind parameters.`,
949
- formatColumnError: () => `DEFAULT for column '${col.name}' in table '${tableName}' may not reference columns; use a generated column instead.`,
950
- });
951
- let defaultExpr;
952
- try {
953
- defaultExpr = buildExpression(planningCtx, col.defaultValue);
1769
+ }
1770
+ /**
1771
+ * Validate a single DEFAULT expression — the per-default core of
1772
+ * {@link validateDefaultDeterminism}, factored out so the ALTER COLUMN SET DEFAULT
1773
+ * path ({@link validateAlterColumnDefault}) routes through the identical checks:
1774
+ * bind parameters and (absent a mutation context) bare columns rejected up front,
1775
+ * non-determinism rejected unless `allowNonDeterministic`, and a `new.<column>` /
1776
+ * subquery / mutation-context default deferred to INSERT time (it cannot build
1777
+ * here without the row/table scope; determinism is re-checked when the row scope
1778
+ * is established).
1779
+ */
1780
+ validateOneDefault(planningCtx, defaultValue, columnName, tableName, hasMutationContext, allowNonDeterministic, ddlPhase) {
1781
+ this.rejectIllegalReferences(defaultValue, {
1782
+ rejectColumns: !hasMutationContext,
1783
+ formatParamError: () => `DEFAULT for column '${columnName}' in table '${tableName}' may not reference bind parameters.`,
1784
+ formatColumnError: () => `DEFAULT for column '${columnName}' in table '${tableName}' may not reference a bare column; use 'new.<column>' to read a value supplied by the INSERT, or a generated column instead.`,
1785
+ });
1786
+ let defaultExpr;
1787
+ // A DEFAULT that embeds a subquery may forward-reference the table being
1788
+ // created (a self-referencing allocator — `select max(rid) from t` on `t`):
1789
+ // the table is not yet registered here, so the build legitimately fails.
1790
+ // A DEFAULT that reads `new.<column>` resolves only against the row scope
1791
+ // established at INSERT time, so it likewise cannot build here. Either way
1792
+ // determinism is re-checked at INSERT time (both the single-source insert
1793
+ // expansion and the shared-key envelope re-validate the compiled default),
1794
+ // so defer rather than reject. Other build failures (a typo'd function /
1795
+ // bare column) stay strict.
1796
+ const defaultEmbedsSubquery = this.defaultEmbedsSubquery(defaultValue);
1797
+ const defaultReferencesNewRow = this.defaultReferencesNewRow(defaultValue);
1798
+ try {
1799
+ defaultExpr = buildExpression(planningCtx, defaultValue);
1800
+ }
1801
+ catch (e) {
1802
+ if (hasMutationContext || defaultEmbedsSubquery || defaultReferencesNewRow) {
1803
+ // Column-style identifiers in DEFAULT may resolve to mutation
1804
+ // context variables at INSERT time; a subquery may forward-reference
1805
+ // the table being created; `new.<column>` resolves against the INSERT
1806
+ // row scope. The row/table scope isn't available here, so a build
1807
+ // failure isn't necessarily a bug. Determinism is re-checked at
1808
+ // INSERT time.
1809
+ log('Skipping determinism validation for default on column %s.%s at %s time (deferred to INSERT%s): %s', tableName, columnName, ddlPhase, hasMutationContext ? ', mutation context present' : defaultEmbedsSubquery ? ', embeds subquery' : ', references new row', e.message);
954
1810
  }
955
- catch (e) {
956
- if (hasMutationContext) {
957
- // Column-style identifiers in DEFAULT may resolve to mutation
958
- // context variables at INSERT time; the row scope isn't
959
- // available here, so a build failure isn't necessarily a bug.
960
- // Determinism is re-checked at INSERT time.
961
- log('Skipping determinism validation for default on column %s.%s at CREATE TABLE time (deferred to INSERT, mutation context present): %s', tableName, col.name, e.message);
962
- }
963
- else {
964
- const message = e instanceof Error ? e.message : String(e);
965
- const code = e instanceof QuereusError ? e.code : StatusCode.ERROR;
966
- throw new QuereusError(`DEFAULT for column '${col.name}' in table '${tableName}' is invalid: ${message}`, code, e instanceof Error ? e : undefined);
967
- }
1811
+ else {
1812
+ const message = e instanceof Error ? e.message : String(e);
1813
+ const code = e instanceof QuereusError ? e.code : StatusCode.ERROR;
1814
+ throw new QuereusError(`DEFAULT for column '${columnName}' in table '${tableName}' is invalid: ${message}`, code, e instanceof Error ? e : undefined);
968
1815
  }
969
- if (!defaultExpr)
1816
+ }
1817
+ if (!defaultExpr)
1818
+ return;
1819
+ if (allowNonDeterministic)
1820
+ return;
1821
+ const result = checkDeterministic(defaultExpr);
1822
+ if (!result.valid) {
1823
+ throw new QuereusError(`Non-deterministic expression not allowed in DEFAULT for column '${columnName}' in table '${tableName}'. ` +
1824
+ `Expression: ${result.expression}. ` +
1825
+ `Use mutation context to pass non-deterministic values (e.g., WITH CONTEXT (timestamp = datetime('now'))).`, StatusCode.ERROR);
1826
+ }
1827
+ }
1828
+ validateDefaultDeterminism(columns, tableName, hasMutationContext, allowNonDeterministic = false) {
1829
+ const planningCtx = this.makeDdlValidationContext();
1830
+ for (const col of columns) {
1831
+ if (!col.defaultValue || typeof col.defaultValue !== 'object' || col.defaultValue === null || !('type' in col.defaultValue)) {
970
1832
  continue;
971
- const result = checkDeterministic(defaultExpr);
972
- if (!result.valid) {
973
- throw new QuereusError(`Non-deterministic expression not allowed in DEFAULT for column '${col.name}' in table '${tableName}'. ` +
974
- `Expression: ${result.expression}. ` +
975
- `Use mutation context to pass non-deterministic values (e.g., WITH CONTEXT (timestamp = datetime('now'))).`, StatusCode.ERROR);
976
1833
  }
1834
+ this.validateOneDefault(planningCtx, col.defaultValue, col.name, tableName, hasMutationContext, allowNonDeterministic, 'CREATE TABLE');
977
1835
  }
978
1836
  }
1837
+ /**
1838
+ * Validate a DEFAULT expression supplied by an `ALTER COLUMN … SET DEFAULT`,
1839
+ * routing it through the same checks CREATE TABLE applies so the stored default is
1840
+ * consistent with what INSERT will accept: bind parameters and (absent a mutation
1841
+ * context) bare columns are rejected, non-determinism is rejected unless the
1842
+ * `nondeterministic_schema` option is set, and a `new.<column>` default is accepted
1843
+ * with the build/determinism check deferred to INSERT time. DROP DEFAULT (a null
1844
+ * expression) never reaches here. Called from the ALTER TABLE runtime emitter.
1845
+ */
1846
+ validateAlterColumnDefault(defaultExpr, columnName, tableName, hasMutationContext) {
1847
+ this.validateDdlDefault(defaultExpr, columnName, tableName, hasMutationContext, 'ALTER COLUMN SET DEFAULT');
1848
+ }
1849
+ /**
1850
+ * Validate a DEFAULT supplied by `ALTER TABLE ADD COLUMN`, routing it through the
1851
+ * same checks CREATE TABLE / ALTER COLUMN apply so the stored default is consistent
1852
+ * with what INSERT (and the per-row backfill) will accept: bind parameters and
1853
+ * (absent a mutation context) bare columns are rejected, non-determinism is rejected
1854
+ * unless `nondeterministic_schema` is set, and a `new.<column>` default is accepted
1855
+ * with its build deferred — it reads the existing row's sibling during backfill and
1856
+ * the INSERT-supplied sibling for future inserts. Called from the ALTER TABLE
1857
+ * statement builder (`buildAlterTableStmt`) at plan-build time.
1858
+ */
1859
+ validateAddColumnDefault(defaultExpr, columnName, tableName, hasMutationContext) {
1860
+ this.validateDdlDefault(defaultExpr, columnName, tableName, hasMutationContext, 'ALTER TABLE ADD COLUMN');
1861
+ }
1862
+ /** Shared body for the ALTER-time DEFAULT validators (ALTER COLUMN SET DEFAULT / ADD COLUMN). */
1863
+ validateDdlDefault(defaultExpr, columnName, tableName, hasMutationContext, ddlPhase) {
1864
+ const allowNonDet = this.db.options.getBooleanOption('nondeterministic_schema');
1865
+ this.validateOneDefault(this.makeDdlValidationContext(), defaultExpr, columnName, tableName, hasMutationContext, allowNonDet, ddlPhase);
1866
+ }
979
1867
  /**
980
1868
  * Validates that CHECK constraint expressions don't call non-deterministic
981
1869
  * functions and don't reference bind parameters. Walks the AST and looks
@@ -984,13 +1872,15 @@ export class SchemaManager {
984
1872
  * CHECK expressions reference table columns whose scope is not yet
985
1873
  * established at CREATE TABLE time.
986
1874
  */
987
- validateCheckConstraintDeterminism(checkConstraints, tableName) {
1875
+ validateCheckConstraintDeterminism(checkConstraints, tableName, allowNonDeterministic = false) {
988
1876
  for (const cc of checkConstraints) {
989
1877
  const constraintName = cc.name ?? `_check_${tableName}`;
990
1878
  this.rejectIllegalReferences(cc.expr, {
991
1879
  rejectColumns: false,
992
1880
  formatParamError: () => `CHECK constraint '${constraintName}' on table '${tableName}' may not reference bind parameters.`,
993
1881
  });
1882
+ if (allowNonDeterministic)
1883
+ continue;
994
1884
  let offendingExpr;
995
1885
  traverseAst(cc.expr, {
996
1886
  enterNode: (node) => {
@@ -1057,7 +1947,8 @@ export class SchemaManager {
1057
1947
  if (!tableSchema) {
1058
1948
  throw new QuereusError(`no such table: ${tableName}`, StatusCode.ERROR, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
1059
1949
  }
1060
- if (!tableSchema.vtabModule.createIndex) {
1950
+ const vtabModule = requireVtabModule(tableSchema);
1951
+ if (!vtabModule.createIndex) {
1061
1952
  throw new QuereusError(`Virtual table module '${tableSchema.vtabModuleName}' for table '${tableName}' does not support CREATE INDEX.`, StatusCode.ERROR, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
1062
1953
  }
1063
1954
  const existingIndex = tableSchema.indexes?.find(idx => idx.name.toLowerCase() === indexName.toLowerCase());
@@ -1070,27 +1961,36 @@ export class SchemaManager {
1070
1961
  }
1071
1962
  const indexSchema = this.buildIndexSchema(stmt, tableSchema, tableName, indexName);
1072
1963
  try {
1073
- await tableSchema.vtabModule.createIndex(this.db, targetSchemaName, tableName, indexSchema);
1964
+ // Module-facing stored-name contract (see canonicalSchemaName): hand the
1965
+ // module the resolved table's canonical schemaName and stored display
1966
+ // casing, never the raw `create index … on T` spelling — a module may key
1967
+ // storage/registries by these args verbatim. `indexSchema.name` is the
1968
+ // *new* index's own name (the future stored name), left as-spelled.
1969
+ await vtabModule.createIndex(this.db, tableSchema.schemaName, tableSchema.name, indexSchema);
1074
1970
  }
1075
1971
  catch (e) {
1076
1972
  const message = e instanceof Error ? e.message : String(e);
1077
1973
  const code = e instanceof QuereusError ? e.code : StatusCode.ERROR;
1078
1974
  throw new QuereusError(`createIndex failed for index '${indexName}' on table '${tableName}': ${message}`, code, e instanceof Error ? e : undefined, stmt.loc?.start.line, stmt.loc?.start.column);
1079
1975
  }
1080
- const updatedTableSchema = this.addIndexToTableSchema(tableSchema, indexSchema);
1976
+ const updatedTableSchema = appendIndexToTableSchema(tableSchema, indexSchema);
1081
1977
  const schema = this.getSchemaOrFail(targetSchemaName);
1082
1978
  schema.addTable(updatedTableSchema);
1083
1979
  this.changeNotifier.notifyChange({
1084
1980
  type: 'table_modified',
1085
- schemaName: targetSchemaName,
1086
- objectName: tableName,
1981
+ // Stored names of the swapped table, not the raw statement spelling — see
1982
+ // canonicalSchemaName for the emitter/stored-name invariant. A raw
1983
+ // `create index … on T` (stored `t`) or an unqualified CREATE INDEX against
1984
+ // a `MAIN.`-created table would otherwise miss the cached plan's table dep.
1985
+ schemaName: updatedTableSchema.schemaName,
1986
+ objectName: updatedTableSchema.name,
1087
1987
  oldObject: tableSchema,
1088
1988
  newObject: updatedTableSchema
1089
1989
  });
1090
1990
  this.emitAutoSchemaEventIfNeeded(tableSchema.vtabModuleName, {
1091
1991
  type: 'create',
1092
1992
  objectType: 'index',
1093
- schemaName: targetSchemaName,
1993
+ schemaName: updatedTableSchema.schemaName,
1094
1994
  objectName: indexName,
1095
1995
  });
1096
1996
  log(`Successfully created index %s on table %s.%s`, indexName, targetSchemaName, tableName);
@@ -1100,12 +2000,13 @@ export class SchemaManager {
1100
2000
  */
1101
2001
  buildIndexSchema(stmt, tableSchema, tableName, indexName) {
1102
2002
  const indexColumns = stmt.columns.map((indexedCol) => {
1103
- if (indexedCol.expr) {
1104
- throw new QuereusError(`Indices on expressions are not supported yet.`, StatusCode.ERROR, undefined, indexedCol.expr.loc?.start.line, indexedCol.expr.loc?.start.column);
1105
- }
1106
- const colName = indexedCol.name;
2003
+ // The parser folds `col COLLATE x` into a collate expression over a bare
2004
+ // column reference; resolveImportedIndexColumn unwraps that form to a
2005
+ // { name, collation } pair, mirroring importIndex. A genuine expression
2006
+ // index (non-column operand) resolves to an unset name and is rejected.
2007
+ const { name: colName, collation } = resolveImportedIndexColumn(indexedCol);
1107
2008
  if (!colName) {
1108
- throw new QuereusError(`Indexed column must be a simple column name.`, StatusCode.ERROR);
2009
+ throw new QuereusError(`Indices on expressions are not supported yet.`, StatusCode.ERROR, undefined, indexedCol.expr?.loc?.start.line, indexedCol.expr?.loc?.start.column);
1109
2010
  }
1110
2011
  const tableColIndex = tableSchema.columnIndexMap.get(colName.toLowerCase());
1111
2012
  if (tableColIndex === undefined) {
@@ -1115,7 +2016,7 @@ export class SchemaManager {
1115
2016
  return {
1116
2017
  index: tableColIndex,
1117
2018
  desc: indexedCol.direction === 'desc',
1118
- collation: indexedCol.collation || tableColSchema.collation
2019
+ collation: normalizeCollationName(collation || tableColSchema.collation || 'BINARY')
1119
2020
  };
1120
2021
  });
1121
2022
  return {
@@ -1126,29 +2027,6 @@ export class SchemaManager {
1126
2027
  tags: stmt.tags && Object.keys(stmt.tags).length > 0 ? Object.freeze({ ...stmt.tags }) : undefined,
1127
2028
  };
1128
2029
  }
1129
- /**
1130
- * Returns a new TableSchema with the given index appended. If the index is
1131
- * unique, also adds a matching uniqueConstraint so the mutation manager
1132
- * enforces uniqueness on insert/update through its existing checks.
1133
- */
1134
- addIndexToTableSchema(tableSchema, indexSchema) {
1135
- const updatedIndexes = [...(tableSchema.indexes || []), indexSchema];
1136
- const result = {
1137
- ...tableSchema,
1138
- indexes: Object.freeze(updatedIndexes),
1139
- };
1140
- if (indexSchema.unique) {
1141
- const newConstraint = {
1142
- name: indexSchema.name,
1143
- columns: Object.freeze(indexSchema.columns.map(c => c.index)),
1144
- predicate: indexSchema.predicate,
1145
- derivedFromIndex: indexSchema.name,
1146
- };
1147
- const updatedConstraints = [...(tableSchema.uniqueConstraints ?? []), newConstraint];
1148
- result.uniqueConstraints = Object.freeze(updatedConstraints);
1149
- }
1150
- return result;
1151
- }
1152
2030
  /**
1153
2031
  * Drops a secondary index from the table that owns it.
1154
2032
  * Searches all tables in the target schema to find the owning table.
@@ -1180,11 +2058,18 @@ export class SchemaManager {
1180
2058
  }
1181
2059
  throw new QuereusError(`no such index: ${indexName}`, StatusCode.ERROR);
1182
2060
  }
2061
+ // Stored display casing of the dropped index — the raw `indexName` arg may
2062
+ // differ in case. Computed *before* the module call so module.dropIndex
2063
+ // receives the stored name (the module-facing stored-name contract; see
2064
+ // canonicalSchemaName), keeping a case-divergent `DROP INDEX iDx` from
2065
+ // missing a module handle/registry keyed by the stored `idx` (e.g. the
2066
+ // store's StoreTable.indexStores cache). Also reused by the events below.
2067
+ const storedIndexName = ownerTable.indexes.find(idx => idx.name.toLowerCase() === lowerIndexName).name;
1183
2068
  // Call module.dropIndex if the module supports it
1184
2069
  const moduleReg = ownerTable.vtabModuleName ? this.getModule(ownerTable.vtabModuleName) : undefined;
1185
2070
  if (moduleReg?.module?.dropIndex) {
1186
2071
  try {
1187
- await moduleReg.module.dropIndex(this.db, schemaName, ownerTable.name, indexName);
2072
+ await moduleReg.module.dropIndex(this.db, ownerTable.schemaName, ownerTable.name, storedIndexName);
1188
2073
  }
1189
2074
  catch (e) {
1190
2075
  const message = e instanceof Error ? e.message : String(e);
@@ -1193,7 +2078,7 @@ export class SchemaManager {
1193
2078
  }
1194
2079
  }
1195
2080
  // Remove the index from the table schema, along with any uniqueConstraint
1196
- // that was synthesized from this index (see addIndexToTableSchema).
2081
+ // that was synthesized from this index (see appendIndexToTableSchema).
1197
2082
  const updatedIndexes = (ownerTable.indexes || []).filter(idx => idx.name.toLowerCase() !== lowerIndexName);
1198
2083
  const updatedUniqueConstraints = (ownerTable.uniqueConstraints ?? []).filter(uc => uc.derivedFromIndex?.toLowerCase() !== lowerIndexName);
1199
2084
  const updatedTableSchema = {
@@ -1206,7 +2091,9 @@ export class SchemaManager {
1206
2091
  schema.addTable(updatedTableSchema);
1207
2092
  this.changeNotifier.notifyChange({
1208
2093
  type: 'table_modified',
1209
- schemaName,
2094
+ // Stored names of the swapped table, not the raw drop args — see
2095
+ // canonicalSchemaName for the emitter/stored-name invariant.
2096
+ schemaName: ownerTable.schemaName,
1210
2097
  objectName: ownerTable.name,
1211
2098
  oldObject: ownerTable,
1212
2099
  newObject: updatedTableSchema
@@ -1214,18 +2101,19 @@ export class SchemaManager {
1214
2101
  this.emitAutoSchemaEventIfNeeded(ownerTable.vtabModuleName, {
1215
2102
  type: 'drop',
1216
2103
  objectType: 'index',
1217
- schemaName,
1218
- objectName: indexName,
2104
+ schemaName: ownerTable.schemaName,
2105
+ objectName: storedIndexName,
1219
2106
  });
1220
2107
  log(`Successfully dropped index %s from table %s.%s`, indexName, schemaName, ownerTable.name);
1221
2108
  }
1222
2109
  /**
1223
2110
  * Emits an auto schema event for modules that don't have native event support,
1224
- * if any schema listeners are registered.
2111
+ * if the engine needs schema events i.e. any `onSchemaChange` or
2112
+ * `onTransactionCommit` listener is registered (see `Database._needsSchemaEvents`).
1225
2113
  */
1226
2114
  emitAutoSchemaEventIfNeeded(moduleName, event) {
1227
2115
  const moduleReg = moduleName ? this.getModule(moduleName) : undefined;
1228
- if (this.db.hasSchemaListeners() && !hasNativeEventSupport(moduleReg?.module)) {
2116
+ if (this.db._needsSchemaEvents() && !hasNativeEventSupport(moduleReg?.module)) {
1229
2117
  this.db._getEventEmitter().emitAutoSchemaEvent(moduleName ?? 'memory', event);
1230
2118
  }
1231
2119
  }
@@ -1237,7 +2125,36 @@ export class SchemaManager {
1237
2125
  * @returns A Promise that resolves to the created TableSchema.
1238
2126
  * @throws QuereusError on errors (e.g., module not found, create fails, table exists).
1239
2127
  */
1240
- async createTable(stmt) {
2128
+ /**
2129
+ * Builds the {@link TableSchema} a CREATE TABLE statement WOULD register,
2130
+ * without touching the module or the catalog. Resolution matches
2131
+ * {@link createTable} exactly (same module-info resolution, same live-session
2132
+ * default collation), so the result is byte-for-byte what registration would
2133
+ * produce. Used by the maintained-table create path
2134
+ * (`createMaintainedTable`) to verify the declared shape against the
2135
+ * derivation body BEFORE any catalog registration — the all-or-nothing
2136
+ * posture of `create table … maintained as`.
2137
+ */
2138
+ buildDeclaredTableSchema(stmt) {
2139
+ const { moduleName, effectiveModuleArgs, moduleInfo } = this.resolveModuleInfo(stmt);
2140
+ const defaultCollation = normalizeCollationName(this.db.options.getStringOption('default_collation'));
2141
+ return this.buildTableSchemaFromAST(stmt, moduleName, effectiveModuleArgs, moduleInfo, defaultCollation);
2142
+ }
2143
+ /**
2144
+ * `preferBacking` routes the module instantiation through the durable backing
2145
+ * seam — `createBacking?() ?? create()`, the SAME preference
2146
+ * {@link createBackingTable} uses — instead of `module.create` directly. It is
2147
+ * set ONLY by the maintained-table create path (`createMaintainedTable`), so a
2148
+ * durable-backing module (e.g. lamina) builds the basis `RowStore` that
2149
+ * `getBackingHost` later resolves for row-time maintenance; without it the table
2150
+ * is an ordinary relational collection with no basis store, and the maintained
2151
+ * fill throws `backing host not found`. An ordinary user CREATE leaves it false
2152
+ * and stays byte-for-byte on `module.create`. A module without `createBacking`
2153
+ * (e.g. memory) falls through to `create` regardless, so the flag is a no-op
2154
+ * there. Every gate above (determinism, FK-collation) runs identically in both
2155
+ * modes — the flag only selects the factory method.
2156
+ */
2157
+ async createTable(stmt, preferBacking = false) {
1241
2158
  const targetSchemaName = stmt.table.schema || this.getCurrentSchemaName();
1242
2159
  const tableName = stmt.table.name;
1243
2160
  const schema = this.getSchema(targetSchemaName);
@@ -1265,13 +2182,32 @@ export class SchemaManager {
1265
2182
  throw new QuereusError(`${itemType} ${targetSchemaName}.${tableName} already exists`, StatusCode.CONSTRAINT, undefined, stmt.table.loc?.start.line, stmt.table.loc?.start.column);
1266
2183
  }
1267
2184
  const { moduleName, effectiveModuleArgs, moduleInfo } = this.resolveModuleInfo(stmt);
1268
- const baseTableSchema = this.buildTableSchemaFromAST(stmt, moduleName, effectiveModuleArgs, moduleInfo);
2185
+ // User-authored CREATE: omitted-COLLATE columns resolve under the live session default.
2186
+ const defaultCollation = normalizeCollationName(this.db.options.getStringOption('default_collation'));
2187
+ const baseTableSchema = this.buildTableSchemaFromAST(stmt, moduleName, effectiveModuleArgs, moduleInfo, defaultCollation);
1269
2188
  const hasMutationContext = !!baseTableSchema.mutationContext && baseTableSchema.mutationContext.length > 0;
1270
- this.validateDefaultDeterminism(baseTableSchema.columns, tableName, hasMutationContext);
1271
- this.validateCheckConstraintDeterminism(baseTableSchema.checkConstraints, tableName);
2189
+ // `nondeterministic_schema = true` lifts the strict-rejection gate at CREATE TABLE.
2190
+ // The captured artifact at the vtab.update() frontier is fully resolved per row, so
2191
+ // defaults / CHECKs / generated columns containing non-determinism remain replay-safe
2192
+ // (see docs/architecture.md § Constraints and docs/module-authoring.md § Mutation Statements).
2193
+ // The bind-parameter / column-reference pre-walks inside the validators still run
2194
+ // in both modes — those are scope checks, not determinism checks.
2195
+ const allowNonDet = this.db.options.getBooleanOption('nondeterministic_schema');
2196
+ this.validateDefaultDeterminism(baseTableSchema.columns, tableName, hasMutationContext, allowNonDet);
2197
+ this.validateCheckConstraintDeterminism(baseTableSchema.checkConstraints, tableName, allowNonDet);
1272
2198
  let tableInstance;
1273
2199
  try {
1274
- tableInstance = await moduleInfo.module.create(this.db, baseTableSchema);
2200
+ // `preferBacking` (set only by the maintained-table create path) routes
2201
+ // through the durable backing seam — the SAME `createBacking?() ?? create()`
2202
+ // preference {@link createBackingTable} uses — so a durable-backing module
2203
+ // builds the basis `RowStore` that `getBackingHost` later resolves for
2204
+ // row-time maintenance. An ordinary user CREATE leaves it false and stays
2205
+ // byte-for-byte on `module.create`; a module without `createBacking` (memory)
2206
+ // falls through to `create` regardless.
2207
+ const create = preferBacking
2208
+ ? (moduleInfo.module.createBacking?.bind(moduleInfo.module) ?? moduleInfo.module.create.bind(moduleInfo.module))
2209
+ : moduleInfo.module.create.bind(moduleInfo.module);
2210
+ tableInstance = await create(this.db, baseTableSchema);
1275
2211
  }
1276
2212
  catch (e) {
1277
2213
  const message = e instanceof Error ? e.message : String(e);
@@ -1279,8 +2215,81 @@ export class SchemaManager {
1279
2215
  throw new QuereusError(`Module '${moduleName}' create failed for table '${tableName}': ${message}`, code, e instanceof Error ? e : undefined, stmt.loc?.start.line, stmt.loc?.start.column);
1280
2216
  }
1281
2217
  const completeTableSchema = this.finalizeCreatedTableSchema(tableInstance, tableName, targetSchemaName, moduleName, effectiveModuleArgs, moduleInfo);
2218
+ // Reject a FK whose child/parent column collations declare a same-rank conflict
2219
+ // — the same conflict FK enforcement raises at first DML, surfaced here at CREATE.
2220
+ // `completeTableSchema` is post-reconcile (the store's `reconcilePkCollations` ran
2221
+ // inside `module.create`), so an implicit-default text PK reconciled to the store's
2222
+ // NOCASE keeps `collationExplicit` unset → contributes the engine floor, never a
2223
+ // false conflict. Done BEFORE `addTable` so a conflicting FK leaves the catalog
2224
+ // clean (a self-ref FK resolves against `completeTableSchema` itself). Deliberately
2225
+ // only here — never in `buildTableSchemaFromAST` or the import/rehydrate path, so a
2226
+ // legacy persisted conflicting FK reloads fine and still surfaces at DML.
2227
+ for (const fk of completeTableSchema.foreignKeys ?? []) {
2228
+ validateForeignKeyCollations(this.db, completeTableSchema, fk);
2229
+ }
1282
2230
  schema.addTable(completeTableSchema);
1283
2231
  log(`Successfully created table %s.%s using module %s`, targetSchemaName, tableName, moduleName);
2232
+ this.changeNotifier.notifyChange({
2233
+ type: 'table_added',
2234
+ // Stored names of the registered table — see canonicalSchemaName for the
2235
+ // emitter/stored-name invariant.
2236
+ schemaName: completeTableSchema.schemaName,
2237
+ objectName: completeTableSchema.name,
2238
+ newObject: completeTableSchema
2239
+ });
2240
+ this.emitAutoSchemaEventIfNeeded(moduleName, {
2241
+ type: 'create',
2242
+ objectType: 'table',
2243
+ schemaName: completeTableSchema.schemaName,
2244
+ objectName: completeTableSchema.name,
2245
+ });
2246
+ return completeTableSchema;
2247
+ }
2248
+ /**
2249
+ * Creates a backing table from a pre-built `TableSchema` rather than a
2250
+ * CREATE TABLE AST. Used by materialized views, whose backing-table columns
2251
+ * and primary key are derived from the optimized body relation (carrying
2252
+ * full {@link import('../common/datatype.js').ScalarType} fidelity that a
2253
+ * round-trip through SQL type strings would lose).
2254
+ *
2255
+ * Reuses the same internal sequence as {@link createTable} —
2256
+ * `finalizeCreatedTableSchema` → `addTable` → `table_added` notify — so the
2257
+ * backing table behaves like any other table, except the instance is built via
2258
+ * the module's optional `createBacking` when present (`createBacking?() ??
2259
+ * create()`): a durable-backing module routes the backing into its durable
2260
+ * store there; modules without it fall through to `create` (today's behavior).
2261
+ * The supplied schema must carry `vtabModule`/`vtabModuleName` (typically
2262
+ * `memory`).
2263
+ */
2264
+ async createBackingTable(tableSchema) {
2265
+ const targetSchemaName = tableSchema.schemaName;
2266
+ const tableName = tableSchema.name;
2267
+ const schema = this.getSchema(targetSchemaName);
2268
+ if (!schema) {
2269
+ throw new QuereusError(`Internal error: Schema '${targetSchemaName}' not found.`, StatusCode.INTERNAL);
2270
+ }
2271
+ if (schema.getTable(tableName) || schema.getView(tableName)) {
2272
+ throw new QuereusError(`Backing table ${targetSchemaName}.${tableName} already exists`, StatusCode.CONSTRAINT);
2273
+ }
2274
+ const moduleName = tableSchema.vtabModuleName;
2275
+ const moduleInfo = this.getModule(moduleName);
2276
+ if (!moduleInfo || !moduleInfo.module) {
2277
+ throw new QuereusError(`No virtual table module named '${moduleName}'`, StatusCode.ERROR);
2278
+ }
2279
+ let tableInstance;
2280
+ try {
2281
+ const create = moduleInfo.module.createBacking?.bind(moduleInfo.module)
2282
+ ?? moduleInfo.module.create.bind(moduleInfo.module);
2283
+ tableInstance = await create(this.db, tableSchema);
2284
+ }
2285
+ catch (e) {
2286
+ const message = e instanceof Error ? e.message : String(e);
2287
+ const code = e instanceof QuereusError ? e.code : StatusCode.ERROR;
2288
+ throw new QuereusError(`Module '${moduleName}' backing create failed for '${tableName}': ${message}`, code, e instanceof Error ? e : undefined);
2289
+ }
2290
+ const completeTableSchema = this.finalizeCreatedTableSchema(tableInstance, tableName, targetSchemaName, moduleName, tableSchema.vtabArgs ?? {}, moduleInfo);
2291
+ schema.addTable(completeTableSchema);
2292
+ log(`Successfully created backing table %s.%s using module %s`, targetSchemaName, tableName, moduleName);
1284
2293
  this.changeNotifier.notifyChange({
1285
2294
  type: 'table_added',
1286
2295
  schemaName: targetSchemaName,
@@ -1299,25 +2308,46 @@ export class SchemaManager {
1299
2308
  * Import catalog objects from DDL statements without triggering storage creation.
1300
2309
  * Used when connecting to existing storage that already contains data.
1301
2310
  *
2311
+ * Options enable the materialized-view **adopt-without-refill fast path**
2312
+ * (see {@link importMaterializedView}); omitted, every MV refills — the
2313
+ * always-correct default.
2314
+ *
1302
2315
  * This method:
1303
2316
  * 1. Parses each DDL statement
1304
- * 2. Registers the schema objects (tables, indexes)
1305
- * 3. Calls module.connect() instead of module.create()
2317
+ * 2. Registers the schema objects (tables, indexes, views, materialized views)
2318
+ * 3. Calls module.connect() instead of module.create() for tables (a
2319
+ * materialized view's memory backing is re-materialized instead — see
2320
+ * {@link importMaterializedView})
1306
2321
  * 4. Skips schema change hooks (since these are existing objects)
1307
2322
  *
1308
- * @param ddlStatements Array of DDL strings (CREATE TABLE, CREATE INDEX, etc.)
2323
+ * Each DDL string may hold **one or more** statements: a catalog entry can
2324
+ * bundle a `CREATE TABLE` immediately followed by the `CREATE INDEX`es that
2325
+ * belong to it. Statements within an entry are imported in document order, so
2326
+ * a table always precedes the indexes that reference it. (Because every
2327
+ * table's indexes are co-located with it in one entry, no global
2328
+ * table-before-index ordering across entries is required.)
2329
+ *
2330
+ * @param ddlStatements Array of DDL strings (each one or more CREATE TABLE / CREATE INDEX, etc.)
2331
+ * @param options Adopt-fast-path options for materialized views — see {@link ImportCatalogOptions}
1309
2332
  * @returns Array of imported object names
1310
2333
  */
1311
- async importCatalog(ddlStatements) {
1312
- const imported = { tables: [], indexes: [] };
2334
+ async importCatalog(ddlStatements, options) {
2335
+ const imported = { tables: [], indexes: [], views: [], materializedViews: [] };
1313
2336
  for (const ddl of ddlStatements) {
1314
2337
  try {
1315
- const result = await this.importSingleDDL(ddl);
1316
- if (result.type === 'table') {
1317
- imported.tables.push(result.name);
1318
- }
1319
- else if (result.type === 'index') {
1320
- imported.indexes.push(result.name);
2338
+ for (const result of await this.importDDL(ddl, options)) {
2339
+ if (result.type === 'table') {
2340
+ imported.tables.push(result.name);
2341
+ }
2342
+ else if (result.type === 'index') {
2343
+ imported.indexes.push(result.name);
2344
+ }
2345
+ else if (result.type === 'view') {
2346
+ imported.views.push(result.name);
2347
+ }
2348
+ else {
2349
+ imported.materializedViews.push(result.name);
2350
+ }
1321
2351
  }
1322
2352
  }
1323
2353
  catch (e) {
@@ -1326,26 +2356,285 @@ export class SchemaManager {
1326
2356
  throw e;
1327
2357
  }
1328
2358
  }
1329
- log('Imported catalog: %d tables, %d indexes', imported.tables.length, imported.indexes.length);
2359
+ log('Imported catalog: %d tables, %d indexes, %d views, %d materialized views', imported.tables.length, imported.indexes.length, imported.views.length, imported.materializedViews.length);
1330
2360
  return imported;
1331
2361
  }
1332
2362
  /**
1333
- * Import a single DDL statement without creating storage.
2363
+ * Import every statement in a DDL string without creating storage, in document
2364
+ * order. A single string may carry several statements (a table bundled with
2365
+ * its indexes); single-statement entries remain valid. An `alter index … tags`
2366
+ * statement (the store bundle's vehicle for exposed-implicit-index user tags)
2367
+ * applies silently against the just-imported table and contributes no result
2368
+ * entry — it modifies an existing object rather than importing one. Any other
2369
+ * unsupported statement type throws — `rehydrateCatalog` relies on this
2370
+ * fail-loud contract to record import errors rather than silently dropping
2371
+ * objects.
1334
2372
  */
1335
- async importSingleDDL(ddl) {
2373
+ async importDDL(ddl, options) {
1336
2374
  const parser = new Parser();
1337
2375
  const statements = parser.parseAll(ddl);
1338
- if (statements.length !== 1) {
1339
- throw new QuereusError(`importCatalog expects exactly one statement per DDL, got ${statements.length}`, StatusCode.ERROR);
2376
+ const results = [];
2377
+ for (const stmt of statements) {
2378
+ if (stmt.type === 'createTable') {
2379
+ const createTable = stmt;
2380
+ // Unified model: a maintained table persists/exports as the canonical
2381
+ // `create table … maintained as <body>` form (see generateMaintainedTableDDL).
2382
+ // That re-parses as a createTable carrying a `maintained` clause, so route
2383
+ // it through the same re-materialize/adopt path as the MV sugar — the plain
2384
+ // `importTable` connect path would try (and fail) to reconnect an ephemeral
2385
+ // backing that was never persisted.
2386
+ if (createTable.maintained) {
2387
+ results.push(await this.importMaterializedView(maintainedImportFromTableStmt(createTable), options));
2388
+ }
2389
+ else {
2390
+ results.push(await this.importTable(createTable));
2391
+ }
2392
+ }
2393
+ else if (stmt.type === 'createIndex') {
2394
+ results.push(await this.importIndex(stmt));
2395
+ }
2396
+ else if (stmt.type === 'createView') {
2397
+ // Plain views import silently (body planning deferred to first reference).
2398
+ results.push(this.importView(stmt));
2399
+ }
2400
+ else if (stmt.type === 'createMaterializedView') {
2401
+ // Materialized views re-materialize (backing rebuilt from current source
2402
+ // data, row-time maintenance re-registered) — silent like the other arms.
2403
+ // Under `options.trustBackings` a pre-existing durable backing that
2404
+ // passes every adopt gate is adopted without the refill.
2405
+ results.push(await this.importMaterializedView(maintainedImportFromMvStmt(stmt), options));
2406
+ }
2407
+ else if (stmt.type === 'alterIndex') {
2408
+ // Tag re-application on an already-imported object — no result entry.
2409
+ this.applyImportedIndexTags(stmt);
2410
+ }
2411
+ else {
2412
+ throw new QuereusError(`importCatalog does not support statement type: ${stmt.type}`, StatusCode.ERROR);
2413
+ }
2414
+ }
2415
+ return results;
2416
+ }
2417
+ /**
2418
+ * Apply an `alter index … {set|add|drop} tags` statement during catalog import,
2419
+ * **silently** — no `notifyChange`, mirroring {@link importTable}/{@link importIndex}
2420
+ * (a store rehydrating its own catalog must not re-emit persistence events).
2421
+ * Shares {@link resolveIndexTagSwap} with the live {@link updateIndexTags} path,
2422
+ * so the bundle's statement resolves exactly as a user-issued ALTER would —
2423
+ * materialized `IndexSchema` first, then the exposed-implicit-constraint
2424
+ * fallback (the store-bundle case: the `alter index` line follows its
2425
+ * `CREATE TABLE` in the same entry, so the constraint is already registered).
2426
+ * All three action forms map through the shared `freezeTags`/`mutateTagRecord`
2427
+ * helpers, though the bundle generator only emits the whole-set replace form.
2428
+ * An unresolvable target throws NOTFOUND — the bundle and its alter line come
2429
+ * from one `TableSchema` snapshot, so a miss indicates real corruption, which
2430
+ * `rehydrateCatalog` records per-entry.
2431
+ */
2432
+ applyImportedIndexTags(stmt) {
2433
+ const targetSchemaName = stmt.name.schema ?? this.getCurrentSchemaName();
2434
+ const action = stmt.action;
2435
+ let compute;
2436
+ if (action.type === 'dropTags') {
2437
+ compute = current => this.mutateTagRecord(current, { op: 'drop', keys: action.keys });
2438
+ }
2439
+ else if (action.mode === 'merge') {
2440
+ compute = current => this.mutateTagRecord(current, { op: 'merge', tags: action.tags });
1340
2441
  }
1341
- const stmt = statements[0];
1342
- if (stmt.type === 'createTable') {
1343
- return this.importTable(stmt);
2442
+ else {
2443
+ compute = () => this.freezeTags(action.tags);
2444
+ }
2445
+ const { newSchema } = this.resolveIndexTagSwap(targetSchemaName, stmt.name.name, compute);
2446
+ this.getSchemaOrFail(targetSchemaName).addTable(newSchema);
2447
+ log(`Applied imported index tags for %s in schema %s`, stmt.name.name, targetSchemaName);
2448
+ }
2449
+ /**
2450
+ * Import a plain view from its parsed DDL **without planning the body**.
2451
+ * Registration is silent — no `notifyChange` fires, mirroring
2452
+ * {@link importTable}/{@link importIndex} (a store rehydrating its own catalog
2453
+ * must not re-emit persistence events). Body validation is deferred to first
2454
+ * reference, exactly as {@link importTable} defers create-time work via
2455
+ * `connect`: this makes view rehydration order-independent — a view over
2456
+ * another view, a materialized view, or a not-yet-imported relation registers
2457
+ * regardless of phase order, and a broken body surfaces only when queried.
2458
+ *
2459
+ * The stored `sql` is the canonical {@link createViewToString} rendering (not
2460
+ * the raw entry text, which may bundle several statements). Synchronous: unlike
2461
+ * table/index import there is no module storage to bind, so there is nothing to
2462
+ * await.
2463
+ */
2464
+ importView(stmt) {
2465
+ // Create the schema if absent (mirrors importTable) so a view rehydrates
2466
+ // even into a schema that holds no tables; its `.name` is the canonical
2467
+ // stored schemaName (see canonicalSchemaName).
2468
+ const schema = this.getOrCreateSchema(stmt.view.schema || this.getCurrentSchemaName());
2469
+ const targetSchemaName = schema.name;
2470
+ const viewName = stmt.view.name;
2471
+ const viewSchema = {
2472
+ name: viewName,
2473
+ schemaName: targetSchemaName,
2474
+ sql: createViewToString(stmt),
2475
+ // Any `with defaults (…)` rides inside stmt.select (→ selectAst).
2476
+ selectAst: stmt.select,
2477
+ columns: stmt.columns ? Object.freeze([...stmt.columns]) : undefined,
2478
+ tags: stmt.tags && Object.keys(stmt.tags).length > 0 ? Object.freeze({ ...stmt.tags }) : undefined,
2479
+ };
2480
+ schema.addView(viewSchema);
2481
+ log(`Imported view %s.%s`, targetSchemaName, viewName);
2482
+ return { type: 'view', name: `${targetSchemaName}.${viewName}` };
2483
+ }
2484
+ /**
2485
+ * Import a materialized view from its parsed DDL by re-materializing it
2486
+ * through the shared {@link materializeView} core: the body is re-planned
2487
+ * against the current (already-imported) sources, the memory backing table is
2488
+ * rebuilt and filled, and row-time maintenance is re-registered — the same
2489
+ * work the create emitter does, minus the `materialized_view_added` event (a
2490
+ * store rehydrating its own catalog must not re-emit persistence events;
2491
+ * `table_added` still fires for the backing table, exactly as on create).
2492
+ *
2493
+ * Unlike {@link importView}, the body plans EAGERLY (the backing cannot be
2494
+ * filled without running it), so MV import is order-dependent: the body's
2495
+ * sources — including another MV's backing for MV-over-MV — must already be
2496
+ * registered. The store's `rehydrateCatalog` orders MVs after tables/views
2497
+ * and resolves MV-over-MV chains by fixpoint retry. A body that cannot plan,
2498
+ * fills with duplicate keys, or fails the row-time eligibility gate in
2499
+ * `registerMaterializedView` throws (after {@link materializeView} rolls the
2500
+ * half-built backing back), and the caller records it as a per-entry
2501
+ * rehydration error.
2502
+ *
2503
+ * **Adopt fast path.** A pre-existing table at the backing name in the MV's
2504
+ * own backing module (a durable host's phase-1 rehydration of its backing)
2505
+ * is ADOPTED — registered as-is, no body re-execution — iff ALL gates pass
2506
+ * (see {@link tryAdoptPreExistingBacking}); otherwise it is dropped and the
2507
+ * MV refills through {@link materializeView}. Only import ever adopts —
2508
+ * create and refresh are unchanged.
2509
+ */
2510
+ async importMaterializedView(spec, options) {
2511
+ // Canonical stored schemaName (see canonicalSchemaName) — the schema itself
2512
+ // is created below, after the DML-body gate.
2513
+ const targetSchemaName = this.canonicalSchemaName(spec.schemaName || this.getCurrentSchemaName());
2514
+ const viewName = spec.name;
2515
+ // A DML body (insert/update/delete … returning) parses but is un-creatable —
2516
+ // `planViewBody` rejects it at build time. Reject it here too, BEFORE
2517
+ // materializing: a corrupt or hand-edited catalog entry would otherwise
2518
+ // EXECUTE the mutation against live source tables during rehydrate.
2519
+ if (spec.select.type === 'insert' || spec.select.type === 'update' || spec.select.type === 'delete') {
2520
+ throw new QuereusError(`${spec.select.type.toUpperCase()} cannot be used as a materialized view body`, StatusCode.ERROR);
2521
+ }
2522
+ // Create the schema if absent (mirrors importTable/importView) so an MV
2523
+ // rehydrates even into a schema that holds no tables.
2524
+ this.getOrCreateSchema(targetSchemaName);
2525
+ // Honor the re-parsed `using <module>(...)` clause (the generator emits it
2526
+ // only when non-default). An unknown or capability-less module throws from
2527
+ // buildBackingTableSchema inside materializeView — the caller records it
2528
+ // as a per-entry rehydration error.
2529
+ const backing = normalizeBackingModule(spec.moduleName, spec.moduleArgs);
2530
+ const def = {
2531
+ schemaName: targetSchemaName,
2532
+ viewName,
2533
+ // Any `with defaults (…)` rides inside spec.select (→ selectAst).
2534
+ selectAst: spec.select,
2535
+ bodySql: astToString(spec.select),
2536
+ columns: spec.columns,
2537
+ tags: spec.tags ? Object.freeze({ ...spec.tags }) : undefined,
2538
+ backingModuleName: backing.storedModuleName,
2539
+ backingModuleArgs: backing.storedModuleArgs,
2540
+ };
2541
+ // Derive the backing shape ONCE for the whole import (ordering gate, adopt
2542
+ // gates, and the refill all read it). Throws when the body cannot plan —
2543
+ // deliberately BEFORE any drop, so a not-yet-resolvable body (the store's
2544
+ // MV-over-MV fixpoint: a dependent fails until its upstream's round lands)
2545
+ // errors per-entry with any pre-existing backing preserved — data-safe.
2546
+ const shape = deriveBackingShape(this.db, def.bodySql, def.columns);
2547
+ // Declared-column arity mismatch: the entry can NEVER materialize, so throw
2548
+ // with the backing preserved rather than dropping durable rows for nothing.
2549
+ assertDeclaredColumnArity(def, shape);
2550
+ // Ordering gate: a body source that is itself a pending maintained-table
2551
+ // entry (its own `create materialized view` not yet imported this session)
2552
+ // already pre-exists as a *plain* table, so the body PLANS — but its content
2553
+ // may be about to be replaced by the upstream's own import. Defer this entry
2554
+ // to a later fixpoint round (per-entry error; the store retries).
2555
+ for (const src of shape.sourceTables) {
2556
+ if (options?.pendingDerivations?.has(src)) {
2557
+ throw new QuereusError(`cannot import materialized view '${targetSchemaName}.${viewName}' yet: source '${src}' is a maintained table whose own catalog entry has not imported this session`, StatusCode.ERROR);
2558
+ }
1344
2559
  }
1345
- else if (stmt.type === 'createIndex') {
1346
- return this.importIndex(stmt);
2560
+ // A durable backing-host module may have rehydrated the maintained table
2561
+ // itself (phase 1: a plain `create table` bundle under the same name)
2562
+ // before this MV catalog entry imports (phase 3). A pre-existing
2563
+ // derivation-less table owned by the MV's own backing module IS that
2564
+ // rehydrated backing: adopt it when every gate passes, else drop it and
2565
+ // re-materialize from the body. A table in a DIFFERENT module is not
2566
+ // ours — fail the entry rather than dropping user data. The create
2567
+ // emitter keeps the plain collision error.
2568
+ const preExisting = this.getTable(targetSchemaName, viewName);
2569
+ if (preExisting) {
2570
+ if (isMaintainedTable(preExisting)) {
2571
+ throw new QuereusError(`cannot import materialized view '${targetSchemaName}.${viewName}': a maintained table with the same name already exists`, StatusCode.CONSTRAINT);
2572
+ }
2573
+ if ((preExisting.vtabModuleName ?? '').toLowerCase() === backing.moduleName) {
2574
+ if (options?.trustBackings && await this.tryAdoptPreExistingBacking(def, preExisting, backing.moduleName, options.adoptedBackings, shape)) {
2575
+ log(`Adopted materialized view %s.%s (durable backing trusted; refill skipped)`, targetSchemaName, viewName);
2576
+ return { type: 'materializedView', name: `${targetSchemaName}.${viewName}` };
2577
+ }
2578
+ await this.dropTable(targetSchemaName, viewName, /*ifExists*/ true);
2579
+ }
2580
+ else {
2581
+ throw new QuereusError(`cannot import materialized view '${targetSchemaName}.${viewName}': table '${viewName}' already exists in module '${preExisting.vtabModuleName}', not the MV's backing module '${backing.moduleName}'`, StatusCode.CONSTRAINT);
2582
+ }
1347
2583
  }
1348
- throw new QuereusError(`importCatalog does not support statement type: ${stmt.type}`, StatusCode.ERROR);
2584
+ await materializeView(this.db, def, shape);
2585
+ log(`Imported materialized view %s.%s`, targetSchemaName, viewName);
2586
+ return { type: 'materializedView', name: `${targetSchemaName}.${viewName}` };
2587
+ }
2588
+ /**
2589
+ * The adopt-without-refill gate check + adopt for a pre-existing same-module
2590
+ * backing during MV import. Returns true when the backing was adopted; false
2591
+ * means "fall back to drop+refill". `shape` is the caller's pre-derived
2592
+ * backing shape (derived before any drop — see {@link importMaterializedView}).
2593
+ * Gates, of five (the caller already verified gate 1, same-module, and
2594
+ * gate 5, `trustBackings`):
2595
+ *
2596
+ * 2. **Shape** — `backingShapeMatches(preExisting, shape)`: the persisted
2597
+ * backing is column-for-column what the re-planned body would build
2598
+ * (names, logical types, not-null, collation, physical PK).
2599
+ * 3. **bodyHash** — automatic by construction: the catalog persists DDL and
2600
+ * import re-parses it, recomputing `computeBodyHash` from the same
2601
+ * canonical definition — there is no independently persisted hash that
2602
+ * could diverge, so no runtime check is possible or needed.
2603
+ * 4. **Sources** — every table the body reads lives in the SAME module as
2604
+ * the backing (one storage substrate ⇒ the divergence window is the
2605
+ * documented crash window the marker attests against; a cross-module
2606
+ * source — e.g. memory — was itself just recomputed, so persisted backing
2607
+ * rows may be stale relative to it), AND every source that is itself a
2608
+ * maintained table was ADOPTED this session (`adoptedBackings`) — a
2609
+ * refilled upstream may hold new content its dependents must reflect.
2610
+ * (The caller's pending-derivations gate guarantees every maintained
2611
+ * source's own entry has already imported, so derivation presence is
2612
+ * decidable here.)
2613
+ *
2614
+ * A throw from `adoptMaterializedView` itself (the row-time eligibility gate
2615
+ * in registration) propagates — per-entry error, backing left registered as
2616
+ * a plain (derivation-less) table.
2617
+ */
2618
+ async tryAdoptPreExistingBacking(def, preExisting, backingModuleName, adoptedBackings, shape) {
2619
+ if (!backingShapeMatches(preExisting, shape))
2620
+ return false;
2621
+ for (const qualified of shape.sourceTables) {
2622
+ const dot = qualified.indexOf('.');
2623
+ const sourceSchema = dot >= 0 ? qualified.slice(0, dot) : this.getCurrentSchemaName();
2624
+ const sourceName = dot >= 0 ? qualified.slice(dot + 1) : qualified;
2625
+ const source = this.getTable(sourceSchema, sourceName);
2626
+ if (!source)
2627
+ return false;
2628
+ if ((source.vtabModuleName ?? '').toLowerCase() !== backingModuleName)
2629
+ return false;
2630
+ // A maintained-table source is another MV's backing; require it was
2631
+ // adopted, not refilled, this session.
2632
+ if (isMaintainedTable(source) && !adoptedBackings?.has(qualified))
2633
+ return false;
2634
+ }
2635
+ await adoptMaterializedView(this.db, def, preExisting, shape);
2636
+ adoptedBackings?.add(`${def.schemaName}.${def.viewName}`.toLowerCase());
2637
+ return true;
1349
2638
  }
1350
2639
  /**
1351
2640
  * Import a table schema without calling module.create().
@@ -1355,26 +2644,45 @@ export class SchemaManager {
1355
2644
  const targetSchemaName = stmt.table.schema || this.getCurrentSchemaName();
1356
2645
  const tableName = stmt.table.name;
1357
2646
  const { moduleName, effectiveModuleArgs, moduleInfo } = this.resolveModuleInfo(stmt);
1358
- const tableSchema = this.buildTableSchemaFromAST(stmt, moduleName, effectiveModuleArgs, moduleInfo);
2647
+ // Rehydrate path: persisted DDL already made any non-BINARY collation explicit,
2648
+ // so an omitted COLLATE is canonical BINARY regardless of the live session default.
2649
+ const tableSchema = this.buildTableSchemaFromAST(stmt, moduleName, effectiveModuleArgs, moduleInfo, 'BINARY');
1359
2650
  try {
1360
- await moduleInfo.module.connect(this.db, moduleInfo.auxData, moduleName, targetSchemaName, tableName, effectiveModuleArgs, tableSchema);
2651
+ // Module-facing stored-name contract (see canonicalSchemaName): the
2652
+ // rehydrated connect receives `tableSchema`'s canonical schemaName and
2653
+ // stored display casing, never the raw persisted-DDL spelling — a module
2654
+ // keying storage by the args addresses the same physical store it created.
2655
+ await moduleInfo.module.connect(this.db, moduleInfo.auxData, moduleName, tableSchema.schemaName, tableSchema.name, effectiveModuleArgs, tableSchema);
1361
2656
  }
1362
2657
  catch (e) {
1363
2658
  const message = e instanceof Error ? e.message : String(e);
1364
2659
  throw new QuereusError(`Module '${moduleName}' connect failed during import for table '${tableName}': ${message}`, StatusCode.ERROR);
1365
2660
  }
1366
- let schema = this.getSchema(targetSchemaName);
1367
- if (!schema) {
1368
- const lowerSchemaName = targetSchemaName.toLowerCase();
1369
- schema = new Schema(lowerSchemaName);
1370
- this.schemas.set(lowerSchemaName, schema);
1371
- }
2661
+ const schema = this.getOrCreateSchema(tableSchema.schemaName);
1372
2662
  schema.addTable(tableSchema);
2663
+ // Catalog rehydration registers FK-bearing tables silently (no `table_added`
2664
+ // fires) and via `getOrCreateSchema`, which only resets when it *creates* a
2665
+ // schema — so importing a child into an existing schema (e.g. `main`) would
2666
+ // otherwise leave a stale reverse FK index. Cold reopen masks this (the index
2667
+ // is built lazily only after rehydration completes), but a re-import onto a
2668
+ // live, already-built index would under-report — the fatal direction. Reset
2669
+ // directly so the silent-import path upholds the same invariant as the events.
2670
+ // The lens basis-FK gate shares this vector: a basis table imported silently
2671
+ // after the gate was built would otherwise leave it under-reporting too.
2672
+ this.invalidateReverseFkIndex();
2673
+ this.invalidateLensFkGate();
1373
2674
  log(`Imported table %s.%s using module %s`, targetSchemaName, tableName, moduleName);
1374
- return { type: 'table', name: `${targetSchemaName}.${tableName}` };
2675
+ return { type: 'table', name: `${tableSchema.schemaName}.${tableSchema.name}` };
1375
2676
  }
1376
2677
  /**
1377
2678
  * Import an index schema without calling module.createIndex().
2679
+ *
2680
+ * Reconstructs the index with full fidelity from the re-parsed DDL so a
2681
+ * `CREATE [UNIQUE] INDEX ... (col [COLLATE x]) [WHERE ...]` survives a
2682
+ * catalog round-trip: per-column collation, the UNIQUE flag, the partial
2683
+ * predicate, and (for a unique index) the synthesized `derivedFromIndex`
2684
+ * UNIQUE constraint — mirroring the live `buildIndexSchema` + the shared
2685
+ * {@link appendIndexToTableSchema} that {@link createIndex} uses.
1378
2686
  */
1379
2687
  async importIndex(stmt) {
1380
2688
  const targetSchemaName = stmt.table.schema || this.getCurrentSchemaName();
@@ -1385,9 +2693,10 @@ export class SchemaManager {
1385
2693
  if (!tableSchema) {
1386
2694
  throw new QuereusError(`Cannot import index '${indexName}': table '${tableName}' not found`, StatusCode.ERROR);
1387
2695
  }
1388
- // Build index columns schema
2696
+ // Build index columns schema. Mirrors buildIndexSchema's collation resolution
2697
+ // (per-column COLLATE → table column collation → BINARY).
1389
2698
  const indexColumns = stmt.columns.map(col => {
1390
- const colName = col.name;
2699
+ const { name: colName, collation } = resolveImportedIndexColumn(col);
1391
2700
  if (!colName) {
1392
2701
  throw new QuereusError(`Expression-based index columns are not supported during import`, StatusCode.ERROR);
1393
2702
  }
@@ -1395,26 +2704,52 @@ export class SchemaManager {
1395
2704
  if (colIdx === undefined) {
1396
2705
  throw new QuereusError(`Column '${colName}' not found in table '${tableName}'`, StatusCode.ERROR);
1397
2706
  }
2707
+ const tableColSchema = tableSchema.columns[colIdx];
1398
2708
  return {
1399
2709
  index: colIdx,
1400
2710
  desc: col.direction === 'desc',
2711
+ collation: normalizeCollationName(collation || tableColSchema.collation || 'BINARY'),
1401
2712
  };
1402
2713
  });
1403
2714
  const indexSchema = {
1404
2715
  name: indexName,
1405
2716
  columns: Object.freeze(indexColumns),
2717
+ unique: stmt.isUnique || undefined,
2718
+ predicate: stmt.where,
1406
2719
  tags: stmt.tags && Object.keys(stmt.tags).length > 0 ? Object.freeze({ ...stmt.tags }) : undefined,
1407
2720
  };
1408
- // Add index to table without calling module.createIndex()
1409
- const updatedIndexes = [...(tableSchema.indexes || []), indexSchema];
1410
- const updatedTableSchema = {
1411
- ...tableSchema,
1412
- indexes: Object.freeze(updatedIndexes),
1413
- };
2721
+ // Append the index (and synthesize the derived UNIQUE constraint when
2722
+ // unique) without calling module.createIndex() the storage already exists.
2723
+ const updatedTableSchema = appendIndexToTableSchema(tableSchema, indexSchema);
1414
2724
  const schema = this.getSchemaOrFail(targetSchemaName);
1415
2725
  schema.addTable(updatedTableSchema);
1416
2726
  log(`Imported index %s on table %s.%s`, indexName, targetSchemaName, tableName);
1417
2727
  return { type: 'index', name: `${targetSchemaName}.${tableName}.${indexName}` };
1418
2728
  }
1419
2729
  }
2730
+ /**
2731
+ * Resolves an indexed-column AST node to its underlying column name and optional
2732
+ * collation, for catalog import.
2733
+ *
2734
+ * The parser folds `col COLLATE x` into a `collate` expression wrapping a bare
2735
+ * column reference (see `indexedColumn()` in parser.ts), leaving `col.name`
2736
+ * unset. Since `generateIndexDDL` always emits an explicit `COLLATE <c>` per
2737
+ * column, *every* generated index DDL re-parses into this collate-wrapped form —
2738
+ * so unwrapping it is required for the common case, not just non-BINARY
2739
+ * collations. A genuine expression index (non-column operand) returns an unset
2740
+ * name and is rejected by the caller.
2741
+ */
2742
+ function resolveImportedIndexColumn(col) {
2743
+ // Bare column reference (`col [ASC|DESC]`) — name set directly by the parser.
2744
+ if (col.name) {
2745
+ return { name: col.name, collation: col.collation };
2746
+ }
2747
+ // Collate-wrapped column (`col COLLATE x`) — unwrap to the column + collation.
2748
+ const expr = col.expr;
2749
+ if (expr?.type === 'collate' && expr.expr.type === 'column' && !expr.expr.table && !expr.expr.schema) {
2750
+ return { name: expr.expr.name, collation: expr.collation };
2751
+ }
2752
+ // Anything else is a genuine expression index — unsupported on import.
2753
+ return { name: undefined, collation: undefined };
2754
+ }
1420
2755
  //# sourceMappingURL=manager.js.map