@quereus/quereus 4.20.0 → 4.20.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/README.md +2 -0
  2. package/dist/src/common/types.d.ts +4 -4
  3. package/dist/src/index.d.ts +1 -1
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +3 -2
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/schema/catalog-rendering.js +13 -1
  8. package/dist/src/schema/catalog-rendering.js.map +1 -1
  9. package/dist/src/schema/catalog.d.ts +35 -2
  10. package/dist/src/schema/catalog.d.ts.map +1 -1
  11. package/dist/src/schema/catalog.js +42 -33
  12. package/dist/src/schema/catalog.js.map +1 -1
  13. package/dist/src/schema/schema-differ.d.ts +1 -1
  14. package/dist/src/schema/schema-differ.d.ts.map +1 -1
  15. package/dist/src/schema/schema-differ.js +195 -33
  16. package/dist/src/schema/schema-differ.js.map +1 -1
  17. package/dist/src/schema/unique-enforcement.d.ts +23 -1
  18. package/dist/src/schema/unique-enforcement.d.ts.map +1 -1
  19. package/dist/src/schema/unique-enforcement.js +43 -1
  20. package/dist/src/schema/unique-enforcement.js.map +1 -1
  21. package/dist/src/vtab/memory/layer/manager.d.ts +15 -12
  22. package/dist/src/vtab/memory/layer/manager.d.ts.map +1 -1
  23. package/dist/src/vtab/memory/layer/manager.js +87 -100
  24. package/dist/src/vtab/memory/layer/manager.js.map +1 -1
  25. package/package.json +7 -4
  26. package/test/README.md +173 -0
  27. package/test/logic/01-basic.sqllogic +39 -0
  28. package/test/logic/01.1-select-projection-extras.sqllogic +130 -0
  29. package/test/logic/01.2-strings.sqllogic +43 -0
  30. package/test/logic/01.5-insert-select.sqllogic +237 -0
  31. package/test/logic/01.6-update-extras.sqllogic +118 -0
  32. package/test/logic/01.7-update-from.sqllogic +67 -0
  33. package/test/logic/01.8-delete-extras.sqllogic +122 -0
  34. package/test/logic/01.8.1-delete-range-cow.sqllogic +115 -0
  35. package/test/logic/01.9-query-expr-dml-gates.sqllogic +32 -0
  36. package/test/logic/01.9-query-expr-dml.sqllogic +171 -0
  37. package/test/logic/01.9-query-expr-values.sqllogic +111 -0
  38. package/test/logic/01.9.1-limit-over-dml-subquery.sqllogic +119 -0
  39. package/test/logic/02-filters.sqllogic +54 -0
  40. package/test/logic/02-smoke.sqllogic +62 -0
  41. package/test/logic/02.1-bind-parameters.sqllogic +223 -0
  42. package/test/logic/03-expressions.sqllogic +190 -0
  43. package/test/logic/03.1-quoted-identifiers.sqllogic +55 -0
  44. package/test/logic/03.4-defaults.sqllogic +554 -0
  45. package/test/logic/03.4.1-default-edge-cases.sqllogic +99 -0
  46. package/test/logic/03.5-tvf.sqllogic +61 -0
  47. package/test/logic/03.6-type-system.sqllogic +544 -0
  48. package/test/logic/03.6.1-numeric-text-comparison-coercion.sqllogic +244 -0
  49. package/test/logic/03.6.2-value-to-text.sqllogic +224 -0
  50. package/test/logic/03.7-bigint-mixed-arithmetic.sqllogic +65 -0
  51. package/test/logic/03.8-not-precedence.sqllogic +255 -0
  52. package/test/logic/03.9-is-bool-predicate.sqllogic +150 -0
  53. package/test/logic/03.9.1-is-bool-predicate-partial-index.sqllogic +34 -0
  54. package/test/logic/04-order-by.sqllogic +38 -0
  55. package/test/logic/04-transactions.sqllogic +239 -0
  56. package/test/logic/04a-savepoint-lazy-attach.sqllogic +122 -0
  57. package/test/logic/05-vtab_memory.sqllogic +50 -0
  58. package/test/logic/05.0.1-vtab-memory-unique-index-collation.sqllogic +34 -0
  59. package/test/logic/05.1-composite-pk-range-scan.sqllogic +147 -0
  60. package/test/logic/05.1.1-secondary-index-range-scan.sqllogic +56 -0
  61. package/test/logic/05.2-cast-seek-correctness.sqllogic +124 -0
  62. package/test/logic/06-builtin_functions.sqllogic +149 -0
  63. package/test/logic/06.1-string-functions.sqllogic +92 -0
  64. package/test/logic/06.1.3-like-glob-edges.sqllogic +53 -0
  65. package/test/logic/06.2-math-functions.sqllogic +82 -0
  66. package/test/logic/06.3-schema.sqllogic +48 -0
  67. package/test/logic/06.3.1-schema-all-schemas.sqllogic +39 -0
  68. package/test/logic/06.3.2-schema-foreign-keys.sqllogic +208 -0
  69. package/test/logic/06.3.3-introspection-tags.sqllogic +202 -0
  70. package/test/logic/06.3.3.1-introspection-tags-index.sqllogic +74 -0
  71. package/test/logic/06.3.4-view-info.sqllogic +378 -0
  72. package/test/logic/06.3.5-column-info.sqllogic +347 -0
  73. package/test/logic/06.3.6-schema-views.sqllogic +29 -0
  74. package/test/logic/06.4-schema-search-path.sqllogic +354 -0
  75. package/test/logic/06.4.1-schema-case-insensitive.sqllogic +83 -0
  76. package/test/logic/06.4.2-collation-extras.sqllogic +472 -0
  77. package/test/logic/06.4.2.1-collation-index-access-paths.sqllogic +140 -0
  78. package/test/logic/06.4.3-write-path-collation.sqllogic +99 -0
  79. package/test/logic/06.4.4-comparison-collation-precedence.sqllogic +342 -0
  80. package/test/logic/06.4.5-any-collate-declared-keys.sqllogic +92 -0
  81. package/test/logic/06.5-polymorphic-types.sqllogic +169 -0
  82. package/test/logic/06.5.1-conversion-functions.sqllogic +173 -0
  83. package/test/logic/06.5.2-scalar-minmax.sqllogic +16 -0
  84. package/test/logic/06.5.3-undeclared-return-type-comparison.sqllogic +244 -0
  85. package/test/logic/06.5.4-declared-return-type-builtins.sqllogic +288 -0
  86. package/test/logic/06.6-aggregate-extended.sqllogic +78 -0
  87. package/test/logic/06.7-json-extended.sqllogic +53 -0
  88. package/test/logic/06.8-json-path-operators.sqllogic +99 -0
  89. package/test/logic/06.9-json-canonical-key.sqllogic +65 -0
  90. package/test/logic/06.9.1-json-coerce-once.sqllogic +417 -0
  91. package/test/logic/06.9.1.1-json-coerce-once-index.sqllogic +39 -0
  92. package/test/logic/06.9.2-json-structural-equality.sqllogic +382 -0
  93. package/test/logic/06.9.2.1-json-structural-equality-index.sqllogic +76 -0
  94. package/test/logic/06.9.3-json-index-range-seek.sqllogic +205 -0
  95. package/test/logic/07-aggregates.sqllogic +193 -0
  96. package/test/logic/07.1-aggregate-filter-clause.sqllogic +12 -0
  97. package/test/logic/07.2-aggregate-order-by.sqllogic +11 -0
  98. package/test/logic/07.3-group-by-extras.sqllogic +602 -0
  99. package/test/logic/07.3.1-group-by-order-by-key.sqllogic +164 -0
  100. package/test/logic/07.3.2-grouped-select-list-shape.sqllogic +231 -0
  101. package/test/logic/07.4-group-by-filter-composite-pk.sqllogic +120 -0
  102. package/test/logic/07.5-sum-mixed-exact-and-float.sqllogic +151 -0
  103. package/test/logic/07.5-window.sqllogic +1941 -0
  104. package/test/logic/07.5.1-window-named.sqllogic +13 -0
  105. package/test/logic/07.5.2-window-nth-value.sqllogic +13 -0
  106. package/test/logic/07.6-subqueries.sqllogic +78 -0
  107. package/test/logic/07.6.1-subquery-extras.sqllogic +93 -0
  108. package/test/logic/07.7-and-or-short-circuit.sqllogic +58 -0
  109. package/test/logic/07.7-in-subquery-caching.sqllogic +174 -0
  110. package/test/logic/07.7-scalar-agg-decorrelation.sqllogic +197 -0
  111. package/test/logic/07.7.1-in-extras.sqllogic +158 -0
  112. package/test/logic/07.7.1-scalar-agg-decorrelation-filter.sqllogic +105 -0
  113. package/test/logic/07.7.2-scalar-agg-decorrelation-nested.sqllogic +115 -0
  114. package/test/logic/07.7.3-exists-in-select-decorrelation.sqllogic +107 -0
  115. package/test/logic/07.7.4-where-conjunct-ordering.sqllogic +121 -0
  116. package/test/logic/07.7.5-filter-lost-under-index-order.sqllogic +134 -0
  117. package/test/logic/07.7.6-correlated-predicate-scope.sqllogic +178 -0
  118. package/test/logic/07.7.7-join-source-scope-shadowing.sqllogic +137 -0
  119. package/test/logic/07.7.8-correlated-ref-to-computed-column.sqllogic +165 -0
  120. package/test/logic/07.7.9-conjunct-monotonicity.sqllogic +200 -0
  121. package/test/logic/07.8-correlated-subquery-edges.sqllogic +71 -0
  122. package/test/logic/07.8.1-correlated-in-decorrelation-gates.sqllogic +156 -0
  123. package/test/logic/07.9-in-value-list.sqllogic +154 -0
  124. package/test/logic/07.9.1-in-value-list-composite-index.sqllogic +38 -0
  125. package/test/logic/08-views.sqllogic +64 -0
  126. package/test/logic/08.1-semi-anti-join.sqllogic +191 -0
  127. package/test/logic/08.1-view-edge-cases.sqllogic +118 -0
  128. package/test/logic/08.1.1-uncorrelated-in-semijoin.sqllogic +251 -0
  129. package/test/logic/08.2-existence-flag-semijoin-recovery.sqllogic +197 -0
  130. package/test/logic/08.3-existence-flag-inner-recovery.sqllogic +102 -0
  131. package/test/logic/08.4-key-set-semi-join.sqllogic +447 -0
  132. package/test/logic/09-set_operations.sqllogic +244 -0
  133. package/test/logic/09.1-set-op-cross-collation.sqllogic +230 -0
  134. package/test/logic/10-distinct_datatypes.sqllogic +127 -0
  135. package/test/logic/10.1-ddl-lifecycle.sqllogic +203 -0
  136. package/test/logic/10.1.1-create-table-syntax-edges.sqllogic +47 -0
  137. package/test/logic/10.1.2-ddl-in-transaction.sqllogic +143 -0
  138. package/test/logic/10.1.2.1-add-constraint-in-transaction.sqllogic +59 -0
  139. package/test/logic/10.1.3-ddl-drop-in-transaction.sqllogic +187 -0
  140. package/test/logic/10.1.3.1-drop-constraint-in-transaction.sqllogic +45 -0
  141. package/test/logic/10.1.4-create-table-duplicate-constraint-names.sqllogic +149 -0
  142. package/test/logic/10.1.4-ddl-transaction-policy.sqllogic +314 -0
  143. package/test/logic/10.1.5-alter-add-constraint-minted-name-disambiguation.sqllogic +219 -0
  144. package/test/logic/10.1.5-create-index-lifecycle.sqllogic +72 -0
  145. package/test/logic/10.1.6-ddl-transaction-policy-index.sqllogic +143 -0
  146. package/test/logic/10.1.7-create-table-default-type-check.sqllogic +119 -0
  147. package/test/logic/10.1.8-alter-add-check-existing-rows.sqllogic +117 -0
  148. package/test/logic/10.2-column-features.sqllogic +365 -0
  149. package/test/logic/10.2.1-table-options-rejection.sqllogic +63 -0
  150. package/test/logic/10.2.2-default-collation-memory.sqllogic +30 -0
  151. package/test/logic/10.3-function-features.sqllogic +247 -0
  152. package/test/logic/10.4-schema-scale.sqllogic +168 -0
  153. package/test/logic/10.4.1-schema-scale-indexes.sqllogic +121 -0
  154. package/test/logic/10.5-indexes.sqllogic +164 -0
  155. package/test/logic/10.5.1-partial-indexes.sqllogic +526 -0
  156. package/test/logic/10.5.2-expression-indexes.sqllogic +34 -0
  157. package/test/logic/10.5.3-desc-index-ordering.sqllogic +130 -0
  158. package/test/logic/10.5.4-composite-pk-index-update-phantom.sqllogic +55 -0
  159. package/test/logic/10.5.5-index-name-uniqueness.sqllogic +76 -0
  160. package/test/logic/10.5.6-minmax-index-boundary.sqllogic +204 -0
  161. package/test/logic/10.5.7-implicit-unique-index-lifecycle.sqllogic +1216 -0
  162. package/test/logic/10.6-distinct-edge-cases.sqllogic +119 -0
  163. package/test/logic/100-predicate-normalization-edge-cases.sqllogic +238 -0
  164. package/test/logic/100.1-where-extras.sqllogic +163 -0
  165. package/test/logic/100.1.1-where-join-on-order-indexed.sqllogic +35 -0
  166. package/test/logic/101-builtin-mutation-kills.sqllogic +974 -0
  167. package/test/logic/101-transaction-edge-cases.sqllogic +395 -0
  168. package/test/logic/102-schema-catalog-edge-cases.sqllogic +252 -0
  169. package/test/logic/102-unique-constraints.sqllogic +145 -0
  170. package/test/logic/102.0.1-schema-catalog-indexed-table.sqllogic +32 -0
  171. package/test/logic/102.1-unique-edge-cases.sqllogic +111 -0
  172. package/test/logic/102.1.1-unique-index-creation-edge-cases.sqllogic +75 -0
  173. package/test/logic/102.2-unique-collation.sqllogic +181 -0
  174. package/test/logic/102.2.1-unique-collation-index-derived.sqllogic +305 -0
  175. package/test/logic/103-database-options-edge-cases.sqllogic +108 -0
  176. package/test/logic/103.1-pragma-boolean-values.sqllogic +126 -0
  177. package/test/logic/104-emit-mutation-kills.sqllogic +236 -0
  178. package/test/logic/105-vtab-memory-mutation-kills.sqllogic +754 -0
  179. package/test/logic/105.1-vtab-memory-index-mutation-kills.sqllogic +540 -0
  180. package/test/logic/106-constraint-extractor-mutation-kills.sqllogic +350 -0
  181. package/test/logic/107-temporal-arithmetic-mutation-kills.sqllogic +833 -0
  182. package/test/logic/108-cardinality-estimation.sqllogic +146 -0
  183. package/test/logic/108.1-analyze-name-resolution.sqllogic +102 -0
  184. package/test/logic/109-aggregate-physical-selection.sqllogic +103 -0
  185. package/test/logic/11-joins.sqllogic +86 -0
  186. package/test/logic/11.1-join-using.sqllogic +267 -0
  187. package/test/logic/11.2-comma-join.sqllogic +35 -0
  188. package/test/logic/11.3-index-nested-loop-join.sqllogic +305 -0
  189. package/test/logic/11.3-mismatched-collation-join.sqllogic +74 -0
  190. package/test/logic/11.4-hash-join-side-swap.sqllogic +118 -0
  191. package/test/logic/110-scan-emitter-mutation-kills.sqllogic +121 -0
  192. package/test/logic/110.1-scan-emitter-index-scan-kills.sqllogic +47 -0
  193. package/test/logic/12-empty-primary-key.sqllogic +124 -0
  194. package/test/logic/12-join_padding_order.sqllogic +58 -0
  195. package/test/logic/12.1-orderby-index-consumption.sqllogic +40 -0
  196. package/test/logic/13-cte.sqllogic +137 -0
  197. package/test/logic/13.1-cte-multiple-recursive.sqllogic +104 -0
  198. package/test/logic/13.10-cte-qualified-name.sqllogic +136 -0
  199. package/test/logic/13.11-unreferenced-dml-cte.sqllogic +414 -0
  200. package/test/logic/13.12-nested-dml-cte-rejected.sqllogic +280 -0
  201. package/test/logic/13.2-cte-bind-params.sqllogic +98 -0
  202. package/test/logic/13.3-cte-edge-cases.sqllogic +120 -0
  203. package/test/logic/13.4-cte-extras.sqllogic +98 -0
  204. package/test/logic/13.5-cte-join-order.sqllogic +166 -0
  205. package/test/logic/13.6-cte-dml-runs-once.sqllogic +318 -0
  206. package/test/logic/13.7-cte-sibling-visibility.sqllogic +227 -0
  207. package/test/logic/13.8-insert-with-clause-visibility.sqllogic +254 -0
  208. package/test/logic/13.9-schema-authored-cte-isolation.sqllogic +328 -0
  209. package/test/logic/13.9.1-schema-authored-schema-path-isolation.sqllogic +408 -0
  210. package/test/logic/14-utilities.sqllogic +168 -0
  211. package/test/logic/15-timespan.sqllogic +279 -0
  212. package/test/logic/15.1-semantic-ordering.sqllogic +960 -0
  213. package/test/logic/15.1.1-json-check-coercion.sqllogic +178 -0
  214. package/test/logic/15.1.2-semantic-ordering-index.sqllogic +41 -0
  215. package/test/logic/15.1.3-declared-form-write-contract.sqllogic +519 -0
  216. package/test/logic/16-epoch.sqllogic +187 -0
  217. package/test/logic/17-weekday-modifier.sqllogic +78 -0
  218. package/test/logic/18-json-string-escapes.sqllogic +86 -0
  219. package/test/logic/18-strftime-extended-formats.sqllogic +68 -0
  220. package/test/logic/20-empty-single-row.sqllogic +261 -0
  221. package/test/logic/21-null-edge-cases.sqllogic +227 -0
  222. package/test/logic/21.1-where-null-comparisons.sqllogic +151 -0
  223. package/test/logic/21.1.1-where-null-comparisons-secondary-index.sqllogic +35 -0
  224. package/test/logic/21.2-between-edges.sqllogic +157 -0
  225. package/test/logic/22-boundary-values.sqllogic +175 -0
  226. package/test/logic/23-self-joins-duplicates.sqllogic +74 -0
  227. package/test/logic/24-builtin-branches.sqllogic +611 -0
  228. package/test/logic/24.1-substr-extras.sqllogic +75 -0
  229. package/test/logic/24.2-random-extras.sqllogic +39 -0
  230. package/test/logic/25-aggregate-edge-cases.sqllogic +289 -0
  231. package/test/logic/25.1-nested-aggregates.sqllogic +74 -0
  232. package/test/logic/25.2-having-edge-cases.sqllogic +318 -0
  233. package/test/logic/25.3-aggregate-isnull-empty.sqllogic +16 -0
  234. package/test/logic/25.4-groupby-fd-constant-empty.sqllogic +55 -0
  235. package/test/logic/25.5-ungrouped-aggregate-column-free-select-item.sqllogic +196 -0
  236. package/test/logic/26-join-edge-cases.sqllogic +333 -0
  237. package/test/logic/26.1-left-join-isnull-on-notnull.sqllogic +26 -0
  238. package/test/logic/26.2-left-join-on-vs-where.sqllogic +67 -0
  239. package/test/logic/26.3-join-on-constants.sqllogic +61 -0
  240. package/test/logic/26.4-join-on-is-null-nullable.sqllogic +44 -0
  241. package/test/logic/27-window-edge-cases.sqllogic +206 -0
  242. package/test/logic/27.1-window-groups-frame.sqllogic +13 -0
  243. package/test/logic/27.2-window-exclude.sqllogic +13 -0
  244. package/test/logic/27.3-window-json-aggregation.sqllogic +66 -0
  245. package/test/logic/28-set-ops-sort-edge-cases.sqllogic +444 -0
  246. package/test/logic/28.1-compound-limit-offset.sqllogic +143 -0
  247. package/test/logic/28.2-orderby-expression-extras.sqllogic +698 -0
  248. package/test/logic/28.2-set-op-branch-types.sqllogic +217 -0
  249. package/test/logic/29-constraint-edge-cases.sqllogic +442 -0
  250. package/test/logic/29.1-column-level-conflict-clause.sqllogic +171 -0
  251. package/test/logic/29.2-table-level-pk-conflict-clause.sqllogic +113 -0
  252. package/test/logic/40-constraints.sqllogic +418 -0
  253. package/test/logic/40.1-pk-desc-direction.sqllogic +76 -0
  254. package/test/logic/40.2-check-extras.sqllogic +683 -0
  255. package/test/logic/41-alter-table.sqllogic +191 -0
  256. package/test/logic/41-fk-cascade-conflict-and-self-ref.sqllogic +263 -0
  257. package/test/logic/41-fk-cross-schema.sqllogic +155 -0
  258. package/test/logic/41-fk-extended-targets.sqllogic +173 -0
  259. package/test/logic/41-foreign-keys.sqllogic +1001 -0
  260. package/test/logic/41-generated-column-errors.sqllogic +225 -0
  261. package/test/logic/41-generated-column-extras.sqllogic +598 -0
  262. package/test/logic/41-generated-column-scope.sqllogic +329 -0
  263. package/test/logic/41-generated-columns.sqllogic +361 -0
  264. package/test/logic/41.1-alter-pk.sqllogic +230 -0
  265. package/test/logic/41.1-fk-collation-conflict.sqllogic +277 -0
  266. package/test/logic/41.10-alter-drop-column-foreign-key.sqllogic +274 -0
  267. package/test/logic/41.10.1-alter-drop-column-unique-index.sqllogic +169 -0
  268. package/test/logic/41.10.2-alter-drop-column-check-and-assertion.sqllogic +1616 -0
  269. package/test/logic/41.10.3-alter-drop-column-referencing-fk.sqllogic +292 -0
  270. package/test/logic/41.10.4-drop-object-expression-dependents.sqllogic +602 -0
  271. package/test/logic/41.11-deferred-fk-with-rename.sqllogic +232 -0
  272. package/test/logic/41.12-alter-default-coercion.sqllogic +200 -0
  273. package/test/logic/41.13-alter-add-column-generated-backfill.sqllogic +566 -0
  274. package/test/logic/41.14-alter-add-column-subquery-backfill.sqllogic +129 -0
  275. package/test/logic/41.15-alter-column-default-type-check.sqllogic +178 -0
  276. package/test/logic/41.16-fk-unenforceable.sqllogic +364 -0
  277. package/test/logic/41.2-alter-column.sqllogic +382 -0
  278. package/test/logic/41.2.1-alter-column-retype-deleted-row-memory.sqllogic +32 -0
  279. package/test/logic/41.2.2-alter-column-retype-index-rebuild.sqllogic +77 -0
  280. package/test/logic/41.2.3-alter-column-set-not-null-pk-backfill.sqllogic +310 -0
  281. package/test/logic/41.2.3.1-alter-column-set-not-null-pk-backfill-secondary-index.sqllogic +37 -0
  282. package/test/logic/41.3-alter-add-column-unique.sqllogic +184 -0
  283. package/test/logic/41.3-alter-rename-propagation.sqllogic +2352 -0
  284. package/test/logic/41.3.1-alter-rename-index-propagation.sqllogic +94 -0
  285. package/test/logic/41.4-alter-add-column-constraints.sqllogic +855 -0
  286. package/test/logic/41.5-alter-misc.sqllogic +48 -0
  287. package/test/logic/41.5-cross-schema-foreign-keys.sqllogic +253 -0
  288. package/test/logic/41.6-alter-drop-rename-constraint.sqllogic +354 -0
  289. package/test/logic/41.6.1-alter-drop-constraint-index-derived.sqllogic +22 -0
  290. package/test/logic/41.7-alter-column-collate.sqllogic +105 -0
  291. package/test/logic/41.7.1-alter-column-collate-unique.sqllogic +45 -0
  292. package/test/logic/41.7.2-alter-column-collate-unique-store.sqllogic +208 -0
  293. package/test/logic/41.7.2.1-alter-column-collate-unique-index-store.sqllogic +114 -0
  294. package/test/logic/41.7.3-alter-column-retype-unique.sqllogic +293 -0
  295. package/test/logic/41.7.3.1-alter-column-retype-staged-rows-memory.sqllogic +41 -0
  296. package/test/logic/41.7.3.2-alter-column-retype-unique-index.sqllogic +72 -0
  297. package/test/logic/41.7.4-alter-column-retype-semantic-memory.sqllogic +445 -0
  298. package/test/logic/41.7.4.1-alter-column-retype-semantic-index-memory.sqllogic +91 -0
  299. package/test/logic/41.7.5-alter-column-collate-pk-staged-delete-memory.sqllogic +106 -0
  300. package/test/logic/41.8-alter-add-constraint-unique-fk.sqllogic +275 -0
  301. package/test/logic/41.8-alter-savepoint-staged-rows.sqllogic +443 -0
  302. package/test/logic/41.8.1-alter-rename-savepoint-staged-rows.sqllogic +408 -0
  303. package/test/logic/41.9-alter-constraint-body-change.sqllogic +180 -0
  304. package/test/logic/41.9-fk-restrict-batched.sqllogic +290 -0
  305. package/test/logic/42-committed-snapshot.sqllogic +254 -0
  306. package/test/logic/42-returning.sqllogic +159 -0
  307. package/test/logic/42.1-returning-extras.sqllogic +232 -0
  308. package/test/logic/42.2-returning-stored-row.sqllogic +165 -0
  309. package/test/logic/43-default-nullability.sqllogic +131 -0
  310. package/test/logic/43-transition-constraints.sqllogic +341 -0
  311. package/test/logic/43.1-default-collation.sqllogic +140 -0
  312. package/test/logic/43.1-notnull-or-conflict.sqllogic +79 -0
  313. package/test/logic/43.2-deferred-check-new-on-delete.sqllogic +95 -0
  314. package/test/logic/43.3-nullable-primary-key.sqllogic +587 -0
  315. package/test/logic/44-determinism-validation.sqllogic +164 -0
  316. package/test/logic/44-orthogonality.sqllogic +146 -0
  317. package/test/logic/44.1-nondeterministic-schema.sqllogic +316 -0
  318. package/test/logic/45-udf-determinism.sqllogic +39 -0
  319. package/test/logic/46-mutation-context.sqllogic +595 -0
  320. package/test/logic/47-upsert.sqllogic +255 -0
  321. package/test/logic/47.1-upsert-conflict-targets.sqllogic +169 -0
  322. package/test/logic/47.2-replace-and-or-clauses.sqllogic +164 -0
  323. package/test/logic/47.2.1-pk-replace-secondary-unique.sqllogic +374 -0
  324. package/test/logic/47.3-engine-or-fail.sqllogic +122 -0
  325. package/test/logic/47.3-upsert-conflict-target-collation.sqllogic +139 -0
  326. package/test/logic/47.3.1-upsert-conflict-index-derived-collation.sqllogic +25 -0
  327. package/test/logic/47.4-upsert-conflict-target-affinity.sqllogic +59 -0
  328. package/test/logic/47.5-upsert-do-update-constraints.sqllogic +440 -0
  329. package/test/logic/47.6-upsert-do-update-decomposed.sqllogic +100 -0
  330. package/test/logic/49-reference-graph.sqllogic +84 -0
  331. package/test/logic/50-declarative-schema.sqllogic +2550 -0
  332. package/test/logic/50-metadata-tags.sqllogic +1033 -0
  333. package/test/logic/50.0.1-metadata-tags-index.sqllogic +141 -0
  334. package/test/logic/50.1-declare-schema-pk.sqllogic +150 -0
  335. package/test/logic/50.2-declare-schema-renames.sqllogic +949 -0
  336. package/test/logic/50.2.1-declare-schema-index-renames.sqllogic +173 -0
  337. package/test/logic/50.3-declare-schema-duplicate-names.sqllogic +280 -0
  338. package/test/logic/50.4-declare-schema-apply-restore.sqllogic +192 -0
  339. package/test/logic/51-lens-foundation.sqllogic +147 -0
  340. package/test/logic/51-materialized-views.sqllogic +376 -0
  341. package/test/logic/51.5-materialized-views-coarsened-key.sqllogic +198 -0
  342. package/test/logic/51.7-maintained-table-attach-detach.sqllogic +468 -0
  343. package/test/logic/51.8-maintained-table-declared-constraints.sqllogic +440 -0
  344. package/test/logic/51.85-maintained-table-parent-fk.sqllogic +103 -0
  345. package/test/logic/51.9-maintained-table-secondary-unique.sqllogic +237 -0
  346. package/test/logic/51.9.1-maintained-table-partial-unique.sqllogic +36 -0
  347. package/test/logic/52-lens-overrides.sqllogic +566 -0
  348. package/test/logic/53-materialized-views-rowtime.sqllogic +1601 -0
  349. package/test/logic/53-reserved-tags.sqllogic +252 -0
  350. package/test/logic/53.1-materialized-view-write-through.sqllogic +329 -0
  351. package/test/logic/53.2-materialized-view-rename-propagation.sqllogic +483 -0
  352. package/test/logic/53.3-materialized-view-constraint-only-ddl.sqllogic +409 -0
  353. package/test/logic/53.3.1-materialized-view-index-ddl-irrelevance.sqllogic +29 -0
  354. package/test/logic/53.4-materialized-view-structural-alter-restore.sqllogic +436 -0
  355. package/test/logic/54-covering-mv-enforcement.sqllogic +314 -0
  356. package/test/logic/54.1-covering-mv-partial-enforcement.sqllogic +34 -0
  357. package/test/logic/55-internal-eviction-reporting.sqllogic +89 -0
  358. package/test/logic/55-lens-prover.sqllogic +202 -0
  359. package/test/logic/55.1-eviction-restrict-fk.sqllogic +148 -0
  360. package/test/logic/55.5-lens-authored-inverse.sqllogic +1468 -0
  361. package/test/logic/80-grow-retrieve-pass.sqllogic +79 -0
  362. package/test/logic/81-quickpick.sqllogic +24 -0
  363. package/test/logic/82-bloom-join.sqllogic +185 -0
  364. package/test/logic/83-merge-join.sqllogic +280 -0
  365. package/test/logic/84-asof-scan.sqllogic +238 -0
  366. package/test/logic/84-key-cardinality.sqllogic +50 -0
  367. package/test/logic/85-relational-const-folding.sqllogic +51 -0
  368. package/test/logic/86-scalar-cse.sqllogic +42 -0
  369. package/test/logic/90-error_paths.sqllogic +114 -0
  370. package/test/logic/90.1-parse-errors.sqllogic +77 -0
  371. package/test/logic/90.2-alter-table-errors.sqllogic +53 -0
  372. package/test/logic/90.2.1-alter-extra-errors.sqllogic +51 -0
  373. package/test/logic/90.3-expression-errors.sqllogic +43 -0
  374. package/test/logic/90.4-dml-errors.sqllogic +47 -0
  375. package/test/logic/90.5-unsupported-join-types.sqllogic +37 -0
  376. package/test/logic/90.5.1-right-full-join-read.sqllogic +157 -0
  377. package/test/logic/90.6-select-error-paths.sqllogic +57 -0
  378. package/test/logic/91-merge-join-edge-cases.sqllogic +159 -0
  379. package/test/logic/92-hash-aggregate-edge-cases.sqllogic +139 -0
  380. package/test/logic/93-ddl-view-edge-cases.sqllogic +104 -0
  381. package/test/logic/93.1-view-error-paths.sqllogic +74 -0
  382. package/test/logic/93.2-view-mutation-pending.sqllogic +108 -0
  383. package/test/logic/93.3-view-mutation-or-clause.sqllogic +60 -0
  384. package/test/logic/93.4-view-mutation.sqllogic +4881 -0
  385. package/test/logic/93.4.1-view-mutation-partial-unique-cardinality.sqllogic +31 -0
  386. package/test/logic/93.5-authored-inverse.sqllogic +415 -0
  387. package/test/logic/93.6-set-op-flagless-write.sqllogic +862 -0
  388. package/test/logic/94-tvf-edge-cases.sqllogic +58 -0
  389. package/test/logic/94.1-limit-offset-edge-cases.sqllogic +109 -0
  390. package/test/logic/94.2-limit-offset-extras.sqllogic +98 -0
  391. package/test/logic/95-assertions.sqllogic +1417 -0
  392. package/test/logic/96-subquery-edge-cases.sqllogic +276 -0
  393. package/test/logic/97-json-function-edge-cases.sqllogic +633 -0
  394. package/test/logic/97.1-json-blob-and-special-values.sqllogic +92 -0
  395. package/test/logic/98-temporal-edge-cases.sqllogic +515 -0
  396. package/test/logic/99-conversion-edge-cases.sqllogic +325 -0
  397. package/test/logic/99.0.1-schema-index-listing.sqllogic +21 -0
  398. package/test/logic/99.1-cast-syntax-extras.sqllogic +167 -0
  399. package/test/logic/drop-unique-index.sqllogic +88 -0
  400. package/test/logic/pushdown-test.sqllogic +36 -0
@@ -0,0 +1,1941 @@
1
+ -- Window Function Tests
2
+ -- Tests for SQL window functions with OVER clauses
3
+
4
+ -- Setup test data
5
+ CREATE TABLE sales (
6
+ id INTEGER PRIMARY KEY,
7
+ product TEXT,
8
+ region TEXT,
9
+ amount NUMERIC
10
+ );
11
+
12
+ INSERT INTO sales VALUES
13
+ (1, 'Widget', 'North', 100.50),
14
+ (2, 'Gadget', 'North', 75.25),
15
+ (3, 'Widget', 'South', 120.75),
16
+ (4, 'Gadget', 'South', 90.00),
17
+ (5, 'Widget', 'North', 85.50),
18
+ (6, 'Gadget', 'West', 110.25);
19
+
20
+ -- Basic ROW_NUMBER() without partitioning
21
+ SELECT id, product, ROW_NUMBER() OVER (ORDER BY id) as rn
22
+ FROM sales ORDER BY id;
23
+ → [{"id":1,"product":"Widget","rn":1},{"id":2,"product":"Gadget","rn":2},{"id":3,"product":"Widget","rn":3},{"id":4,"product":"Gadget","rn":4},{"id":5,"product":"Widget","rn":5},{"id":6,"product":"Gadget","rn":6}]
24
+
25
+ -- ROW_NUMBER() with PARTITION BY
26
+ SELECT product, region, amount, ROW_NUMBER() OVER (PARTITION BY product ORDER BY amount) as rn
27
+ FROM sales ORDER BY product, amount;
28
+ → [{"product":"Gadget","region":"North","amount":75.25,"rn":1},{"product":"Gadget","region":"South","amount":90.00,"rn":2},{"product":"Gadget","region":"West","amount":110.25,"rn":3},{"product":"Widget","region":"North","amount":85.50,"rn":1},{"product":"Widget","region":"North","amount":100.50,"rn":2},{"product":"Widget","region":"South","amount":120.75,"rn":3}]
29
+
30
+ -- Test RANK() function
31
+ SELECT product, amount, RANK() OVER (PARTITION BY product ORDER BY amount) as rnk
32
+ FROM sales ORDER BY product, amount;
33
+ → [{"product":"Gadget","amount":75.25,"rnk":1},{"product":"Gadget","amount":90.00,"rnk":2},{"product":"Gadget","amount":110.25,"rnk":3},{"product":"Widget","amount":85.50,"rnk":1},{"product":"Widget","amount":100.50,"rnk":2},{"product":"Widget","amount":120.75,"rnk":3}]
34
+
35
+ -- Test DENSE_RANK() function
36
+ SELECT product, amount, DENSE_RANK() OVER (PARTITION BY product ORDER BY amount) as dense_rnk
37
+ FROM sales ORDER BY product, amount;
38
+ → [{"product":"Gadget","amount":75.25,"dense_rnk":1},{"product":"Gadget","amount":90.00,"dense_rnk":2},{"product":"Gadget","amount":110.25,"dense_rnk":3},{"product":"Widget","amount":85.50,"dense_rnk":1},{"product":"Widget","amount":100.50,"dense_rnk":2},{"product":"Widget","amount":120.75,"dense_rnk":3}]
39
+
40
+ -- Test SUM() OVER for running totals
41
+ SELECT product, amount, SUM(amount) OVER (PARTITION BY product ORDER BY id ROWS UNBOUNDED PRECEDING) as running_sum
42
+ FROM sales ORDER BY product, id;
43
+ → [{"product":"Gadget","amount":75.25,"running_sum":75.25},{"product":"Gadget","amount":90.00,"running_sum":165.25},{"product":"Gadget","amount":110.25,"running_sum":275.50},{"product":"Widget","amount":100.50,"running_sum":100.50},{"product":"Widget","amount":120.75,"running_sum":221.25},{"product":"Widget","amount":85.50,"running_sum":306.75}]
44
+
45
+ -- Test COUNT() OVER for row counting
46
+ SELECT region, product, COUNT(*) OVER (PARTITION BY region ORDER BY id ROWS UNBOUNDED PRECEDING) as cumulative_count
47
+ FROM sales ORDER BY region, id;
48
+ → [{"region":"North","product":"Widget","cumulative_count":1},{"region":"North","product":"Gadget","cumulative_count":2},{"region":"North","product":"Widget","cumulative_count":3},{"region":"South","product":"Widget","cumulative_count":1},{"region":"South","product":"Gadget","cumulative_count":2},{"region":"West","product":"Gadget","cumulative_count":1}]
49
+
50
+ -- Test AVG() OVER for running averages
51
+ SELECT product, amount, AVG(amount) OVER (PARTITION BY product ORDER BY id ROWS UNBOUNDED PRECEDING) as running_avg
52
+ FROM sales ORDER BY product, id;
53
+ → [{"product":"Gadget","amount":75.25,"running_avg":75.25},{"product":"Gadget","amount":90.00,"running_avg":82.625},{"product":"Gadget","amount":110.25,"running_avg":91.83333333333333},{"product":"Widget","amount":100.50,"running_avg":100.50},{"product":"Widget","amount":120.75,"running_avg":110.625},{"product":"Widget","amount":85.50,"running_avg":102.25}]
54
+
55
+ -- Test multiple window functions in same query
56
+ SELECT
57
+ product,
58
+ region,
59
+ amount,
60
+ ROW_NUMBER() OVER (ORDER BY amount DESC) as overall_rank,
61
+ ROW_NUMBER() OVER (PARTITION BY product ORDER BY amount DESC) as product_rank,
62
+ SUM(amount) OVER (PARTITION BY product) as product_total
63
+ FROM sales ORDER BY amount DESC;
64
+ → [{"product":"Widget","region":"South","amount":120.75,"overall_rank":1,"product_rank":1,"product_total":306.75},{"product":"Gadget","region":"West","amount":110.25,"overall_rank":2,"product_rank":1,"product_total":275.50},{"product":"Widget","region":"North","amount":100.50,"overall_rank":3,"product_rank":2,"product_total":306.75},{"product":"Gadget","region":"South","amount":90.00,"overall_rank":4,"product_rank":2,"product_total":275.50},{"product":"Widget","region":"North","amount":85.50,"overall_rank":5,"product_rank":3,"product_total":306.75},{"product":"Gadget","region":"North","amount":75.25,"overall_rank":6,"product_rank":3,"product_total":275.50}]
65
+
66
+ -- Test window functions with different frame specifications
67
+ SELECT id, amount, SUM(amount) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) as windowed_sum
68
+ FROM sales ORDER BY id;
69
+ → [{"id":1,"amount":100.50,"windowed_sum":175.75},{"id":2,"amount":75.25,"windowed_sum":296.50},{"id":3,"amount":120.75,"windowed_sum":286.00},{"id":4,"amount":90.00,"windowed_sum":296.25},{"id":5,"amount":85.50,"windowed_sum":285.75},{"id":6,"amount":110.25,"windowed_sum":195.75}]
70
+
71
+ -- Test window frames with non-1 literal offsets
72
+ SELECT id, amount, SUM(amount) OVER (ORDER BY id ROWS BETWEEN 2 PRECEDING AND 1 FOLLOWING) as windowed_sum
73
+ FROM sales ORDER BY id;
74
+ → [{"id":1,"amount":100.50,"windowed_sum":175.75},{"id":2,"amount":75.25,"windowed_sum":296.50},{"id":3,"amount":120.75,"windowed_sum":386.50},{"id":4,"amount":90.00,"windowed_sum":371.50},{"id":5,"amount":85.50,"windowed_sum":406.50},{"id":6,"amount":110.25,"windowed_sum":285.75}]
75
+
76
+ -- Test multiple window functions with same OVER clause
77
+ SELECT product, amount,
78
+ ROW_NUMBER() OVER (PARTITION BY product ORDER BY amount) as rn,
79
+ RANK() OVER (PARTITION BY product ORDER BY amount) as rnk,
80
+ COUNT(*) OVER (PARTITION BY product ORDER BY amount ROWS UNBOUNDED PRECEDING) as cnt
81
+ FROM sales ORDER BY product, amount;
82
+ → [{"product":"Gadget","amount":75.25,"rn":1,"rnk":1,"cnt":1},{"product":"Gadget","amount":90.00,"rn":2,"rnk":2,"cnt":2},{"product":"Gadget","amount":110.25,"rn":3,"rnk":3,"cnt":3},{"product":"Widget","amount":85.50,"rn":1,"rnk":1,"cnt":1},{"product":"Widget","amount":100.50,"rn":2,"rnk":2,"cnt":2},{"product":"Widget","amount":120.75,"rn":3,"rnk":3,"cnt":3}]
83
+
84
+ -- Edge case: Window function with NULL handling
85
+ CREATE TABLE sales_with_nulls (
86
+ id INTEGER PRIMARY KEY,
87
+ product TEXT,
88
+ amount NUMERIC NULL
89
+ );
90
+
91
+ INSERT INTO sales_with_nulls VALUES
92
+ (1, 'Widget', 100.00),
93
+ (2, 'Widget', NULL),
94
+ (3, 'Gadget', 75.00),
95
+ (4, 'Widget', 120.00),
96
+ (5, 'Gadget', NULL);
97
+
98
+ SELECT product, amount, ROW_NUMBER() OVER (PARTITION BY product ORDER BY amount NULLS LAST) as rn
99
+ FROM sales_with_nulls ORDER BY product, COALESCE(amount, 999999);
100
+ → [{"product":"Gadget","amount":75.00,"rn":1},{"product":"Gadget","amount":null,"rn":2},{"product":"Widget","amount":100.00,"rn":1},{"product":"Widget","amount":120.00,"rn":2},{"product":"Widget","amount":null,"rn":3}]
101
+
102
+ -- Test aggregate window functions with NULLs
103
+ SELECT product, amount,
104
+ COUNT(*) OVER (PARTITION BY product) as total_rows,
105
+ COUNT(amount) OVER (PARTITION BY product) as non_null_amounts,
106
+ SUM(amount) OVER (PARTITION BY product) as total_amount
107
+ FROM sales_with_nulls ORDER BY product, COALESCE(amount, 999999);
108
+ → [{"product":"Gadget","amount":75.00,"total_rows":2,"non_null_amounts":1,"total_amount":75.00},{"product":"Gadget","amount":null,"total_rows":2,"non_null_amounts":1,"total_amount":75.00},{"product":"Widget","amount":100.00,"total_rows":3,"non_null_amounts":2,"total_amount":220.00},{"product":"Widget","amount":120.00,"total_rows":3,"non_null_amounts":2,"total_amount":220.00},{"product":"Widget","amount":null,"total_rows":3,"non_null_amounts":2,"total_amount":220.00}]
109
+
110
+ DROP TABLE sales;
111
+ DROP TABLE sales_with_nulls;
112
+
113
+ -- Window Function Frame Equivalence Tests
114
+ CREATE TABLE frame_test (id INTEGER PRIMARY KEY, value INTEGER, grp INTEGER);
115
+ INSERT INTO frame_test VALUES
116
+ (1, 10, 1), (2, 20, 1), (3, 30, 1),
117
+ (4, 15, 2), (5, 25, 2), (6, 35, 2);
118
+
119
+ -- Test equivalent window frame specifications produce same results
120
+ SELECT value, grp,
121
+ SUM(value) OVER (PARTITION BY grp ORDER BY value ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as running_sum
122
+ FROM frame_test ORDER BY grp, value;
123
+ → [{"value":10,"grp":1,"running_sum":10},{"value":20,"grp":1,"running_sum":30},{"value":30,"grp":1,"running_sum":60},{"value":15,"grp":2,"running_sum":15},{"value":25,"grp":2,"running_sum":40},{"value":35,"grp":2,"running_sum":75}]
124
+
125
+ SELECT value, grp,
126
+ SUM(value) OVER (PARTITION BY grp ORDER BY value ROWS UNBOUNDED PRECEDING) as running_sum
127
+ FROM frame_test ORDER BY grp, value;
128
+ → [{"value":10,"grp":1,"running_sum":10},{"value":20,"grp":1,"running_sum":30},{"value":30,"grp":1,"running_sum":60},{"value":15,"grp":2,"running_sum":15},{"value":25,"grp":2,"running_sum":40},{"value":35,"grp":2,"running_sum":75}]
129
+
130
+ -- Test different but equivalent frame specifications for COUNT
131
+ SELECT value, grp,
132
+ COUNT(*) OVER (PARTITION BY grp ORDER BY value ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as running_count
133
+ FROM frame_test ORDER BY grp, value;
134
+ → [{"value":10,"grp":1,"running_count":1},{"value":20,"grp":1,"running_count":2},{"value":30,"grp":1,"running_count":3},{"value":15,"grp":2,"running_count":1},{"value":25,"grp":2,"running_count":2},{"value":35,"grp":2,"running_count":3}]
135
+
136
+ SELECT value, grp,
137
+ COUNT(*) OVER (PARTITION BY grp ORDER BY value ROWS UNBOUNDED PRECEDING) as running_count
138
+ FROM frame_test ORDER BY grp, value;
139
+ → [{"value":10,"grp":1,"running_count":1},{"value":20,"grp":1,"running_count":2},{"value":30,"grp":1,"running_count":3},{"value":15,"grp":2,"running_count":1},{"value":25,"grp":2,"running_count":2},{"value":35,"grp":2,"running_count":3}]
140
+
141
+ DROP TABLE frame_test;
142
+
143
+ -- =============================================================================
144
+ -- Window PARTITION BY with collation tests
145
+ -- =============================================================================
146
+
147
+ -- NOCASE collation: PARTITION BY should group case-insensitively
148
+ CREATE TABLE win_nocase (
149
+ id INTEGER PRIMARY KEY,
150
+ category TEXT COLLATE NOCASE,
151
+ val INTEGER
152
+ );
153
+
154
+ INSERT INTO win_nocase VALUES
155
+ (1, 'Alpha', 10),
156
+ (2, 'ALPHA', 20),
157
+ (3, 'alpha', 30),
158
+ (4, 'Beta', 40),
159
+ (5, 'BETA', 50);
160
+
161
+ -- All 'alpha' variants should be in the same partition (3 rows), 'beta' variants together (2 rows)
162
+ SELECT id, category, val,
163
+ ROW_NUMBER() OVER (PARTITION BY category ORDER BY id) as rn,
164
+ COUNT(*) OVER (PARTITION BY category) as partition_size
165
+ FROM win_nocase ORDER BY id;
166
+ → [{"id":1,"category":"Alpha","val":10,"rn":1,"partition_size":3},{"id":2,"category":"ALPHA","val":20,"rn":2,"partition_size":3},{"id":3,"category":"alpha","val":30,"rn":3,"partition_size":3},{"id":4,"category":"Beta","val":40,"rn":1,"partition_size":2},{"id":5,"category":"BETA","val":50,"rn":2,"partition_size":2}]
167
+
168
+ -- SUM within NOCASE partition
169
+ SELECT id, category, val,
170
+ SUM(val) OVER (PARTITION BY category) as partition_total
171
+ FROM win_nocase ORDER BY id;
172
+ → [{"id":1,"category":"Alpha","val":10,"partition_total":60},{"id":2,"category":"ALPHA","val":20,"partition_total":60},{"id":3,"category":"alpha","val":30,"partition_total":60},{"id":4,"category":"Beta","val":40,"partition_total":90},{"id":5,"category":"BETA","val":50,"partition_total":90}]
173
+
174
+ -- DENSE_RANK with NOCASE ORDER BY
175
+ CREATE TABLE win_rank_nocase (
176
+ id INTEGER PRIMARY KEY,
177
+ name TEXT COLLATE NOCASE,
178
+ score INTEGER
179
+ );
180
+
181
+ INSERT INTO win_rank_nocase VALUES
182
+ (1, 'Alice', 100),
183
+ (2, 'ALICE', 100),
184
+ (3, 'Bob', 90),
185
+ (4, 'Charlie', 80);
186
+
187
+ -- 'Alice' and 'ALICE' should be considered equal for ranking under NOCASE
188
+ SELECT id, name, score,
189
+ DENSE_RANK() OVER (ORDER BY name) as drnk
190
+ FROM win_rank_nocase ORDER BY id;
191
+ → [{"id":1,"name":"Alice","score":100,"drnk":1},{"id":2,"name":"ALICE","score":100,"drnk":1},{"id":3,"name":"Bob","score":90,"drnk":2},{"id":4,"name":"Charlie","score":80,"drnk":3}]
192
+
193
+ -- RANK with NOCASE ORDER BY
194
+ SELECT id, name, score,
195
+ RANK() OVER (ORDER BY name) as rnk
196
+ FROM win_rank_nocase ORDER BY id;
197
+ → [{"id":1,"name":"Alice","score":100,"rnk":1},{"id":2,"name":"ALICE","score":100,"rnk":1},{"id":3,"name":"Bob","score":90,"rnk":3},{"id":4,"name":"Charlie","score":80,"rnk":4}]
198
+
199
+ -- NULL in PARTITION BY: NULLs should group together
200
+ CREATE TABLE win_null_part (
201
+ id INTEGER PRIMARY KEY,
202
+ grp TEXT NULL,
203
+ val INTEGER
204
+ );
205
+
206
+ INSERT INTO win_null_part VALUES
207
+ (1, NULL, 10),
208
+ (2, NULL, 20),
209
+ (3, 'A', 30),
210
+ (4, 'A', 40);
211
+
212
+ SELECT id, grp, val,
213
+ COUNT(*) OVER (PARTITION BY grp) as partition_size,
214
+ SUM(val) OVER (PARTITION BY grp) as partition_total
215
+ FROM win_null_part ORDER BY id;
216
+ → [{"id":1,"grp":null,"val":10,"partition_size":2,"partition_total":30},{"id":2,"grp":null,"val":20,"partition_size":2,"partition_total":30},{"id":3,"grp":"A","val":30,"partition_size":2,"partition_total":70},{"id":4,"grp":"A","val":40,"partition_size":2,"partition_total":70}]
217
+
218
+ DROP TABLE win_nocase;
219
+ DROP TABLE win_rank_nocase;
220
+ DROP TABLE win_null_part;
221
+
222
+ -- =============================================================================
223
+ -- LAG / LEAD tests
224
+ -- =============================================================================
225
+
226
+ CREATE TABLE nav_test (id INTEGER PRIMARY KEY, val INTEGER, grp TEXT);
227
+ INSERT INTO nav_test VALUES
228
+ (1, 10, 'A'), (2, 20, 'A'), (3, 30, 'A'),
229
+ (4, 100, 'B'), (5, 200, 'B');
230
+
231
+ -- Basic LAG (default offset = 1)
232
+ SELECT id, val, LAG(val) OVER (ORDER BY id) as prev_val
233
+ FROM nav_test ORDER BY id;
234
+ → [{"id":1,"val":10,"prev_val":null},{"id":2,"val":20,"prev_val":10},{"id":3,"val":30,"prev_val":20},{"id":4,"val":100,"prev_val":30},{"id":5,"val":200,"prev_val":100}]
235
+
236
+ -- Basic LEAD (default offset = 1)
237
+ SELECT id, val, LEAD(val) OVER (ORDER BY id) as next_val
238
+ FROM nav_test ORDER BY id;
239
+ → [{"id":1,"val":10,"next_val":20},{"id":2,"val":20,"next_val":30},{"id":3,"val":30,"next_val":100},{"id":4,"val":100,"next_val":200},{"id":5,"val":200,"next_val":null}]
240
+
241
+ -- LAG with explicit offset
242
+ SELECT id, val, LAG(val, 2) OVER (ORDER BY id) as prev2_val
243
+ FROM nav_test ORDER BY id;
244
+ → [{"id":1,"val":10,"prev2_val":null},{"id":2,"val":20,"prev2_val":null},{"id":3,"val":30,"prev2_val":10},{"id":4,"val":100,"prev2_val":20},{"id":5,"val":200,"prev2_val":30}]
245
+
246
+ -- LEAD with explicit offset and default
247
+ SELECT id, val, LEAD(val, 1, -1) OVER (ORDER BY id) as next_val
248
+ FROM nav_test ORDER BY id;
249
+ → [{"id":1,"val":10,"next_val":20},{"id":2,"val":20,"next_val":30},{"id":3,"val":30,"next_val":100},{"id":4,"val":100,"next_val":200},{"id":5,"val":200,"next_val":-1}]
250
+
251
+ -- LAG with default value
252
+ SELECT id, val, LAG(val, 1, 0) OVER (ORDER BY id) as prev_val
253
+ FROM nav_test ORDER BY id;
254
+ → [{"id":1,"val":10,"prev_val":0},{"id":2,"val":20,"prev_val":10},{"id":3,"val":30,"prev_val":20},{"id":4,"val":100,"prev_val":30},{"id":5,"val":200,"prev_val":100}]
255
+
256
+ -- LAG/LEAD with PARTITION BY (should not cross partition boundaries)
257
+ SELECT id, val, grp,
258
+ LAG(val) OVER (PARTITION BY grp ORDER BY id) as prev_val,
259
+ LEAD(val) OVER (PARTITION BY grp ORDER BY id) as next_val
260
+ FROM nav_test ORDER BY id;
261
+ → [{"id":1,"val":10,"grp":"A","prev_val":null,"next_val":20},{"id":2,"val":20,"grp":"A","prev_val":10,"next_val":30},{"id":3,"val":30,"grp":"A","prev_val":20,"next_val":null},{"id":4,"val":100,"grp":"B","prev_val":null,"next_val":200},{"id":5,"val":200,"grp":"B","prev_val":100,"next_val":null}]
262
+
263
+ DROP TABLE nav_test;
264
+
265
+ -- =============================================================================
266
+ -- FIRST_VALUE / LAST_VALUE tests
267
+ -- =============================================================================
268
+
269
+ CREATE TABLE fv_test (id INTEGER PRIMARY KEY, val INTEGER, grp TEXT);
270
+ INSERT INTO fv_test VALUES
271
+ (1, 10, 'A'), (2, 20, 'A'), (3, 30, 'A'),
272
+ (4, 100, 'B'), (5, 200, 'B');
273
+
274
+ -- FIRST_VALUE over entire partition
275
+ SELECT id, val, grp,
276
+ FIRST_VALUE(val) OVER (PARTITION BY grp ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as first_val
277
+ FROM fv_test ORDER BY id;
278
+ → [{"id":1,"val":10,"grp":"A","first_val":10},{"id":2,"val":20,"grp":"A","first_val":10},{"id":3,"val":30,"grp":"A","first_val":10},{"id":4,"val":100,"grp":"B","first_val":100},{"id":5,"val":200,"grp":"B","first_val":100}]
279
+
280
+ -- LAST_VALUE over entire partition (must use explicit frame)
281
+ SELECT id, val, grp,
282
+ LAST_VALUE(val) OVER (PARTITION BY grp ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as last_val
283
+ FROM fv_test ORDER BY id;
284
+ → [{"id":1,"val":10,"grp":"A","last_val":30},{"id":2,"val":20,"grp":"A","last_val":30},{"id":3,"val":30,"grp":"A","last_val":30},{"id":4,"val":100,"grp":"B","last_val":200},{"id":5,"val":200,"grp":"B","last_val":200}]
285
+
286
+ -- LAST_VALUE with default frame (ORDER BY present, no explicit frame = up to current row)
287
+ SELECT id, val,
288
+ LAST_VALUE(val) OVER (ORDER BY id) as last_val
289
+ FROM fv_test ORDER BY id;
290
+ → [{"id":1,"val":10,"last_val":10},{"id":2,"val":20,"last_val":20},{"id":3,"val":30,"last_val":30},{"id":4,"val":100,"last_val":100},{"id":5,"val":200,"last_val":200}]
291
+
292
+ -- FIRST_VALUE without ORDER BY (entire partition)
293
+ SELECT id, val, grp,
294
+ FIRST_VALUE(val) OVER (PARTITION BY grp) as first_val
295
+ FROM fv_test ORDER BY id;
296
+ → [{"id":1,"val":10,"grp":"A","first_val":10},{"id":2,"val":20,"grp":"A","first_val":10},{"id":3,"val":30,"grp":"A","first_val":10},{"id":4,"val":100,"grp":"B","first_val":100},{"id":5,"val":200,"grp":"B","first_val":100}]
297
+
298
+ DROP TABLE fv_test;
299
+
300
+ -- =============================================================================
301
+ -- PERCENT_RANK / CUME_DIST tests
302
+ -- =============================================================================
303
+
304
+ CREATE TABLE stat_test (id INTEGER PRIMARY KEY, val INTEGER);
305
+ INSERT INTO stat_test VALUES (1, 10), (2, 20), (3, 20), (4, 30), (5, 40);
306
+
307
+ -- PERCENT_RANK: (rank - 1) / (rows - 1)
308
+ SELECT id, val, PERCENT_RANK() OVER (ORDER BY val) as pct_rank
309
+ FROM stat_test ORDER BY id;
310
+ → [{"id":1,"val":10,"pct_rank":0},{"id":2,"val":20,"pct_rank":0.25},{"id":3,"val":20,"pct_rank":0.25},{"id":4,"val":30,"pct_rank":0.75},{"id":5,"val":40,"pct_rank":1}]
311
+
312
+ -- CUME_DIST: (count of rows <= current) / total
313
+ SELECT id, val, CUME_DIST() OVER (ORDER BY val) as cume
314
+ FROM stat_test ORDER BY id;
315
+ → [{"id":1,"val":10,"cume":0.2},{"id":2,"val":20,"cume":0.6},{"id":3,"val":20,"cume":0.6},{"id":4,"val":30,"cume":0.8},{"id":5,"val":40,"cume":1}]
316
+
317
+ -- Single-row partition: PERCENT_RANK = 0, CUME_DIST = 1
318
+ SELECT val, PERCENT_RANK() OVER (PARTITION BY val ORDER BY val) as pct_rank,
319
+ CUME_DIST() OVER (PARTITION BY val ORDER BY val) as cume
320
+ FROM stat_test WHERE val = 10;
321
+ → [{"val":10,"pct_rank":0,"cume":1}]
322
+
323
+ DROP TABLE stat_test;
324
+
325
+ -- =============================================================================
326
+ -- NTILE tests
327
+ -- =============================================================================
328
+
329
+ CREATE TABLE ntile_test (id INTEGER PRIMARY KEY, val INTEGER);
330
+ INSERT INTO ntile_test VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50);
331
+
332
+ -- NTILE(2): 3 rows in bucket 1, 2 rows in bucket 2
333
+ SELECT id, val, NTILE(2) OVER (ORDER BY id) as bucket
334
+ FROM ntile_test ORDER BY id;
335
+ → [{"id":1,"val":10,"bucket":1},{"id":2,"val":20,"bucket":1},{"id":3,"val":30,"bucket":1},{"id":4,"val":40,"bucket":2},{"id":5,"val":50,"bucket":2}]
336
+
337
+ -- NTILE(3): 2 rows in bucket 1, 2 in bucket 2, 1 in bucket 3
338
+ SELECT id, val, NTILE(3) OVER (ORDER BY id) as bucket
339
+ FROM ntile_test ORDER BY id;
340
+ → [{"id":1,"val":10,"bucket":1},{"id":2,"val":20,"bucket":1},{"id":3,"val":30,"bucket":2},{"id":4,"val":40,"bucket":2},{"id":5,"val":50,"bucket":3}]
341
+
342
+ -- NTILE(5): 1 row per bucket
343
+ SELECT id, val, NTILE(5) OVER (ORDER BY id) as bucket
344
+ FROM ntile_test ORDER BY id;
345
+ → [{"id":1,"val":10,"bucket":1},{"id":2,"val":20,"bucket":2},{"id":3,"val":30,"bucket":3},{"id":4,"val":40,"bucket":4},{"id":5,"val":50,"bucket":5}]
346
+
347
+ DROP TABLE ntile_test;
348
+
349
+ -- =============================================================================
350
+ -- RANGE BETWEEN tests (value-based window frames)
351
+ -- =============================================================================
352
+
353
+ CREATE TABLE range_test (id INTEGER PRIMARY KEY, val INTEGER, grp TEXT);
354
+ INSERT INTO range_test VALUES
355
+ (1, 10, 'A'), (2, 20, 'A'), (3, 20, 'A'), (4, 30, 'A'), (5, 50, 'A');
356
+
357
+ -- RANGE CURRENT ROW: includes all peer rows (same ORDER BY value)
358
+ SELECT id, val,
359
+ COUNT(*) OVER (ORDER BY val RANGE CURRENT ROW) as peer_count
360
+ FROM range_test ORDER BY id;
361
+ → [{"id":1,"val":10,"peer_count":1},{"id":2,"val":20,"peer_count":2},{"id":3,"val":20,"peer_count":2},{"id":4,"val":30,"peer_count":1},{"id":5,"val":50,"peer_count":1}]
362
+
363
+ -- RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW (default frame with ORDER BY)
364
+ -- Should include all peers at current value
365
+ SELECT id, val,
366
+ SUM(val) OVER (ORDER BY val RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as running_sum
367
+ FROM range_test ORDER BY id;
368
+ → [{"id":1,"val":10,"running_sum":10},{"id":2,"val":20,"running_sum":50},{"id":3,"val":20,"running_sum":50},{"id":4,"val":30,"running_sum":80},{"id":5,"val":50,"running_sum":130}]
369
+
370
+ -- Default frame (ORDER BY present) should be RANGE UNBOUNDED PRECEDING AND CURRENT ROW
371
+ -- including peers
372
+ SELECT id, val,
373
+ SUM(val) OVER (ORDER BY val) as running_sum
374
+ FROM range_test ORDER BY id;
375
+ → [{"id":1,"val":10,"running_sum":10},{"id":2,"val":20,"running_sum":50},{"id":3,"val":20,"running_sum":50},{"id":4,"val":30,"running_sum":80},{"id":5,"val":50,"running_sum":130}]
376
+
377
+ -- RANGE N PRECEDING: value-based offset
378
+ SELECT id, val,
379
+ SUM(val) OVER (ORDER BY val RANGE BETWEEN 10 PRECEDING AND CURRENT ROW) as range_sum
380
+ FROM range_test ORDER BY id;
381
+ → [{"id":1,"val":10,"range_sum":10},{"id":2,"val":20,"range_sum":50},{"id":3,"val":20,"range_sum":50},{"id":4,"val":30,"range_sum":70},{"id":5,"val":50,"range_sum":50}]
382
+
383
+ -- RANGE BETWEEN N PRECEDING AND N FOLLOWING
384
+ -- val=10: [5,15] → 10; val=20: [15,25] → 20+20=40; val=30: [25,35] → 30; val=50: [45,55] → 50
385
+ SELECT id, val,
386
+ SUM(val) OVER (ORDER BY val RANGE BETWEEN 5 PRECEDING AND 5 FOLLOWING) as range_sum
387
+ FROM range_test ORDER BY id;
388
+ → [{"id":1,"val":10,"range_sum":10},{"id":2,"val":20,"range_sum":40},{"id":3,"val":20,"range_sum":40},{"id":4,"val":30,"range_sum":30},{"id":5,"val":50,"range_sum":50}]
389
+
390
+ -- RANGE UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING (same as ROWS)
391
+ SELECT id, val,
392
+ SUM(val) OVER (ORDER BY val RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) as total
393
+ FROM range_test ORDER BY id;
394
+ → [{"id":1,"val":10,"total":130},{"id":2,"val":20,"total":130},{"id":3,"val":20,"total":130},{"id":4,"val":30,"total":130},{"id":5,"val":50,"total":130}]
395
+
396
+ -- FIRST_VALUE/LAST_VALUE with RANGE CURRENT ROW
397
+ SELECT id, val,
398
+ FIRST_VALUE(id) OVER (ORDER BY val RANGE CURRENT ROW) as first_peer_id,
399
+ LAST_VALUE(id) OVER (ORDER BY val RANGE CURRENT ROW) as last_peer_id
400
+ FROM range_test ORDER BY id;
401
+ → [{"id":1,"val":10,"first_peer_id":1,"last_peer_id":1},{"id":2,"val":20,"first_peer_id":2,"last_peer_id":3},{"id":3,"val":20,"first_peer_id":2,"last_peer_id":3},{"id":4,"val":30,"first_peer_id":4,"last_peer_id":4},{"id":5,"val":50,"first_peer_id":5,"last_peer_id":5}]
402
+
403
+ DROP TABLE range_test;
404
+
405
+ -- =============================================================================
406
+ -- Streaming fast-path tests (rule: monotonic-window).
407
+ --
408
+ -- These verify the streaming emitter produces identical output to the buffered
409
+ -- emitter on a variety of monotonic-input cases. The leaf table's PK provides
410
+ -- the monotonicOn(id) advertisement that the rule keys off; queries below
411
+ -- exercise ranking, navigation, and running-aggregate functions over
412
+ -- ORDER BY id.
413
+ -- =============================================================================
414
+
415
+ CREATE TABLE stream_t (id INTEGER PRIMARY KEY, val INTEGER, grp TEXT);
416
+ INSERT INTO stream_t VALUES
417
+ (1, 10, 'A'), (2, 20, 'A'), (3, 30, 'A'),
418
+ (4, 40, 'B'), (5, 50, 'B'), (6, 60, 'B');
419
+
420
+ -- ROW_NUMBER over monotonic PK: streaming.
421
+ SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM stream_t ORDER BY id;
422
+ → [{"id":1,"rn":1},{"id":2,"rn":2},{"id":3,"rn":3},{"id":4,"rn":4},{"id":5,"rn":5},{"id":6,"rn":6}]
423
+
424
+ -- RANK / DENSE_RANK with no peer ties (PK is unique).
425
+ SELECT id, val, RANK() OVER (ORDER BY id) AS r, DENSE_RANK() OVER (ORDER BY id) AS dr
426
+ FROM stream_t ORDER BY id;
427
+ → [{"id":1,"val":10,"r":1,"dr":1},{"id":2,"val":20,"r":2,"dr":2},{"id":3,"val":30,"r":3,"dr":3},{"id":4,"val":40,"r":4,"dr":4},{"id":5,"val":50,"r":5,"dr":5},{"id":6,"val":60,"r":6,"dr":6}]
428
+
429
+ -- LAG with default offset = 1.
430
+ SELECT id, LAG(val) OVER (ORDER BY id) AS prv FROM stream_t ORDER BY id;
431
+ → [{"id":1,"prv":null},{"id":2,"prv":10},{"id":3,"prv":20},{"id":4,"prv":30},{"id":5,"prv":40},{"id":6,"prv":50}]
432
+
433
+ -- LAG with literal offset 2 and a non-NULL default.
434
+ SELECT id, LAG(val, 2, -1) OVER (ORDER BY id) AS prv2 FROM stream_t ORDER BY id;
435
+ → [{"id":1,"prv2":-1},{"id":2,"prv2":-1},{"id":3,"prv2":10},{"id":4,"prv2":20},{"id":5,"prv2":30},{"id":6,"prv2":40}]
436
+
437
+ -- LEAD with default offset and no default value (NULL at boundary).
438
+ SELECT id, LEAD(val) OVER (ORDER BY id) AS nxt FROM stream_t ORDER BY id;
439
+ → [{"id":1,"nxt":20},{"id":2,"nxt":30},{"id":3,"nxt":40},{"id":4,"nxt":50},{"id":5,"nxt":60},{"id":6,"nxt":null}]
440
+
441
+ -- LEAD with literal offset 1 and a non-NULL default at partition end.
442
+ SELECT id, LEAD(val, 1, -99) OVER (ORDER BY id) AS nxt FROM stream_t ORDER BY id;
443
+ → [{"id":1,"nxt":20},{"id":2,"nxt":30},{"id":3,"nxt":40},{"id":4,"nxt":50},{"id":5,"nxt":60},{"id":6,"nxt":-99}]
444
+
445
+ -- Running SUM over default RANGE frame (UNBOUNDED PRECEDING TO CURRENT ROW).
446
+ SELECT id, val, SUM(val) OVER (ORDER BY id) AS rs FROM stream_t ORDER BY id;
447
+ → [{"id":1,"val":10,"rs":10},{"id":2,"val":20,"rs":30},{"id":3,"val":30,"rs":60},{"id":4,"val":40,"rs":100},{"id":5,"val":50,"rs":150},{"id":6,"val":60,"rs":210}]
448
+
449
+ -- Running MIN over explicit ROWS UNBOUNDED PRECEDING.
450
+ SELECT id, val, MIN(val) OVER (ORDER BY id ROWS UNBOUNDED PRECEDING) AS rm FROM stream_t ORDER BY id;
451
+ → [{"id":1,"val":10,"rm":10},{"id":2,"val":20,"rm":10},{"id":3,"val":30,"rm":10},{"id":4,"val":40,"rm":10},{"id":5,"val":50,"rm":10},{"id":6,"val":60,"rm":10}]
452
+
453
+ -- FIRST_VALUE caches the first row's expr for the partition.
454
+ SELECT id, FIRST_VALUE(val) OVER (ORDER BY id) AS fv FROM stream_t ORDER BY id;
455
+ → [{"id":1,"fv":10},{"id":2,"fv":10},{"id":3,"fv":10},{"id":4,"fv":10},{"id":5,"fv":10},{"id":6,"fv":10}]
456
+
457
+ -- LAST_VALUE under default frame (== current row in RANGE mode for unique keys).
458
+ SELECT id, LAST_VALUE(val) OVER (ORDER BY id) AS lv FROM stream_t ORDER BY id;
459
+ → [{"id":1,"lv":10},{"id":2,"lv":20},{"id":3,"lv":30},{"id":4,"lv":40},{"id":5,"lv":50},{"id":6,"lv":60}]
460
+
461
+ -- All-NULL expr column for LAG: defaults bubble through unchanged.
462
+ CREATE TABLE stream_n (id INTEGER PRIMARY KEY, x INTEGER NULL);
463
+ INSERT INTO stream_n VALUES (1, NULL), (2, NULL), (3, NULL);
464
+ SELECT id, LAG(x) OVER (ORDER BY id) AS prv FROM stream_n ORDER BY id;
465
+ → [{"id":1,"prv":null},{"id":2,"prv":null},{"id":3,"prv":null}]
466
+
467
+ -- Single-row "partition" — running aggregate equals the value itself.
468
+ CREATE TABLE stream_one (id INTEGER PRIMARY KEY, val INTEGER);
469
+ INSERT INTO stream_one VALUES (1, 42);
470
+ SELECT id, val, SUM(val) OVER (ORDER BY id) AS rs FROM stream_one ORDER BY id;
471
+ → [{"id":1,"val":42,"rs":42}]
472
+
473
+ -- Empty result via WHERE 1=0: streaming yields zero rows, no errors.
474
+ SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM stream_t WHERE 1=0;
475
+ → []
476
+
477
+ DROP TABLE stream_t;
478
+ DROP TABLE stream_n;
479
+ DROP TABLE stream_one;
480
+
481
+ -- =============================================================================
482
+ -- Shared-subtree row-context regression tests.
483
+ --
484
+ -- Two scalar subqueries against the same CTE share a plan subtree. Without
485
+ -- serialized callback evaluation (see ticket
486
+ -- serialize-emit-callbacks-returning-window), parallel `Promise.all` over
487
+ -- the partition / ORDER BY callbacks raced on the shared inner-scan RowSlot
488
+ -- under real async I/O (LevelDB store), producing wrong/null partition keys.
489
+ -- =============================================================================
490
+
491
+ create table tr (id integer primary key, v integer);
492
+ insert into tr values (1, 10), (2, 20), (3, 30);
493
+
494
+ -- PARTITION BY with two scalar subqueries against the same CTE.
495
+ -- Both keys are constants (same for every row), so all rows land in a single
496
+ -- partition and rn must be 1,2,3.
497
+ with high as (select * from tr where v >= 20)
498
+ select
499
+ row_number() over (
500
+ partition by (select count(*) from high), (select sum(v) from high)
501
+ order by id
502
+ ) as rn
503
+ from tr
504
+ order by id;
505
+ → [{"rn":1},{"rn":2},{"rn":3}]
506
+
507
+ -- ORDER BY with two scalar subqueries against the same CTE.
508
+ -- Both keys are constants, so the entire result is one peer group; the outer
509
+ -- ORDER BY v then yields rn=1,2,3 in v order.
510
+ with high as (select * from tr where v >= 20)
511
+ select
512
+ v,
513
+ row_number() over (
514
+ order by (select count(*) from high), (select sum(v) from high)
515
+ ) as rn
516
+ from tr
517
+ order by v;
518
+ → [{"v":10,"rn":1},{"v":20,"rn":2},{"v":30,"rn":3}]
519
+
520
+ drop table tr;
521
+
522
+ -- =============================================================================
523
+ -- Streaming sliding-frame tests (rule: monotonic-window).
524
+ --
525
+ -- These verify the streaming sliding-frame path under
526
+ -- `ROWS BETWEEN n PRECEDING AND m FOLLOWING` and
527
+ -- `RANGE BETWEEN <num> PRECEDING AND <num> FOLLOWING`. The leaf table's PK
528
+ -- provides the monotonicOn(id) advertisement that the rule keys off.
529
+ -- =============================================================================
530
+
531
+ CREATE TABLE stream_s (id INTEGER PRIMARY KEY, val INTEGER, grp TEXT);
532
+ INSERT INTO stream_s VALUES
533
+ (1, 10, 'A'), (2, 20, 'A'), (3, 30, 'A'),
534
+ (4, 40, 'B'), (5, 50, 'B'), (6, 60, 'B');
535
+
536
+ -- ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING — SUM
537
+ SELECT id, SUM(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS s
538
+ FROM stream_s ORDER BY id;
539
+ → [{"id":1,"s":30},{"id":2,"s":60},{"id":3,"s":90},{"id":4,"s":120},{"id":5,"s":150},{"id":6,"s":110}]
540
+
541
+ -- ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING — COUNT
542
+ SELECT id, COUNT(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS c
543
+ FROM stream_s ORDER BY id;
544
+ → [{"id":1,"c":2},{"id":2,"c":3},{"id":3,"c":3},{"id":4,"c":3},{"id":5,"c":3},{"id":6,"c":2}]
545
+
546
+ -- ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING — AVG
547
+ SELECT id, AVG(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS a
548
+ FROM stream_s ORDER BY id;
549
+ → [{"id":1,"a":15},{"id":2,"a":20},{"id":3,"a":30},{"id":4,"a":40},{"id":5,"a":50},{"id":6,"a":55}]
550
+
551
+ -- ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING — MIN / MAX
552
+ SELECT id,
553
+ MIN(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS mn,
554
+ MAX(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS mx
555
+ FROM stream_s ORDER BY id;
556
+ → [{"id":1,"mn":10,"mx":20},{"id":2,"mn":10,"mx":30},{"id":3,"mn":20,"mx":40},{"id":4,"mn":30,"mx":50},{"id":5,"mn":40,"mx":60},{"id":6,"mn":50,"mx":60}]
557
+
558
+ -- Asymmetric: ROWS BETWEEN 2 PRECEDING AND 0 FOLLOWING — SUM
559
+ SELECT id, SUM(val) OVER (ORDER BY id ROWS BETWEEN 2 PRECEDING AND 0 FOLLOWING) AS s
560
+ FROM stream_s ORDER BY id;
561
+ → [{"id":1,"s":10},{"id":2,"s":30},{"id":3,"s":60},{"id":4,"s":90},{"id":5,"s":120},{"id":6,"s":150}]
562
+
563
+ -- One-sided right: ROWS BETWEEN 0 PRECEDING AND 2 FOLLOWING — SUM
564
+ SELECT id, SUM(val) OVER (ORDER BY id ROWS BETWEEN 0 PRECEDING AND 2 FOLLOWING) AS s
565
+ FROM stream_s ORDER BY id;
566
+ → [{"id":1,"s":60},{"id":2,"s":90},{"id":3,"s":120},{"id":4,"s":150},{"id":5,"s":110},{"id":6,"s":60}]
567
+
568
+ -- Edge clamping: window much larger than partition (5 PRECEDING AND 5 FOLLOWING).
569
+ -- Every row sees the full partition sum (210).
570
+ SELECT id, SUM(val) OVER (ORDER BY id ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING) AS s
571
+ FROM stream_s ORDER BY id;
572
+ → [{"id":1,"s":210},{"id":2,"s":210},{"id":3,"s":210},{"id":4,"s":210},{"id":5,"s":210},{"id":6,"s":210}]
573
+
574
+ -- FIRST_VALUE / LAST_VALUE under sliding ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
575
+ SELECT id,
576
+ FIRST_VALUE(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS fv,
577
+ LAST_VALUE(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS lv
578
+ FROM stream_s ORDER BY id;
579
+ → [{"id":1,"fv":10,"lv":20},{"id":2,"fv":10,"lv":30},{"id":3,"fv":20,"lv":40},{"id":4,"fv":30,"lv":50},{"id":5,"fv":40,"lv":60},{"id":6,"fv":50,"lv":60}]
580
+
581
+ -- Single-row partition with a sliding frame (clamps to the lone row).
582
+ CREATE TABLE stream_s_one (id INTEGER PRIMARY KEY, val INTEGER);
583
+ INSERT INTO stream_s_one VALUES (1, 42);
584
+ SELECT id, SUM(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS s,
585
+ MIN(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS mn
586
+ FROM stream_s_one ORDER BY id;
587
+ → [{"id":1,"s":42,"mn":42}]
588
+
589
+ -- Sliding SUM with NULL argVals: nulls don't contribute to SUM/COUNT.
590
+ CREATE TABLE stream_s_null (id INTEGER PRIMARY KEY, val INTEGER NULL);
591
+ INSERT INTO stream_s_null VALUES (1, 10), (2, NULL), (3, 30), (4, NULL);
592
+ SELECT id,
593
+ SUM(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS s,
594
+ COUNT(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS c
595
+ FROM stream_s_null ORDER BY id;
596
+ → [{"id":1,"s":10,"c":1},{"id":2,"s":40,"c":2},{"id":3,"s":30,"c":1},{"id":4,"s":30,"c":1}]
597
+
598
+ -- One-sided frames. `CURRENT ROW` is the offset-zero case of the bound it
599
+ -- replaces, so these run the same streaming sliding-buffer state machine as
600
+ -- the two-sided forms above.
601
+
602
+ -- ROWS BETWEEN 2 PRECEDING AND CURRENT ROW — the trailing moving sum.
603
+ SELECT id, SUM(val) OVER (ORDER BY id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS s
604
+ FROM stream_s ORDER BY id;
605
+ → [{"id":1,"s":10},{"id":2,"s":30},{"id":3,"s":60},{"id":4,"s":90},{"id":5,"s":120},{"id":6,"s":150}]
606
+
607
+ -- The start-only spelling `ROWS 2 PRECEDING` means the same frame.
608
+ SELECT id, SUM(val) OVER (ORDER BY id ROWS 2 PRECEDING) AS s
609
+ FROM stream_s ORDER BY id;
610
+ → [{"id":1,"s":10},{"id":2,"s":30},{"id":3,"s":60},{"id":4,"s":90},{"id":5,"s":120},{"id":6,"s":150}]
611
+
612
+ -- ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING — the leading moving sum.
613
+ SELECT id, SUM(val) OVER (ORDER BY id ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING) AS s
614
+ FROM stream_s ORDER BY id;
615
+ → [{"id":1,"s":60},{"id":2,"s":90},{"id":3,"s":120},{"id":4,"s":150},{"id":5,"s":110},{"id":6,"s":60}]
616
+
617
+ -- ROWS BETWEEN CURRENT ROW AND CURRENT ROW — the single current row.
618
+ SELECT id, SUM(val) OVER (ORDER BY id ROWS BETWEEN CURRENT ROW AND CURRENT ROW) AS s
619
+ FROM stream_s ORDER BY id;
620
+ → [{"id":1,"s":10},{"id":2,"s":20},{"id":3,"s":30},{"id":4,"s":40},{"id":5,"s":50},{"id":6,"s":60}]
621
+
622
+ -- Trailing MIN / MAX (the running min-max shape over an ordered table).
623
+ SELECT id,
624
+ MIN(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS mn,
625
+ MAX(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS mx
626
+ FROM stream_s ORDER BY id;
627
+ → [{"id":1,"mn":10,"mx":10},{"id":2,"mn":10,"mx":20},{"id":3,"mn":20,"mx":30},{"id":4,"mn":30,"mx":40},{"id":5,"mn":40,"mx":50},{"id":6,"mn":50,"mx":60}]
628
+
629
+ -- A one-sided frame and the default (running) frame in one query: distinct
630
+ -- frames land in distinct window nodes and each keeps its own strategy.
631
+ SELECT id,
632
+ SUM(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS trailing,
633
+ SUM(val) OVER (ORDER BY id) AS running
634
+ FROM stream_s ORDER BY id;
635
+ → [{"id":1,"trailing":10,"running":10},{"id":2,"trailing":30,"running":30},{"id":3,"trailing":50,"running":60},{"id":4,"trailing":70,"running":100},{"id":5,"trailing":90,"running":150},{"id":6,"trailing":110,"running":210}]
636
+
637
+ -- Trailing frame with PARTITION BY: the frame restarts at each partition.
638
+ SELECT id, SUM(val) OVER (PARTITION BY grp ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS s
639
+ FROM stream_s ORDER BY id;
640
+ → [{"id":1,"s":10},{"id":2,"s":30},{"id":3,"s":50},{"id":4,"s":40},{"id":5,"s":90},{"id":6,"s":110}]
641
+
642
+ -- One-sided frame over NULL argument values: NULLs still don't contribute.
643
+ SELECT id,
644
+ SUM(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS s,
645
+ COUNT(val) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS c
646
+ FROM stream_s_null ORDER BY id;
647
+ → [{"id":1,"s":10,"c":1},{"id":2,"s":10,"c":1},{"id":3,"s":30,"c":1},{"id":4,"s":30,"c":1}]
648
+
649
+ DROP TABLE stream_s;
650
+ DROP TABLE stream_s_one;
651
+ DROP TABLE stream_s_null;
652
+
653
+ -- RANGE BETWEEN with peer ties.
654
+ -- Numeric ORDER BY column (val) with literal numeric offsets. The PK is id,
655
+ -- so the rule may or may not fire here (depends on whether monotonicOn covers
656
+ -- val); the test confirms correctness on whichever path runs.
657
+ CREATE TABLE range_slide (id INTEGER PRIMARY KEY, val INTEGER);
658
+ INSERT INTO range_slide VALUES
659
+ (1, 10), (2, 15), (3, 15), (4, 20), (5, 25), (6, 40);
660
+
661
+ -- val=10: window=[0,20] → 10,15,15,20 = 60
662
+ -- val=15 (twice): window=[5,25] → 10,15,15,20,25 = 85
663
+ -- val=20: window=[10,30] → 10,15,15,20,25 = 85
664
+ -- val=25: window=[15,35] → 15,15,20,25 = 75
665
+ -- val=40: window=[30,50] → 40
666
+ SELECT val, SUM(val) OVER (ORDER BY val RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING) AS s
667
+ FROM range_slide ORDER BY val, id;
668
+ → [{"val":10,"s":60},{"val":15,"s":85},{"val":15,"s":85},{"val":20,"s":85},{"val":25,"s":75},{"val":40,"s":40}]
669
+
670
+ -- COUNT under same RANGE frame.
671
+ SELECT val, COUNT(*) OVER (ORDER BY val RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING) AS c
672
+ FROM range_slide ORDER BY val, id;
673
+ → [{"val":10,"c":4},{"val":15,"c":5},{"val":15,"c":5},{"val":20,"c":5},{"val":25,"c":4},{"val":40,"c":1}]
674
+
675
+ -- MIN / MAX under same RANGE frame.
676
+ SELECT val,
677
+ MIN(val) OVER (ORDER BY val RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING) AS mn,
678
+ MAX(val) OVER (ORDER BY val RANGE BETWEEN 10 PRECEDING AND 10 FOLLOWING) AS mx
679
+ FROM range_slide ORDER BY val, id;
680
+ → [{"val":10,"mn":10,"mx":20},{"val":15,"mn":10,"mx":25},{"val":15,"mn":10,"mx":25},{"val":20,"mn":10,"mx":25},{"val":25,"mn":15,"mx":25},{"val":40,"mn":40,"mx":40}]
681
+
682
+ -- One-sided RANGE frames. In RANGE mode `CURRENT ROW` means the whole peer
683
+ -- group, which is exactly the offset-zero interval [v, v] — so the two 15s
684
+ -- must be treated identically at either edge of the frame.
685
+
686
+ -- val=10: [0,10] → 10
687
+ -- val=15 (twice): [5,15] → 10,15,15 = 40
688
+ -- val=20: [10,20] → 10,15,15,20 = 60
689
+ -- val=25: [15,25] → 15,15,20,25 = 75
690
+ -- val=40: [30,40] → 40
691
+ SELECT val, SUM(val) OVER (ORDER BY val RANGE BETWEEN 10 PRECEDING AND CURRENT ROW) AS s
692
+ FROM range_slide ORDER BY val, id;
693
+ → [{"val":10,"s":10},{"val":15,"s":40},{"val":15,"s":40},{"val":20,"s":60},{"val":25,"s":75},{"val":40,"s":40}]
694
+
695
+ -- The start-only spelling `RANGE 10 PRECEDING` means the same frame.
696
+ SELECT val, SUM(val) OVER (ORDER BY val RANGE 10 PRECEDING) AS s
697
+ FROM range_slide ORDER BY val, id;
698
+ → [{"val":10,"s":10},{"val":15,"s":40},{"val":15,"s":40},{"val":20,"s":60},{"val":25,"s":75},{"val":40,"s":40}]
699
+
700
+ -- val=10: [10,20] → 10,15,15,20 = 60
701
+ -- val=15 (twice): [15,25] → 15,15,20,25 = 75
702
+ -- val=20: [20,30] → 20,25 = 45
703
+ -- val=25: [25,35] → 25
704
+ -- val=40: [40,50] → 40
705
+ SELECT val, SUM(val) OVER (ORDER BY val RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING) AS s
706
+ FROM range_slide ORDER BY val, id;
707
+ → [{"val":10,"s":60},{"val":15,"s":75},{"val":15,"s":75},{"val":20,"s":45},{"val":25,"s":25},{"val":40,"s":40}]
708
+
709
+ -- RANGE BETWEEN CURRENT ROW AND CURRENT ROW is exactly the peer group, so the
710
+ -- two val=15 rows each see a frame of 2 rows and everything else sees 1.
711
+ SELECT val,
712
+ COUNT(*) OVER (ORDER BY val RANGE BETWEEN CURRENT ROW AND CURRENT ROW) AS c,
713
+ SUM(val) OVER (ORDER BY val RANGE BETWEEN CURRENT ROW AND CURRENT ROW) AS s
714
+ FROM range_slide ORDER BY val, id;
715
+ → [{"val":10,"c":1,"s":10},{"val":15,"c":2,"s":30},{"val":15,"c":2,"s":30},{"val":20,"c":1,"s":20},{"val":25,"c":1,"s":25},{"val":40,"c":1,"s":40}]
716
+
717
+ -- NULL ordering values: a NULL-val row's RANGE window is the contiguous NULL
718
+ -- peer span. Finite-val rows' RANGE windows exclude NULL rows entirely.
719
+ CREATE TABLE range_null (id INTEGER PRIMARY KEY, val INTEGER NULL);
720
+ INSERT INTO range_null VALUES (1, NULL), (2, NULL), (3, 10), (4, 20), (5, 30);
721
+ -- ASC NULLS FIRST default (NULLs at the front of the sort order).
722
+ -- val=NULL (id=1,2): window = both NULL rows. SUM(val)=NULL (no non-null vals).
723
+ -- val=10: window=[5,15] over finite vals → just 10. SUM=10.
724
+ -- val=20: window=[15,25] → 20. SUM=20.
725
+ -- val=30: window=[25,35] → 30. SUM=30.
726
+ SELECT id, val, SUM(val) OVER (ORDER BY val RANGE BETWEEN 5 PRECEDING AND 5 FOLLOWING) AS s
727
+ FROM range_null ORDER BY id;
728
+ → [{"id":1,"val":null,"s":null},{"id":2,"val":null,"s":null},{"id":3,"val":10,"s":10},{"id":4,"val":20,"s":20},{"id":5,"val":30,"s":30}]
729
+
730
+ -- Same, with an interval that reaches down to zero. NULL is not zero: the two
731
+ -- NULL rows must still stay out of val=10's frame. (Regression — the buffered
732
+ -- frame walk read the ORDER BY value with a bare `Number(...)`, which turns
733
+ -- NULL into 0 and pulled both NULL rows in.)
734
+ -- val=NULL (id=1,2): the NULL peer group → no non-null vals → SUM=NULL.
735
+ -- val=10: [0,10] → 10. val=20: [10,20] → 30. val=30: [20,30] → 50.
736
+ SELECT id, val, SUM(val) OVER (ORDER BY val RANGE BETWEEN 10 PRECEDING AND CURRENT ROW) AS s
737
+ FROM range_null ORDER BY id;
738
+ → [{"id":1,"val":null,"s":null},{"id":2,"val":null,"s":null},{"id":3,"val":10,"s":10},{"id":4,"val":20,"s":30},{"id":5,"val":30,"s":50}]
739
+
740
+ -- A fractional RANGE offset is not a supported frame offset on either physical
741
+ -- path — it must raise, not quietly depend on which plan the optimizer picks.
742
+ SELECT val, SUM(val) OVER (ORDER BY val RANGE BETWEEN 1.5 PRECEDING AND CURRENT ROW) AS s
743
+ FROM range_null ORDER BY val, id;
744
+ -- error: Invalid window frame offset
745
+
746
+ DROP TABLE range_slide;
747
+ DROP TABLE range_null;
748
+
749
+ -- =============================================================================
750
+ -- Window function nested inside an arithmetic / scalar projection expression.
751
+ --
752
+ -- Regression: a window function wrapped in a binary/scalar expression
753
+ -- (e.g. `1000 - row_number() over (...)`) must keep the surrounding expression
754
+ -- and substitute the window result back in — not collapse to the bare window
755
+ -- column nor drop the projection entirely.
756
+ -- =============================================================================
757
+
758
+ CREATE TABLE nested_w (id INTEGER PRIMARY KEY, val INTEGER);
759
+ INSERT INTO nested_w VALUES (1, 10), (2, 20), (3, 30);
760
+
761
+ -- Literal minus window fn: arithmetic must actually apply (999, 998, 997).
762
+ SELECT id, 1000 - ROW_NUMBER() OVER (ORDER BY id) AS v FROM nested_w ORDER BY id;
763
+ → [{"id":1,"v":999},{"id":2,"v":998},{"id":3,"v":997}]
764
+
765
+ -- Window fn on the left of the operator.
766
+ SELECT id, ROW_NUMBER() OVER (ORDER BY id) + 5 AS v FROM nested_w ORDER BY id;
767
+ → [{"id":1,"v":6},{"id":2,"v":7},{"id":3,"v":8}]
768
+
769
+ -- Window fn nested inside a scalar function call.
770
+ SELECT id, ABS(1 - ROW_NUMBER() OVER (ORDER BY id)) AS v FROM nested_w ORDER BY id;
771
+ → [{"id":1,"v":0},{"id":2,"v":1},{"id":3,"v":2}]
772
+
773
+ -- Two different window functions in one arithmetic expression.
774
+ -- rn (order by id) = 1,2,3 ; rank (order by val desc) = 3,2,1 ; rn-rank = -2,0,2.
775
+ SELECT id, ROW_NUMBER() OVER (ORDER BY id) - RANK() OVER (ORDER BY val DESC) AS v
776
+ FROM nested_w ORDER BY id;
777
+ → [{"id":1,"v":-2},{"id":2,"v":0},{"id":3,"v":2}]
778
+
779
+ -- Mixed: plain columns + a window-wrapped column + a top-level window column.
780
+ SELECT id, val,
781
+ 1000 - ROW_NUMBER() OVER (ORDER BY id) AS adj,
782
+ ROW_NUMBER() OVER (ORDER BY id) AS rn
783
+ FROM nested_w ORDER BY id;
784
+ → [{"id":1,"val":10,"adj":999,"rn":1},{"id":2,"val":20,"adj":998,"rn":2},{"id":3,"val":30,"adj":997,"rn":3}]
785
+
786
+ DROP TABLE nested_w;
787
+
788
+ -- =============================================================================
789
+ -- Window functions in a GROUPED query.
790
+ --
791
+ -- The window runs over the GROUPED rows — one row per group — so
792
+ -- `row_number() over (order by a) ... group by a` numbers the groups, not the
793
+ -- underlying rows. Regression for
794
+ -- bug-window-function-over-grouped-query-crashes: the grouped select list and
795
+ -- the window select list used to be built by two separate walks, leaving a raw
796
+ -- window-function expression in a projection *below* the node that computes
797
+ -- window values ("No emitter registered for WindowFunctionCall").
798
+ --
799
+ -- No primary key on `wg`, so the functional-dependency GROUP BY reduction cannot
800
+ -- rewrite the grouping keys out from under these.
801
+ -- =============================================================================
802
+
803
+ create table wg (a text, b text);
804
+ insert into wg values ('x','1'),('y','2'),('x','3');
805
+
806
+ -- Bare window function over the groups: two groups, so 1 and 2 (not 1..3).
807
+ select a, row_number() over (order by a) as rn from wg group by a order by a;
808
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
809
+
810
+ -- Alongside a real aggregate.
811
+ select a, count(*) as c, row_number() over (order by a) as rn from wg group by a order by a;
812
+ → [{"a":"x","c":2,"rn":1},{"a":"y","c":1,"rn":2}]
813
+
814
+ -- Window function wrapped in arithmetic keeps the surrounding expression.
815
+ select a, 1000 - row_number() over (order by a) as adj from wg group by a order by a;
816
+ → [{"a":"x","adj":999},{"a":"y","adj":998}]
817
+
818
+ -- HAVING filters groups before the window sees them, so rn restarts at 1.
819
+ select a, row_number() over (order by a) as rn from wg group by a having count(*) > 1;
820
+ → [{"a":"x","rn":1}]
821
+
822
+ -- ORDER BY a window alias.
823
+ select a, row_number() over (order by a) as rn from wg group by a order by rn desc;
824
+ → [{"a":"y","rn":2},{"a":"x","rn":1}]
825
+
826
+ -- COUNT(*) OVER () counts GROUPS (2), not source rows (3).
827
+ select a, count(*) over () as g from wg group by a order by a;
828
+ → [{"a":"x","g":2},{"a":"y","g":2}]
829
+
830
+ -- LIMIT over the lot.
831
+ select a, row_number() over (order by a) as rn from wg group by a order by rn limit 1;
832
+ → [{"a":"x","rn":1}]
833
+
834
+ -- Grouped `select *` plus a window column. `group by a, b` collapses no rows
835
+ -- here, so all three groups survive and the window numbers all three.
836
+ select *, row_number() over (order by a, b) as rn from wg group by a, b order by a, b;
837
+ → [{"a":"x","b":"1","rn":1},{"a":"x","b":"3","rn":2},{"a":"y","b":"2","rn":3}]
838
+
839
+ -- An aggregate already in the SELECT list may be spelled out again in the window
840
+ -- specification — it reads the computed aggregate column.
841
+ select a, count(*) as c, row_number() over (order by count(*) desc) as rn
842
+ from wg group by a order by rn;
843
+ → [{"a":"x","c":2,"rn":1},{"a":"y","c":1,"rn":2}]
844
+
845
+ -- A window specification is subject to the same GROUP BY restriction as the rest
846
+ -- of the select list: the grouped rows carry only grouping keys and aggregate
847
+ -- results, so an ungrouped column must fail at plan time (it used to reach the
848
+ -- runtime as a base-table reference and die with an internal row-context error).
849
+ select a, row_number() over (order by b) as rn from wg group by a;
850
+ -- error: Column 'b' must appear in the GROUP BY clause or be used in an aggregate function
851
+
852
+ select a, count(*) over (partition by b) as c from wg group by a;
853
+ -- error: Column 'b' must appear in the GROUP BY clause or be used in an aggregate function
854
+
855
+ -- Same rule for a window function's arguments.
856
+ select a, sum(b) over () as s from wg group by a;
857
+ -- error: Column 'b' must appear in the GROUP BY clause or be used in an aggregate function
858
+
859
+ -- A window function above an UNGROUPED aggregate: the aggregation yields exactly
860
+ -- one implicit-group row, and the window runs over that single row, so
861
+ -- `row_number()` is 1. There is no grouping-key redirect for this shape -- an
862
+ -- ungrouped query has no grouping keys to rewrite onto -- and it needs none.
863
+ -- (Matches PostgreSQL. Previously unreachable: the select list was rejected by
864
+ -- the old ungrouped aggregate/non-aggregate mixing rule before the window phase
865
+ -- ever ran.)
866
+ select count(*) as c, row_number() over (order by count(*)) as rn from wg;
867
+ → [{"c":3,"rn":1}]
868
+
869
+ -- The coverage rule still applies to that shape, and is what keeps it honest: the
870
+ -- single implicit-group row carries only aggregate results, so a window
871
+ -- specification or argument naming a base-table column is rejected with the same
872
+ -- message a grouped query gets.
873
+ select count(*) as c, row_number() over (order by a) as rn from wg;
874
+ -- error: Column 'a' must appear in the GROUP BY clause or be used in an aggregate function
875
+
876
+ select count(*) as c, sum(b) over () as s from wg;
877
+ -- error: Column 'b' must appear in the GROUP BY clause or be used in an aggregate function
878
+
879
+ -- A window function whose ARGUMENTS are column-free runs over that same single
880
+ -- implicit-group row, so it sees exactly one row regardless of how many rows the
881
+ -- aggregate consumed.
882
+ select count(*) as c, sum(1) over (order by count(*)) as s from wg;
883
+ → [{"c":3,"s":1}]
884
+
885
+ -- And the aggregate-inside-a-window-argument limitation is unchanged for the
886
+ -- ungrouped shape (an aggregate over the aggregated row, not over the input).
887
+ select count(*) as c, sum(count(*)) over () as s from wg;
888
+ -- error: Aggregate function count not allowed in this context
889
+
890
+ -- An aggregate that appears ONLY in the window specification is a named
891
+ -- limitation, not an internal error: it would have to be collected into the
892
+ -- aggregate node before the window phase runs.
893
+ select a, row_number() over (order by count(*)) as rn from wg group by a;
894
+ -- error: Aggregate function count in a window function's ORDER BY is only supported when the same aggregate also appears in the SELECT list
895
+
896
+ -- Same limitation when the window spec's aggregate differs from the SELECT
897
+ -- list's only in its argument (bug-aggregate-reuse-matching-ignores-arguments):
898
+ -- `sum(a+0)` here is not `sum(b+0)`, so it is not the aggregate already
899
+ -- collected for the SELECT list and must be rejected the same way.
900
+ select a, sum(b+0) as s, row_number() over (order by sum(a+0)) as rn from wg group by a;
901
+ -- error: Aggregate function sum in a window function's ORDER BY is only supported when the same aggregate also appears in the SELECT list
902
+
903
+ -- The match is textual, so a *qualifier* divergence lands in that same
904
+ -- limitation even though both spellings name the same column: `sum(w.b)` in the
905
+ -- select list is not the fingerprint `sum(b)` the window spec asks for. Writing
906
+ -- both the same way, or referencing the select-list alias, both work.
907
+ select a, sum(w.b) as s, row_number() over (order by sum(b)) as rn from wg w group by a;
908
+ -- error: Aggregate function sum in a window function's ORDER BY is only supported when the same aggregate also appears in the SELECT list
909
+
910
+ -- Same limitation for a literal-case divergence
911
+ -- (bug-aggregate-match-ignores-string-literal-case): a quoted string literal's
912
+ -- case is part of an aggregate's identity, so `count(nullif(b,'a'))` here is not
913
+ -- `count(nullif(b,'A'))` and must be rejected the same way — reading the wrong
914
+ -- aggregate silently would be worse than this loud error.
915
+ create table wgc (id integer primary key, g integer, b text);
916
+ insert into wgc values (1, 1, 'A'), (2, 1, 'A'), (3, 1, 'a');
917
+
918
+ select g, count(nullif(b,'A')) as c, row_number() over (order by count(nullif(b,'a'))) as w
919
+ from wgc group by g;
920
+ -- error: Aggregate function count in a window function's ORDER BY is only supported when the same aggregate also appears in the SELECT list
921
+
922
+ drop table wgc;
923
+
924
+ -- Two window functions with different specifications each get their own window
925
+ -- output column; neither collapses onto the other's.
926
+ select a, row_number() over (order by a) as p, count(*) over (partition by a) as q
927
+ from wg group by a order by a;
928
+ → [{"a":"x","p":1,"q":1},{"a":"y","p":2,"q":1}]
929
+
930
+ -- Two textually identical window functions resolve to their own columns too.
931
+ select a, row_number() over (order by a) as p, row_number() over (order by a) as q
932
+ from wg group by a order by a;
933
+ → [{"a":"x","p":1,"q":1},{"a":"y","p":2,"q":2}]
934
+
935
+ -- A window specification may name an aggregate by its SELECT-list alias — that
936
+ -- resolves through the aggregate output scope, like ORDER BY does.
937
+ select a, count(*) as c, row_number() over (order by c) as rn from wg group by a order by rn;
938
+ → [{"a":"y","c":1,"rn":1},{"a":"x","c":2,"rn":2}]
939
+
940
+ -- A grouping key may be named in a window specification by ANY spelling that
941
+ -- denotes it, not only the one the GROUP BY used. The aggregate output scope
942
+ -- registers a qualified name only when the GROUP BY key was itself written
943
+ -- qualified, and registers a non-bare key only under a synthetic `group_N` name,
944
+ -- so the other spellings bind to a base-table column the grouped row does not
945
+ -- carry; the window phase redirects those subtrees onto the aggregate's own group
946
+ -- output column (bug-window-spec-reads-base-table-column — they used to die with
947
+ -- an internal "No row context found for column a" error).
948
+
949
+ -- GROUP BY key written qualified, window spec qualified.
950
+ select wg.a, row_number() over (order by wg.a) as rn from wg group by wg.a order by rn;
951
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
952
+
953
+ -- GROUP BY key written bare, window spec table-qualified.
954
+ select a, row_number() over (order by wg.a) as rn from wg group by a order by rn;
955
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
956
+
957
+ -- Same, alias-qualified through a FROM alias.
958
+ select a, row_number() over (order by w.a) as rn from wg w group by a order by rn;
959
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
960
+
961
+ -- A non-bare grouping key spelled out again in the window specification.
962
+ select a || '!' as k, row_number() over (order by a || '!') as rn from wg group by a || '!' order by rn;
963
+ → [{"k":"x!","rn":1},{"k":"y!","rn":2}]
964
+
965
+ -- Same, in PARTITION BY: each group is its own partition, so every count is 1.
966
+ select a || '!' as k, count(*) over (partition by a || '!') as c from wg group by a || '!' order by k;
967
+ → [{"k":"x!","c":1},{"k":"y!","c":1}]
968
+
969
+ -- The grouping key nested inside a larger window-specification expression: the
970
+ -- redirect recurses, so only the `a || '!'` subtree is rewritten.
971
+ select a || '!' as k, row_number() over (order by upper(a || '!')) as rn from wg group by a || '!' order by rn;
972
+ → [{"k":"x!","rn":1},{"k":"y!","rn":2}]
973
+
974
+ -- A window function ARGUMENT naming the grouping key another way works too.
975
+ select a, max(wg.a) over () as m from wg group by a order by a;
976
+ → [{"a":"x","m":"y"},{"a":"y","m":"y"}]
977
+
978
+ -- Identifier case is not part of a spelling: the fingerprint that matches a
979
+ -- non-bare grouping key folds identifier case.
980
+ select a || '!' as k, row_number() over (order by A || '!') as rn from wg group by a || '!' order by rn;
981
+ → [{"k":"x!","rn":1},{"k":"y!","rn":2}]
982
+
983
+ -- A quoted literal's case IS part of it, so `a || 'x'` is not the grouping key
984
+ -- `a || 'X'` and its bare `a` is an ungrouped reference.
985
+ select a || 'X' as k, row_number() over (order by a || 'x') as rn from wg group by a || 'X';
986
+ -- error: Column 'a' must appear in the GROUP BY clause or be used in an aggregate function
987
+
988
+ -- A qualified grouping key nested inside a larger window-specification expression.
989
+ select a, row_number() over (order by upper(wg.a)) as rn from wg group by a order by rn;
990
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
991
+
992
+ -- A wrapper around the grouping key survives the redirect.
993
+ select a, row_number() over (order by a collate nocase) as rn from wg group by a order by rn;
994
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
995
+
996
+ -- A *qualified* ungrouped column is not accidentally redirected onto a group key.
997
+ select a, row_number() over (order by wg.b) as rn from wg group by a;
998
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
999
+
1000
+ -- Neither is a same-shaped expression over the wrong column.
1001
+ select a || '!' as k, row_number() over (order by b || '!') as rn from wg group by a || '!';
1002
+ -- error: Column 'b' must appear in the GROUP BY clause or be used in an aggregate function
1003
+
1004
+ -- A grouping key may ALSO be named by the SELECT-list alias of the column that
1005
+ -- selects it, exactly as an *aggregate* alias can (see above). The alias is
1006
+ -- registered in the aggregate output scope beside the key's own name, so it
1007
+ -- resolves straight to the aggregate's group output column — the redirect above
1008
+ -- never has to see it.
1009
+ select a as k, row_number() over (order by k) as rn from wg group by a order by rn;
1010
+ → [{"k":"x","rn":1},{"k":"y","rn":2}]
1011
+
1012
+ -- In PARTITION BY as well. Each group is its own partition, so every count is 1.
1013
+ select a as k, count(*) over (partition by k) as c from wg group by a order by k;
1014
+ → [{"k":"x","c":1},{"k":"y","c":1}]
1015
+
1016
+ -- The alias may be defined AFTER the window function that names it: the scope is
1017
+ -- built from the whole select list before any window specification is.
1018
+ select count(*) over (partition by k) as w, a as k from wg group by a order by k;
1019
+ → [{"w":1,"k":"x"},{"w":1,"k":"y"}]
1020
+
1021
+ -- The GROUP BY key written qualified, the select-list column bare.
1022
+ select a as k, row_number() over (order by k) as rn from wg group by wg.a order by rn;
1023
+ → [{"k":"x","rn":1},{"k":"y","rn":2}]
1024
+
1025
+ -- A COMPUTED grouping key's alias resolves the same way — the select-list column is
1026
+ -- matched to its key by expression identity rather than by attribute.
1027
+ select upper(a) as k, row_number() over (order by k) as rn from wg group by upper(a) order by rn;
1028
+ → [{"k":"X","rn":1},{"k":"Y","rn":2}]
1029
+
1030
+ select a || '!' as k, row_number() over (order by k) as rn from wg group by a || '!' order by rn;
1031
+ → [{"k":"x!","rn":1},{"k":"y!","rn":2}]
1032
+
1033
+ -- Beside an aggregate alias, both resolving through the same scope.
1034
+ select a as k, count(*) as c, row_number() over (order by k) as rn from wg group by a order by rn;
1035
+ → [{"k":"x","c":2,"rn":1},{"k":"y","c":1,"rn":2}]
1036
+
1037
+ -- Identifier case is immaterial, in either direction.
1038
+ select a as k, row_number() over (order by K) as rn from wg group by a order by rn;
1039
+ → [{"k":"x","rn":1},{"k":"y","rn":2}]
1040
+
1041
+ -- The alias reaches inside a SUBQUERY in the specification, where it correlates back
1042
+ -- to the grouping key like any other spelling of it. `wg` has 2 rows with a='x' and 1
1043
+ -- with a='y', so y sorts first.
1044
+ select a as k, row_number() over (order by (select count(*) from wg t where t.a = k)) as rn
1045
+ from wg group by a order by rn;
1046
+ → [{"k":"y","rn":1},{"k":"x","rn":2}]
1047
+
1048
+ -- DELIBERATE: the alias SHADOWS a same-named base-table column. `b` is a real
1049
+ -- column of `wg`, but inside the specification the alias wins and `b` is the
1050
+ -- grouping key `a`. This is the rule an aggregate alias already had; it is not an
1051
+ -- accident of registration order.
1052
+ select a as b, row_number() over (order by b) as rn from wg group by a order by rn;
1053
+ → [{"b":"x","rn":1},{"b":"y","rn":2}]
1054
+
1055
+ -- DELIBERATE, the other way: an alias is the LOWEST-precedence name in that scope,
1056
+ -- so it does not outrank a GROUPING KEY of the same name. Here `b` is both the
1057
+ -- alias of the `a` key and a grouping key in its own right; `partition by b`
1058
+ -- partitions on the grouping key `b` (three distinct values → three partitions of
1059
+ -- one row), not on the alias (which would give two partitions of 2 and 1).
1060
+ select a as b, count(*) over (partition by b) as c from wg group by a, b order by b;
1061
+ → [{"b":"x","c":1},{"b":"x","c":1},{"b":"y","c":1}]
1062
+
1063
+ -- Two aliases naming DIFFERENT grouping keys collide, and nothing in the scope can
1064
+ -- arbitrate, so a bare reference to the shared name is ambiguous.
1065
+ select a as k, b as k, row_number() over (order by k) as rn from wg group by a, b;
1066
+ -- error: ambiguous column name: k
1067
+
1068
+ -- The SELECT list itself is NOT widened: a select-list column still cannot name a
1069
+ -- sibling column's alias. The list is built against the pre-aggregate scope, before
1070
+ -- the aggregate output scope exists.
1071
+ select a as k, k as k2 from wg group by a;
1072
+ -- error: Column not found: k
1073
+
1074
+ -- A quoted alias may contain a dot, which the scope's flat string key cannot tell
1075
+ -- apart from the qualified spelling of a grouping key (`wg.a`). The alias is skipped
1076
+ -- rather than registered, so this query keeps planning exactly as it did before
1077
+ -- aliases were registered at all — registering it raised the internal-sounding
1078
+ -- "Symbol 'wg.a' already exists in the same scope."
1079
+ select wg.a as "wg.a", row_number() over (order by a) as rn from wg group by wg.a order by rn;
1080
+ → [{"wg.a":"x","rn":1},{"wg.a":"y","rn":2}]
1081
+
1082
+ -- Same skip when the dotted alias names a DIFFERENT key than the qualified spelling
1083
+ -- it collides with: `"wg.b"` in the specification is the grouping key `b`, not the
1084
+ -- alias of `a`. Alias precedence is lowest, qualified names included.
1085
+ select wg.a as "wg.b", row_number() over (order by "wg.b") as rn from wg group by wg.a, wg.b order by rn;
1086
+ → [{"wg.b":"x","rn":1},{"wg.b":"y","rn":2},{"wg.b":"x","rn":3}]
1087
+
1088
+ -- An alias whose grouping key came from a GROUP BY ORDINAL resolves like any other:
1089
+ -- the ordinal is expanded to the select-list expression before the keys are indexed.
1090
+ select a as k, row_number() over (order by k) as rn from wg group by 1 order by rn;
1091
+ → [{"k":"x","rn":1},{"k":"y","rn":2}]
1092
+
1093
+ -- The alias survives composition: a grouped, windowed query whose specification names
1094
+ -- one is an ordinary subquery source, and a UNION arm of one.
1095
+ select * from (select a as k, count(*) over (partition by k) as c from wg group by a) z order by k;
1096
+ → [{"k":"x","c":1},{"k":"y","c":1}]
1097
+
1098
+ select a as k, count(*) over (partition by k) as c from wg group by a
1099
+ union all select a, 1 from wg order by k, c;
1100
+ → [{"k":"x","c":1},{"k":"x","c":1},{"k":"x","c":1},{"k":"y","c":1},{"k":"y","c":1}]
1101
+
1102
+ -- A grouped, windowed query composes as a subquery source: its window column is
1103
+ -- an ordinary output column to the enclosing query.
1104
+ with q as (select a, row_number() over (order by a) as rn from wg group by a)
1105
+ select * from q where rn > 1;
1106
+ → [{"a":"y","rn":2}]
1107
+
1108
+ -- A window specification may also contain a SUBQUERY that correlates back to the
1109
+ -- grouping key. The redirect and the coverage check both descend through the
1110
+ -- subquery to reach it, keying off the attribute ids the aggregate's INPUT
1111
+ -- produces so the subquery's own columns are left alone
1112
+ -- (bug-window-spec-subquery-reads-base-table-column — every shape below used to
1113
+ -- die with an internal "No row context found for column a" error).
1114
+
1115
+ -- Scalar subquery in ORDER BY: `max(b)` per group is '3' for a='x' and '2' for
1116
+ -- a='y', so ascending puts y first.
1117
+ select a, row_number() over (order by (select max(t.b) from wg t where t.a = wg.a)) as rn
1118
+ from wg group by a order by rn;
1119
+ → [{"a":"y","rn":1},{"a":"x","rn":2}]
1120
+
1121
+ -- `exists (…)` correlated on the grouping key: true only for the group that has a
1122
+ -- row with b > '2' (a='x'), so y sorts first.
1123
+ select a, row_number() over (order by (case when exists (select 1 from wg t where t.a = wg.a and t.b > '2') then 1 else 0 end)) as rn
1124
+ from wg group by a order by rn;
1125
+ → [{"a":"y","rn":1},{"a":"x","rn":2}]
1126
+
1127
+ -- `x in (select …)`, correlated on both sides of the membership test.
1128
+ select a, row_number() over (order by (case when wg.a in (select t.a from wg t where t.a = wg.a and t.b > '2') then 1 else 0 end)) as rn
1129
+ from wg group by a order by rn;
1130
+ → [{"a":"y","rn":1},{"a":"x","rn":2}]
1131
+
1132
+ -- In a window function's ARGUMENT rather than its specification: the per-group
1133
+ -- values '3' and '2' summed over the whole (unpartitioned) window.
1134
+ select a, sum((select max(t.b) from wg t where t.a = wg.a)) over () as s
1135
+ from wg group by a order by a;
1136
+ → [{"a":"x","s":5},{"a":"y","s":5}]
1137
+
1138
+ -- In PARTITION BY: the correlated subquery yields a distinct value per group, so
1139
+ -- every partition holds exactly one grouped row.
1140
+ select a, count(*) over (partition by (select max(t.b) from wg t where t.a = wg.a)) as c
1141
+ from wg group by a order by a;
1142
+ → [{"a":"x","c":1},{"a":"y","c":1}]
1143
+
1144
+ -- The mirror image, and the reason the redirect cannot simply match by name: a
1145
+ -- BARE `a` written inside the subquery names the subquery's OWN `t.a`, not the
1146
+ -- grouping key, so the predicate is the tautology `t.a = t.a` and every group sees
1147
+ -- the same global `max(b)`. One partition of two grouped rows, not two of one.
1148
+ select a, count(*) over (partition by (select max(t.b) from wg t where t.a = a)) as c
1149
+ from wg group by a order by a;
1150
+ → [{"a":"x","c":2},{"a":"y","c":2}]
1151
+
1152
+ -- A correlated reference to a NON-grouped column is rejected at plan time, with the
1153
+ -- same message a bare ungrouped column in a window specification gets.
1154
+ select a, row_number() over (order by (select max(t.b) from wg t where t.b = wg.b)) as rn
1155
+ from wg group by a;
1156
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1157
+
1158
+ -- Same rejection when the ungrouped column is only on one side of the correlation.
1159
+ select a, row_number() over (order by (select count(*) from wg t where t.a = wg.b)) as rn
1160
+ from wg group by a;
1161
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1162
+
1163
+ -- An UNCORRELATED subquery in a window specification reads nothing of this query's
1164
+ -- and is left entirely alone.
1165
+ select a, row_number() over (order by (select max(t.b) from wg t), a) as rn
1166
+ from wg group by a order by rn;
1167
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1168
+
1169
+ -- A window specification inside a GROUPED SUBQUERY may correlate to the ENCLOSING
1170
+ -- query. `o.a` is not one of the inner query's pre-grouping columns, so the inner
1171
+ -- coverage check has no business rejecting it — it used to, because its only
1172
+ -- vocabulary was "is this an aggregate output column".
1173
+ select o.a,
1174
+ (select count(*) from (select row_number() over (order by i.b, o.a) rn
1175
+ from wg i where i.a = o.a group by i.b) z) c
1176
+ from wg o group by o.a order by o.a;
1177
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1178
+
1179
+ -- A whole SUBQUERY can be the grouping key, and repeating it verbatim in the window
1180
+ -- specification must resolve to that key's output column. It is matched by identity
1181
+ -- fingerprint at the top level of the specification, where the expression is written
1182
+ -- in this query's own scope — the "does this subtree read only pre-grouping columns"
1183
+ -- test that guards the same rule INSIDE a subquery cannot be applied here, because a
1184
+ -- correlated grouping key legitimately reads the subquery's columns too. Two groups:
1185
+ -- max(b) is '3' for a='x' and '2' for a='y'.
1186
+ select count(*) as n, row_number() over (order by (select max(t.b) from wg t where t.a = wg.a)) as rn
1187
+ from wg group by (select max(t.b) from wg t where t.a = wg.a) order by rn;
1188
+ → [{"n":1,"rn":1},{"n":2,"rn":2}]
1189
+
1190
+ -- The mirror: an expression grouping key named from INSIDE a subquery in the
1191
+ -- specification. `wgk` has no column `a`, so the bare `a` resolves outward to this
1192
+ -- query's grouping key and IS redirected. Each group matches one `wgk` row, so both
1193
+ -- grouped rows land in the same partition.
1194
+ create table wgk (c text);
1195
+ insert into wgk values ('x!'), ('y!'), ('z!');
1196
+
1197
+ select a || '!' as k, count(*) over (partition by (select count(*) from wgk t where t.c = a || '!')) as c
1198
+ from wg group by a || '!' order by k;
1199
+ → [{"k":"x!","c":2},{"k":"y!","c":2}]
1200
+
1201
+ drop table wgk;
1202
+
1203
+ -- An aggregate inside a subquery in the window specification belongs to the
1204
+ -- SUBQUERY, so its arguments get no exemption from the coverage check: `wg.b` is a
1205
+ -- correlated reference to an ungrouped column of the grouped query and is rejected at
1206
+ -- plan time. (Only this query's OWN top-level aggregates read pre-grouping columns by
1207
+ -- definition — see the `count(*) over (order by count(*) desc)` cases above.)
1208
+ select a, row_number() over (order by (select max(wg.b) from wg t)) as rn
1209
+ from wg group by a;
1210
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1211
+
1212
+ select a, row_number() over (order by (select count(wg.b) from wg t where t.a = wg.a)) as rn
1213
+ from wg group by a;
1214
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1215
+
1216
+ -- The GROUPING key inside such an aggregate is legal, and is redirected onto the
1217
+ -- aggregate's output column: max('x'||b) is 'x3', max('y'||b) is 'y3'.
1218
+ select a, row_number() over (order by (select max(wg.a || t.b) from wg t)) as rn
1219
+ from wg group by a order by rn;
1220
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1221
+
1222
+ -- A CTE is planned ONCE and shared by every reference to it, so when the grouped
1223
+ -- query and a subquery in its window specification both read the same CTE, the CTE
1224
+ -- body is reachable from both. Its own columns are not this query's pre-grouping
1225
+ -- columns and must not be treated as such — this rejected with "Column 'b' must
1226
+ -- appear in the GROUP BY clause", naming a column written only inside the CTE.
1227
+ with c as (select a, b from wg where b <> '')
1228
+ select a, row_number() over (order by (select count(*) from c z), a) as rn
1229
+ from c group by a order by rn;
1230
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1231
+
1232
+ -- Same shape, correlated: the redirect still reaches the grouping key through the
1233
+ -- shared CTE. `max(b)` is '3' for a='x' and '2' for a='y', so y sorts first.
1234
+ with c as (select a, b from wg where b <> '')
1235
+ select a, row_number() over (order by (select max(t.b) from c t where t.a = c.a)) as rn
1236
+ from c group by a order by rn;
1237
+ → [{"a":"y","rn":1},{"a":"x","rn":2}]
1238
+
1239
+ -- And an ungrouped correlated reference is still rejected when the CTE is shared.
1240
+ with c as (select a, b from wg where b <> '')
1241
+ select a, row_number() over (order by (select max(t.b) from c t where t.b = c.b)) as rn
1242
+ from c group by a;
1243
+ -- error: Column 'c.b' must appear in the GROUP BY clause or be used in an aggregate function
1244
+
1245
+ -- A RECURSIVE CTE is shared the same way.
1246
+ with recursive r(n) as (select 1 union all select n + 1 from r where n < 3)
1247
+ select n, row_number() over (order by (select count(*) from r z), n) as rn
1248
+ from r group by n order by rn;
1249
+ → [{"n":1,"rn":1},{"n":2,"rn":2},{"n":3,"rn":3}]
1250
+
1251
+ -- =============================================================================
1252
+ -- The SELECT LIST of a grouped query names a grouping key.
1253
+ --
1254
+ -- Same claim as the window-specification cases above, one clause over: a
1255
+ -- select-list reference to a grouping key must resolve to the AggregateNode's own
1256
+ -- group output column, whatever spelling it used. The aggregate output scope only
1257
+ -- holds a grouping key under the names the query literally wrote, so a qualifier
1258
+ -- the GROUP BY did not use, a FROM-alias qualifier, or a key nested inside a
1259
+ -- bigger expression all fall through to a BASE-TABLE attribute the grouped row
1260
+ -- does not carry. `buildFinalAggregateProjections` runs the same grouping-key
1261
+ -- redirect the window phase does — both through `redirectPostAggregate`
1262
+ -- (bug-qualified-group-key-in-select-list-breaks-window-query).
1263
+ --
1264
+ -- Each windowed case below is paired with its NON-window twin. The twin answers
1265
+ -- correctly today only because `emit/aggregate.ts` publishes a representative
1266
+ -- source row of the group around each yield and the projection sits directly on
1267
+ -- that yield (docs/runtime.md § Corollary: a published source row reaches only the
1268
+ -- adjacent consumer) — the window function is simply the operator that buffers in
1269
+ -- between and exposes the mis-binding. Both spellings must bind the same way, so
1270
+ -- both are pinned.
1271
+ -- =============================================================================
1272
+
1273
+ -- The reported shape: table-qualified select-list key, bare GROUP BY. The output
1274
+ -- column is named `a`, not `wg.a` — the alias comes from the written column name
1275
+ -- and the redirect must not change it.
1276
+ select wg.a, row_number() over (order by a) as rn from wg group by a order by rn;
1277
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1278
+
1279
+ select wg.a, count(*) as c from wg group by a order by a;
1280
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1281
+
1282
+ -- Qualified through a FROM ALIAS rather than the table name. Attribute ids are
1283
+ -- minted per relation instance, so `w.a` and `wg.a` and bare `a` all land on the
1284
+ -- same base attribute and the match is qualifier-independent.
1285
+ select w.a, row_number() over (order by a) as rn from wg w group by a order by rn;
1286
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1287
+
1288
+ select w.a, count(*) as c from wg w group by a order by a;
1289
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1290
+
1291
+ -- With an explicit alias, which the redirect leaves alone.
1292
+ select wg.a as k, row_number() over (order by a) as rn from wg group by a order by rn;
1293
+ → [{"k":"x","rn":1},{"k":"y","rn":2}]
1294
+
1295
+ -- Alongside an aggregate, with the window ordered by that aggregate's alias.
1296
+ select wg.a, count(*) as c, row_number() over (order by c) as rn from wg group by a order by rn;
1297
+ → [{"a":"y","c":1,"rn":1},{"a":"x","c":2,"rn":2}]
1298
+
1299
+ -- Window specification qualified as well, so both redirects run on one query.
1300
+ select wg.a, row_number() over (order by wg.a) as rn from wg group by a order by rn;
1301
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1302
+
1303
+ -- A qualified grouping key NESTED inside a larger select-list expression: the walk
1304
+ -- recurses, so only the `wg.a` reference is rewritten.
1305
+ select upper(wg.a) as k, row_number() over (order by a) as rn from wg group by a order by rn;
1306
+ → [{"k":"X","rn":1},{"k":"Y","rn":2}]
1307
+
1308
+ select upper(wg.a) as k from wg group by a order by k;
1309
+ → [{"k":"X"},{"k":"Y"}]
1310
+
1311
+ -- A COMPUTED grouping key nested inside a larger select-list expression. The
1312
+ -- whole-expression fingerprint fast path cannot see this one (`upper(a || '!')` is
1313
+ -- not the key `a || '!'`); the redirect matches the inner subtree instead.
1314
+ select upper(a || '!') as k, row_number() over (order by a || '!') as rn from wg group by a || '!' order by rn;
1315
+ → [{"k":"X!","rn":1},{"k":"Y!","rn":2}]
1316
+
1317
+ select upper(a || '!') as k from wg group by a || '!' order by k;
1318
+ → [{"k":"X!"},{"k":"Y!"}]
1319
+
1320
+ -- A qualified key inside an ordinary binary expression.
1321
+ select wg.a || '?' as k, row_number() over (order by a) as rn from wg group by a order by rn;
1322
+ → [{"k":"x?","rn":1},{"k":"y?","rn":2}]
1323
+
1324
+ select wg.a || '?' as k from wg group by a order by k;
1325
+ → [{"k":"x?"},{"k":"y?"}]
1326
+
1327
+ -- NON-WINDOW REGRESSIONS. These all worked before the select-list redirect existed
1328
+ -- and must return exactly the same rows with it in place.
1329
+
1330
+ -- Bare key — already bound to the group column through the aggregate output scope.
1331
+ select a, count(*) as c from wg group by a order by a;
1332
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1333
+
1334
+ -- Whole-expression fingerprint fast path, which runs BEFORE the redirect and is
1335
+ -- left untouched by it.
1336
+ select a || '!' as k, count(*) as c from wg group by a || '!' order by k;
1337
+ → [{"k":"x!","c":2},{"k":"y!","c":1}]
1338
+
1339
+ -- The `SELECT *` arm emits group-column references of its own and is untouched.
1340
+ select * from wg group by a, b order by a, b;
1341
+ → [{"a":"x","b":"1"},{"a":"x","b":"3"},{"a":"y","b":"2"}]
1342
+
1343
+ select *, row_number() over (order by a, b) as rn from wg group by a, b order by rn;
1344
+ → [{"a":"x","b":"1","rn":1},{"a":"x","b":"3","rn":2},{"a":"y","b":"2","rn":3}]
1345
+
1346
+ -- A GROUP BY ORDINAL is expanded to the select-list expression before the keys are
1347
+ -- indexed, so the redirect sees an ordinary bare key.
1348
+ select a, count(*) as c from wg group by 1 order by a;
1349
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1350
+
1351
+ select a, count(*) as c, row_number() over (order by a) as rn from wg group by 1 order by rn;
1352
+ → [{"a":"x","c":2,"rn":1},{"a":"y","c":1,"rn":2}]
1353
+
1354
+ -- TWO projections onto the SAME grouping key. Both now carry the same input
1355
+ -- attribute id; the duplicate output name is disambiguated as `a:1`, exactly as a
1356
+ -- bare `select a, a` already was.
1357
+ select wg.a, a from wg group by a order by 1;
1358
+ → [{"a":"x","a:1":"x"},{"a":"y","a:1":"y"}]
1359
+
1360
+ select wg.a, a, row_number() over (order by a) as rn from wg group by a order by rn;
1361
+ → [{"a":"x","a:1":"x","rn":1},{"a":"y","a:1":"y","rn":2}]
1362
+
1363
+ -- HAVING resolves through its own hybrid scope; an aggregate reference resolves to
1364
+ -- the AggregateNode's output column and needs no redirect.
1365
+ select wg.a, count(*) as c, row_number() over (order by a) as rn from wg group by a having count(*) > 1;
1366
+ → [{"a":"x","c":2,"rn":1}]
1367
+
1368
+ -- HAVING may itself name the grouping key under a spelling its scope does not hold.
1369
+ -- Such a reference used to bind to the base-table attribute and read correctly only
1370
+ -- because the HAVING filter sits directly on the AggregateNode's yield, where the
1371
+ -- representative source row is still published. It now goes through the same
1372
+ -- grouping-key redirect as every other post-aggregate clause and reads the
1373
+ -- aggregate's own output column, so the predicate stays correct wherever the filter
1374
+ -- ends up relative to a buffering operator. Pinned windowed and unwindowed.
1375
+ select wg.a, count(*) as c from wg group by a having wg.a = 'x';
1376
+ → [{"a":"x","c":2}]
1377
+
1378
+ select wg.a, count(*) as c, row_number() over (order by a) as rn from wg group by a having wg.a = 'x';
1379
+ → [{"a":"x","c":2,"rn":1}]
1380
+
1381
+ select wg.a, count(*) as c from wg group by a having upper(wg.a) = 'X';
1382
+ → [{"a":"x","c":2}]
1383
+
1384
+ select wg.a, count(*) as c, row_number() over (order by a) as rn from wg group by a having upper(wg.a) = 'X';
1385
+ → [{"a":"x","c":2,"rn":1}]
1386
+
1387
+ -- A COMPUTED grouping key written out again in HAVING — matched by the whole-subtree
1388
+ -- fingerprint, redirected onto the aggregate's group output column.
1389
+ select a || '!' as k, count(*) as c from wg group by a || '!' having a || '!' = 'x!';
1390
+ → [{"k":"x!","c":2}]
1391
+
1392
+ select a || '!' as k, count(*) as c, row_number() over (order by a || '!') as rn from wg group by a || '!' having a || '!' = 'x!';
1393
+ → [{"k":"x!","c":2,"rn":1}]
1394
+
1395
+ -- A genuinely ungrouped HAVING column keeps its dedicated rejection, windowed or not.
1396
+ select wg.a, count(*) as c from wg group by a having b = '1';
1397
+ -- error: HAVING references non-grouped column 'b'
1398
+
1399
+ select wg.a, count(*) as c, row_number() over (order by a) as rn from wg group by a having b = '1';
1400
+ -- error: HAVING references non-grouped column 'b'
1401
+
1402
+ -- =============================================================================
1403
+ -- The ORDER BY of a grouped query names a grouping key.
1404
+ --
1405
+ -- The same claim as the SELECT-list cases above, one clause over. A sort key spelled
1406
+ -- in a way the projection output scope does not publish — a qualifier the GROUP BY did
1407
+ -- not use, a FROM-alias qualifier, the key nested inside a bigger expression, a
1408
+ -- computed key written out again — falls through to a BASE-TABLE attribute the grouped
1409
+ -- row does not carry. `applyOrderBy` runs the same grouping-key redirect through
1410
+ -- `redirectPostAggregate`, gated on "this key actually references a pre-grouping
1411
+ -- attribute" (bug-order-by-grouping-key-spelling-breaks-window-query).
1412
+ --
1413
+ -- Each windowed case is paired with its NON-window twin. The twin answered correctly
1414
+ -- before this fix only because its sort consumed the aggregate's yield through
1415
+ -- streaming operators, and `emit/aggregate.ts` publishes a representative source row of
1416
+ -- the group around each yield. A WindowNode drains its whole source before yielding, so
1417
+ -- by the time a sort above it runs every representative row is gone — which is what
1418
+ -- turned the mis-binding into "No row context found for column a".
1419
+ -- =============================================================================
1420
+
1421
+ -- The reported shape: table-qualified sort key, bare GROUP BY, both directions.
1422
+ select a, row_number() over (order by a) as rn from wg group by a order by wg.a;
1423
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1424
+
1425
+ select a, count(*) as c from wg group by a order by wg.a;
1426
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1427
+
1428
+ select a, row_number() over (order by a) as rn from wg group by a order by wg.a desc;
1429
+ → [{"a":"y","rn":2},{"a":"x","rn":1}]
1430
+
1431
+ select a, count(*) as c from wg group by a order by wg.a desc;
1432
+ → [{"a":"y","c":1},{"a":"x","c":2}]
1433
+
1434
+ -- Qualified through a FROM ALIAS rather than the table name — same base attribute, so
1435
+ -- the match is qualifier-independent.
1436
+ select w.a, row_number() over (order by a) as rn from wg w group by a order by w.a desc;
1437
+ → [{"a":"y","rn":2},{"a":"x","rn":1}]
1438
+
1439
+ select w.a, count(*) as c from wg w group by a order by w.a desc;
1440
+ → [{"a":"y","c":1},{"a":"x","c":2}]
1441
+
1442
+ -- The grouping key NESTED under a scalar function: the walk recurses, so only the
1443
+ -- `wg.a` reference is rewritten and the sort still evaluates `upper()` above it.
1444
+ select a, row_number() over (order by a) as rn from wg group by a order by upper(wg.a);
1445
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1446
+
1447
+ select a, count(*) as c from wg group by a order by upper(wg.a);
1448
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1449
+
1450
+ select a, row_number() over (order by a) as rn from wg group by a order by upper(wg.a) desc;
1451
+ → [{"a":"y","rn":2},{"a":"x","rn":1}]
1452
+
1453
+ select a, count(*) as c from wg group by a order by upper(wg.a) desc;
1454
+ → [{"a":"y","c":1},{"a":"x","c":2}]
1455
+
1456
+ -- The SELECT list and the ORDER BY both spell the key the same wrong way, so the
1457
+ -- select-list redirect and the ORDER BY redirect run on one query.
1458
+ select upper(wg.a) as k, row_number() over (order by a) as rn from wg group by a order by upper(wg.a) desc;
1459
+ → [{"k":"Y","rn":2},{"k":"X","rn":1}]
1460
+
1461
+ select upper(wg.a) as k, count(*) as c from wg group by a order by upper(wg.a) desc;
1462
+ → [{"k":"Y","c":1},{"k":"X","c":2}]
1463
+
1464
+ -- A COMPUTED grouping key repeated verbatim in ORDER BY — matched by the whole-subtree
1465
+ -- fingerprint rather than by attribute id.
1466
+ select a || '!' as k, row_number() over (order by a || '!') as rn from wg group by a || '!' order by a || '!';
1467
+ → [{"k":"x!","rn":1},{"k":"y!","rn":2}]
1468
+
1469
+ select a || '!' as k, count(*) as c from wg group by a || '!' order by a || '!';
1470
+ → [{"k":"x!","c":2},{"k":"y!","c":1}]
1471
+
1472
+ -- ...and the same computed key nested inside a bigger ORDER BY expression, which the
1473
+ -- whole-expression match cannot see (`upper(a || '!')` is not the key `a || '!'`).
1474
+ select a || '!' as k, row_number() over (order by a || '!') as rn from wg group by a || '!' order by upper(a || '!') desc;
1475
+ → [{"k":"y!","rn":2},{"k":"x!","rn":1}]
1476
+
1477
+ select a || '!' as k, count(*) as c from wg group by a || '!' order by upper(a || '!') desc;
1478
+ → [{"k":"y!","c":1},{"k":"x!","c":2}]
1479
+
1480
+ -- The select list ALIASES the key, so the alias is what the projection scope publishes
1481
+ -- and the qualified spelling still has to be redirected.
1482
+ select a as k, row_number() over (order by a) as rn from wg group by a order by wg.a desc;
1483
+ → [{"k":"y","rn":2},{"k":"x","rn":1}]
1484
+
1485
+ select a as k, count(*) as c from wg group by a order by wg.a desc;
1486
+ → [{"k":"y","c":1},{"k":"x","c":2}]
1487
+
1488
+ -- Alongside an aggregate in the select list.
1489
+ select wg.a, count(*) as c, row_number() over (order by a) as rn from wg group by a order by wg.a desc;
1490
+ → [{"a":"y","c":1,"rn":2},{"a":"x","c":2,"rn":1}]
1491
+
1492
+ select wg.a, count(*) as c from wg group by a order by wg.a desc;
1493
+ → [{"a":"y","c":1},{"a":"x","c":2}]
1494
+
1495
+ -- MIXED sort: a window output column (which resolves through the window output scope
1496
+ -- and gets no redirect) and a redirected grouping key in the same ORDER BY. The
1497
+ -- non-window twin does the same with an aggregate alias.
1498
+ select a, row_number() over (order by a) as rn from wg group by a order by rn desc, wg.a;
1499
+ → [{"a":"y","rn":2},{"a":"x","rn":1}]
1500
+
1501
+ select a, count(*) as c from wg group by a order by c desc, wg.a;
1502
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1503
+
1504
+ -- A CORRELATED subquery as the sort key, naming the grouping key at depth. Redirected
1505
+ -- by the base-attribute rule inside the subquery, so `x` (2 rows) sorts after `y` (1).
1506
+ select a, row_number() over (order by a) as rn from wg group by a order by (select count(*) from wg t where t.a = wg.a);
1507
+ → [{"a":"y","rn":2},{"a":"x","rn":1}]
1508
+
1509
+ select a, count(*) as c from wg group by a order by (select count(*) from wg t where t.a = wg.a);
1510
+ → [{"a":"y","c":1},{"a":"x","c":2}]
1511
+
1512
+ -- TWO grouping keys, sorting on the qualified spelling of the second one.
1513
+ select a, b, row_number() over (order by a) as rn from wg group by a, b order by wg.b desc;
1514
+ → [{"a":"x","b":"3","rn":2},{"a":"y","b":"2","rn":3},{"a":"x","b":"1","rn":1}]
1515
+
1516
+ select a, b, count(*) as c from wg group by a, b order by wg.b desc;
1517
+ → [{"a":"x","b":"3","c":1},{"a":"y","b":"2","c":1},{"a":"x","b":"1","c":1}]
1518
+
1519
+ -- HAVING and a redirected ORDER BY on the same query.
1520
+ select wg.a, count(*) as c, row_number() over (order by a) as rn from wg group by a having count(*) > 1 order by wg.a desc;
1521
+ → [{"a":"x","c":2,"rn":1}]
1522
+
1523
+ select wg.a, count(*) as c from wg group by a having count(*) > 1 order by wg.a desc;
1524
+ → [{"a":"x","c":2}]
1525
+
1526
+ -- REGRESSION PINS. The spellings the projection output scope already publishes — the
1527
+ -- bare key, an output alias, a window output column, an ordinal — resolve there and
1528
+ -- must keep resolving there. The redirect's gate is what protects them: an ungated
1529
+ -- fingerprint match would rewrite a correctly-bound `order by a` onto the
1530
+ -- AggregateNode's attribute and break a query that worked before this fix.
1531
+ select wg.a, row_number() over (order by a) as rn from wg group by a order by a;
1532
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1533
+
1534
+ select wg.a as k, row_number() over (order by a) as rn from wg group by a order by k;
1535
+ → [{"k":"x","rn":1},{"k":"y","rn":2}]
1536
+
1537
+ select a, row_number() over (order by a) as rn from wg group by a order by rn;
1538
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1539
+
1540
+ select wg.a, row_number() over (order by a) as rn from wg group by a order by 1;
1541
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1542
+
1543
+ select a, count(*) as c from wg group by a order by 1;
1544
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1545
+
1546
+ -- An ORDER BY naming a genuinely UNGROUPED column is now REJECTED at plan time,
1547
+ -- windowed and not. It used to be accepted and sorted by an arbitrary representative
1548
+ -- row's `b` — a wrong-result bug, and inconsistent with the SELECT list and HAVING,
1549
+ -- which already reject ungrouped columns. The finished-plan boundary check
1550
+ -- (`assertGroupedPlanCoverage`, see the NOTE at its call in select.ts) is what
1551
+ -- rejects it, with the same user-facing message the SELECT list uses.
1552
+ select a, row_number() over (order by a) as rn from wg group by a order by b;
1553
+ -- error: Column 'b' must appear in the GROUP BY clause or be used in an aggregate function
1554
+
1555
+ select a, count(*) as c from wg group by a order by b;
1556
+ -- error: Column 'b' must appear in the GROUP BY clause or be used in an aggregate function
1557
+
1558
+ -- A COMPUTED grouping key is matched by the whole expression's TEXT, so a qualifier
1559
+ -- the GROUP BY did not use makes it a different expression — and what is left is a
1560
+ -- bare reference to the ungrouped `a`. (A bare-column key is matched by attribute id
1561
+ -- instead, which is why `group by a order by wg.a` further down works under any
1562
+ -- qualifier.) The SELECT list has always rejected this spelling; ORDER BY used to
1563
+ -- accept it and sort by an arbitrary representative row, so the pin below is the
1564
+ -- ORDER BY half becoming consistent with the select-list half above it. Tracked as
1565
+ -- backlog/debt-group-key-match-by-attribute-identity.
1566
+ select upper(a) as k, count(*) as c from wg group by upper(a) order by upper(wg.a);
1567
+ -- error: Column 'wg.a' must appear in the GROUP BY clause or be used in an aggregate function
1568
+
1569
+ select upper(wg.a) as k, count(*) as c from wg group by upper(a);
1570
+ -- error: Column 'wg.a' must appear in the GROUP BY clause or be used in an aggregate function
1571
+
1572
+ -- Control: one spelling on both sides matches, windowed and not.
1573
+ select upper(a) as k, count(*) as c from wg group by upper(a) order by upper(a);
1574
+ → [{"k":"X","c":2},{"k":"Y","c":1}]
1575
+
1576
+ select upper(a) as k, row_number() over (order by upper(a)) as rn from wg group by upper(a);
1577
+ → [{"k":"X","rn":1},{"k":"Y","rn":2}]
1578
+
1579
+ -- The `preWindowSort` placement (an ORDER BY column absent from the select list gets
1580
+ -- its sort BELOW the window projection) with a QUALIFIED spelling of a grouping key:
1581
+ -- that sort still sits ABOVE the AggregateNode, so its key is redirected onto the
1582
+ -- aggregate's own output column like every other post-aggregate sort key. (The
1583
+ -- window's own ordering wins on output — pre-existing preWindowSort semantics; what
1584
+ -- these pin is that the qualified key plans and evaluates instead of dying.)
1585
+ select a, row_number() over (order by a) as rn from wg group by a, b order by wg.b;
1586
+ → [{"a":"x","rn":1},{"a":"x","rn":2},{"a":"y","rn":3}]
1587
+
1588
+ select a, row_number() over (order by a) as rn from wg group by a, b order by wg.b desc;
1589
+ → [{"a":"x","rn":1},{"a":"x","rn":2},{"a":"y","rn":3}]
1590
+
1591
+ -- The EARLY ORDER BY placement (taken when ORDER BY introduces an aggregate the
1592
+ -- select list does not have) alongside a qualified grouping-key spelling: both keys
1593
+ -- of one sort, one resolved through the aggregates context, one redirected.
1594
+ select a from wg group by a order by count(*) desc, wg.a;
1595
+ → [{"a":"x"},{"a":"y"}]
1596
+
1597
+ select a from wg group by a order by count(*), wg.a;
1598
+ → [{"a":"y"},{"a":"x"}]
1599
+
1600
+ -- The grouping key is NOT in the select list, so no projection output column carries it
1601
+ -- and the redirect is the only thing that can bind the sort key. A non-column ORDER BY
1602
+ -- expression, so `preWindowSort` does not divert it either — this reaches the redirect
1603
+ -- and reads the AggregateNode's group column through the WindowNode's passthrough.
1604
+ select count(*) as c, row_number() over (order by count(*)) as rn from wg group by a order by upper(wg.a) desc;
1605
+ → [{"c":1,"rn":1},{"c":2,"rn":2}]
1606
+
1607
+ select count(*) as c from wg group by a order by upper(wg.a) desc;
1608
+ → [{"c":1},{"c":2}]
1609
+
1610
+ -- DISTINCT sits between the projection and the sort, so the redirected key is read
1611
+ -- through one more operator than the cases above.
1612
+ select distinct a, row_number() over (order by a) as rn from wg group by a order by wg.a desc;
1613
+ → [{"a":"y","rn":2},{"a":"x","rn":1}]
1614
+
1615
+ select distinct a, count(*) as c from wg group by a order by wg.a desc;
1616
+ → [{"a":"y","c":1},{"a":"x","c":2}]
1617
+
1618
+ -- =============================================================================
1619
+ -- SELECT LIST of a grouped query, continued.
1620
+ -- =============================================================================
1621
+
1622
+ -- An UNGROUPED select-list column is still rejected at plan time, with the same
1623
+ -- message and the same wording, window function or not. `validateAggregateProjections`
1624
+ -- runs on the pre-aggregate projections and never sees the redirect.
1625
+ select wg.b, row_number() over (order by a) as rn from wg group by a;
1626
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1627
+
1628
+ select wg.b from wg group by a;
1629
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1630
+
1631
+ -- A SUBQUERY in the select list whose text spells the grouping key but resolves to
1632
+ -- its OWN column keeps the inner meaning: inside a subquery the fingerprint rule is
1633
+ -- guarded by "does this subtree read only pre-grouping columns of THIS query", and
1634
+ -- `t.a` is the subquery's. So the predicate is the tautology `t.a = t.a` and every
1635
+ -- group counts all 3 rows.
1636
+ select a, (select count(*) from wg t where t.a = a) as c from wg group by a order by a;
1637
+ → [{"a":"x","c":3},{"a":"y","c":3}]
1638
+
1639
+ select a, (select count(*) from wg t where t.a = a) as c, row_number() over (order by a) as rn from wg group by a order by a;
1640
+ → [{"a":"x","c":3,"rn":1},{"a":"y","c":3,"rn":2}]
1641
+
1642
+ -- A genuinely CORRELATED select-list subquery naming the grouping key is redirected
1643
+ -- through the subquery, by attribute id, at any depth — it used to die with an
1644
+ -- internal "No row context found for column a".
1645
+ select a, (select count(*) from wg t where t.a = wg.a) as c from wg group by a order by a;
1646
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1647
+
1648
+ select a, (select t.b from wg t where t.a = wg.a and t.b = '1') as c, row_number() over (order by a) as rn
1649
+ from wg group by a order by rn;
1650
+ → [{"a":"x","c":"1","rn":1},{"a":"y","c":null,"rn":2}]
1651
+
1652
+ -- A correlated subquery reading a genuinely UNGROUPED column of this query is
1653
+ -- rejected at plan time by the finished-plan boundary check — it used to plan and
1654
+ -- then read an arbitrary representative row (or die at run time with an internal
1655
+ -- "No row context found" error once a WindowNode sat in between). The subquery's own
1656
+ -- `t.b` is a different attribute instance and stays legal; only the correlated
1657
+ -- `wg.b` is flagged.
1658
+ select a, (select count(*) from wg t where t.b = wg.b) as c from wg group by a;
1659
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1660
+
1661
+ select a, (select count(*) from wg t where t.b = wg.b) as c, row_number() over (order by a) as rn from wg group by a;
1662
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1663
+
1664
+ select a, count(*) as c from wg group by a order by (select count(*) from wg t where t.b = wg.b);
1665
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1666
+
1667
+ -- The check walks the finished PLAN, not any one clause, so the other correlated
1668
+ -- shapes reject identically: an EXISTS and an IN, each correlated on the ungrouped
1669
+ -- `wg.b`. (HAVING's own bare-column rejection never sees these — its check does not
1670
+ -- descend into a subquery.)
1671
+ select a, count(*) as c from wg group by a having exists (select 1 from wg t where t.a = wg.b);
1672
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1673
+
1674
+ select a from wg group by a having a in (select t.a from wg t where t.b = wg.b);
1675
+ -- error: Column 'wg.b' must appear in the GROUP BY clause or be used in an aggregate function
1676
+
1677
+ -- Control: the same two shapes correlated on the GROUPING KEY are redirected and run.
1678
+ select a, count(*) as c from wg group by a having exists (select 1 from wg t where t.a = wg.a) order by wg.a;
1679
+ → [{"a":"x","c":2},{"a":"y","c":1}]
1680
+
1681
+ select a from wg group by a having a in (select t.a from wg t where t.a = wg.a) order by wg.a;
1682
+ → [{"a":"x"},{"a":"y"}]
1683
+
1684
+ -- =============================================================================
1685
+ -- A WINDOW RESULT COLUMN IS ADDRESSED BY ATTRIBUTE IDENTITY, NOT POSITION.
1686
+ -- Regression for bug-window-column-read-by-position-hits-wrong-row: the window
1687
+ -- phase used to hand the projection a POSITIONAL read of the WindowNode's row,
1688
+ -- which silently read whatever row context was newest. Whenever decorrelation
1689
+ -- placed a join between the WindowNode and its projection, the window column
1690
+ -- came back as a verbatim copy of the subquery's value. Non-numeric text values
1691
+ -- ('p','q','r') on purpose: numeric-looking text picks up numeric affinity on
1692
+ -- the way through and muddies WHICH column is wrong.
1693
+ -- =============================================================================
1694
+ create table wsq (a text, b text);
1695
+ insert into wsq values ('x','p'),('y','q'),('x','r');
1696
+
1697
+ -- Correlated scalar-aggregate subquery (routes through rule-scalar-agg-decorrelation,
1698
+ -- which puts a grouped LEFT JOIN above the WindowNode) alongside each window shape.
1699
+ -- `n` used to come back as a copy of `c`.
1700
+ select k, (select min(t.b) from wsq t where t.a = k) as c, count(*) over () as n from (select a as k from wsq) group by k;
1701
+ → [{"k":"x","c":"p","n":2},{"k":"y","c":"q","n":2}]
1702
+
1703
+ select k, (select min(t.b) from wsq t where t.a = k) as c, row_number() over (order by k) as rn from (select a as k from wsq) group by k;
1704
+ → [{"k":"x","c":"p","rn":1},{"k":"y","c":"q","rn":2}]
1705
+
1706
+ -- The desc spelling additionally needs the window's honest ordering advertisement
1707
+ -- (bug-window-node-advertises-source-row-order) so the merge join above it does
1708
+ -- not drop rows.
1709
+ select k, (select min(t.b) from wsq t where t.a = k) as c, row_number() over (order by k desc) as rn from (select a as k from wsq) group by k;
1710
+ → [{"k":"y","c":"q","rn":1},{"k":"x","c":"p","rn":2}]
1711
+
1712
+ -- The same three shapes with an EXISTS and with an IN correlated subquery, which
1713
+ -- route through rule-subquery-decorrelation instead.
1714
+ select a, exists (select 1 from wsq t where t.a = wsq.a and t.b = 'p') as e, count(*) over () as n from wsq group by a;
1715
+ → [{"a":"x","e":true,"n":2},{"a":"y","e":false,"n":2}]
1716
+
1717
+ select a, exists (select 1 from wsq t where t.a = wsq.a and t.b = 'p') as e, row_number() over (order by a) as rn from wsq group by a;
1718
+ → [{"a":"x","e":true,"rn":1},{"a":"y","e":false,"rn":2}]
1719
+
1720
+ select a, exists (select 1 from wsq t where t.a = wsq.a and t.b = 'p') as e, row_number() over (order by a desc) as rn from wsq group by a;
1721
+ → [{"a":"y","e":false,"rn":1},{"a":"x","e":true,"rn":2}]
1722
+
1723
+ select a, 'p' in (select t.b from wsq t where t.a = wsq.a) as i, count(*) over () as n from wsq group by a;
1724
+ → [{"a":"x","i":true,"n":2},{"a":"y","i":false,"n":2}]
1725
+
1726
+ select a, 'p' in (select t.b from wsq t where t.a = wsq.a) as i, row_number() over (order by a) as rn from wsq group by a;
1727
+ → [{"a":"x","i":true,"rn":1},{"a":"y","i":false,"rn":2}]
1728
+
1729
+ select a, 'p' in (select t.b from wsq t where t.a = wsq.a) as i, row_number() over (order by a desc) as rn from wsq group by a;
1730
+ → [{"a":"y","i":false,"rn":1},{"a":"x","i":true,"rn":2}]
1731
+
1732
+ -- UNGROUPED windowed query with a correlated subquery — was already correct (the
1733
+ -- streaming window path keeps the window's own row newest); regression guard.
1734
+ select a, (select min(t.b) from wsq t where t.a = wsq.a) as c, row_number() over (order by a) as rn from wsq order by rn;
1735
+ → [{"a":"x","c":"p","rn":1},{"a":"x","c":"p","rn":2},{"a":"y","c":"q","rn":3}]
1736
+
1737
+ -- Two window functions with DIFFERENT specifications alongside the decorrelated
1738
+ -- subquery, so a mis-addressed read cannot be masked by both columns holding the
1739
+ -- same value.
1740
+ select k, (select min(t.b) from wsq t where t.a = k) as c, count(*) over () as n, row_number() over (order by k) as rn from (select a as k from wsq) group by k;
1741
+ → [{"k":"x","c":"p","n":2,"rn":1},{"k":"y","c":"q","n":2,"rn":2}]
1742
+
1743
+ -- Two window functions with the SAME specification now genuinely share one
1744
+ -- WindowNode; each must still resolve to its own distinct output column — the
1745
+ -- case the old source-location accident in the spec grouping was protecting.
1746
+ select a, row_number() over (order by a) as rn, count(*) over (order by a) as rc from wsq group by a;
1747
+ → [{"a":"x","rn":1,"rc":1},{"a":"y","rn":2,"rc":2}]
1748
+
1749
+ -- Same-NAME functions sharing one specification (and so one WindowNode) keep
1750
+ -- their own argument expressions.
1751
+ select b, sum(length(b)) over (order by b) as s1, sum(length(b) * 10) over (order by b) as s2 from wsq order by b;
1752
+ → [{"b":"p","s1":1,"s2":10},{"b":"q","s1":2,"s2":20},{"b":"r","s1":3,"s2":30}]
1753
+
1754
+ -- A PARTITIONED window beside the decorrelated subquery: the partitioned buffered
1755
+ -- path emits whole partitions in first-seen order and advertises neither ordering
1756
+ -- nor monotonicity, so the join above it sees a different shape than the sorted
1757
+ -- cases above.
1758
+ select a, (select min(t.b) from wsq t where t.a = wsq.a) as c, count(*) over (partition by a) as n from wsq group by a;
1759
+ → [{"a":"x","c":"p","n":1},{"a":"y","c":"q","n":1}]
1760
+
1761
+ -- Two functions with ARGUMENTS sharing one specification (so one WindowNode, whose
1762
+ -- per-function argument arrays must stay aligned with its output columns).
1763
+ select a, (select min(t.b) from wsq t where t.a = wsq.a) as c, lag(a) over (order by a) as lg, lead(a) over (order by a) as ld from wsq group by a;
1764
+ → [{"a":"x","c":"p","lg":null,"ld":"y"},{"a":"y","c":"q","lg":"x","ld":null}]
1765
+
1766
+ -- A filter over a window output column through a subquery. The filter references
1767
+ -- a real window attribute the window's SOURCE does not carry, so the availability
1768
+ -- check must keep it above the WindowNode — a future pushdown rule that loses
1769
+ -- this returns [{"a":"x",...}] too.
1770
+ select * from (select a, row_number() over (order by a) as rn from wsq group by a) where rn > 1;
1771
+ → [{"a":"y","rn":2}]
1772
+
1773
+ drop table wsq;
1774
+
1775
+ -- Composition: the fixed shape as a subquery source, inside a CTE, and as a
1776
+ -- `union all` arm.
1777
+ select * from (select wg.a, row_number() over (order by a) as rn from wg group by a) z order by rn;
1778
+ → [{"a":"x","rn":1},{"a":"y","rn":2}]
1779
+
1780
+ with q as (select wg.a, row_number() over (order by a) as rn from wg group by a)
1781
+ select * from q where rn > 1;
1782
+ → [{"a":"y","rn":2}]
1783
+
1784
+ select wg.a as k, row_number() over (order by a) as rn from wg group by a
1785
+ union all select b, 9 from wg order by k, rn;
1786
+ → [{"k":"1","rn":9},{"k":"2","rn":9},{"k":"3","rn":9},{"k":"x","rn":1},{"k":"y","rn":2}]
1787
+
1788
+ -- An EMPTY grouped, windowed query: no group ever yields, so no representative
1789
+ -- source row is ever published. Exercises the path with the fallback absent.
1790
+ delete from wg;
1791
+ select wg.a, row_number() over (order by a) as rn from wg group by a;
1792
+ → []
1793
+
1794
+ drop table wg;
1795
+
1796
+ -- A NULL grouping key forms one group of its own and must appear exactly once.
1797
+ create table wn (id integer primary key, a text null);
1798
+ insert into wn values (1,'x'),(2,null),(3,'x');
1799
+
1800
+ select wn.a, row_number() over (order by a) as rn from wn group by a order by rn;
1801
+ → [{"a":null,"rn":1},{"a":"x","rn":2}]
1802
+
1803
+ select wn.a, count(*) as c from wn group by a order by c;
1804
+ → [{"a":null,"c":1},{"a":"x","c":2}]
1805
+
1806
+ drop table wn;
1807
+
1808
+ -- Two grouping keys sharing a bare name across a join. Their QUALIFIED names are
1809
+ -- registered by the aggregate output scope, so those references resolve there and
1810
+ -- never reach the redirect; the bare name stays ambiguous. Pinned windowed and
1811
+ -- unwindowed so the redirect is proved not to have disturbed the ambiguity.
1812
+ create table ji (id text, v text);
1813
+ create table jc (id text, w text);
1814
+ insert into ji values ('k1','a'),('k2','b');
1815
+ insert into jc values ('k1','p'),('k2','q');
1816
+
1817
+ select i.id, c.id, row_number() over (order by i.id) as rn
1818
+ from ji i join jc c on i.id = c.id group by i.id, c.id order by rn;
1819
+ → [{"id":"k1","id:1":"k1","rn":1},{"id":"k2","id:1":"k2","rn":2}]
1820
+
1821
+ select i.id, c.id from ji i join jc c on i.id = c.id group by i.id, c.id order by 1;
1822
+ → [{"id":"k1","id:1":"k1"},{"id":"k2","id:1":"k2"}]
1823
+
1824
+ select id, row_number() over (order by i.id) as rn
1825
+ from ji i join jc c on i.id = c.id group by i.id, c.id;
1826
+ -- error: ambiguous column name: id
1827
+
1828
+ drop table ji;
1829
+ drop table jc;
1830
+
1831
+ -- =============================================================================
1832
+ -- `*` in a window query.
1833
+ --
1834
+ -- The window phase's projection used to walk the AST select list and handle only
1835
+ -- non-star entries, so a `*` was skipped outright and its columns never reached
1836
+ -- the output. The projection is now the query's ONE select-list projection, with
1837
+ -- stars already expanded, so a star is an ordinary entry.
1838
+ --
1839
+ -- Column NAMES and ORDER are asserted in test/plan/grouped-projection-shape.spec.ts
1840
+ -- (row objects compare key-order-insensitively here); this file pins the values.
1841
+ -- =============================================================================
1842
+
1843
+ create table sw (v integer primary key, g text);
1844
+ insert into sw values (1,'a'),(2,'b');
1845
+
1846
+ -- Star before the window column.
1847
+ select *, row_number() over (order by v) as w from sw order by v;
1848
+ → [{"v":1,"g":"a","w":1},{"v":2,"g":"b","w":2}]
1849
+
1850
+ -- Star after the window column.
1851
+ select row_number() over (order by v) as w, * from sw order by v;
1852
+ → [{"w":1,"v":1,"g":"a"},{"w":2,"v":2,"g":"b"}]
1853
+
1854
+ -- A named column repeated by a later star is disambiguated as `v:1`.
1855
+ select v, row_number() over (order by v) as w, * from sw order by w;
1856
+ → [{"v":1,"w":1,"v:1":1,"g":"a"},{"v":2,"w":2,"v:1":2,"g":"b"}]
1857
+
1858
+ -- Qualified star.
1859
+ select sw.*, row_number() over (order by v) as w from sw order by v;
1860
+ → [{"v":1,"g":"a","w":1},{"v":2,"g":"b","w":2}]
1861
+
1862
+ -- A positional ORDER BY over a star-bearing window query binds the window column
1863
+ -- (position 3 = the column after the two star columns).
1864
+ select *, row_number() over (order by v) as w from sw order by 3 desc;
1865
+ → [{"v":2,"g":"b","w":2},{"v":1,"g":"a","w":1}]
1866
+
1867
+ -- DISTINCT over a window column.
1868
+ select distinct row_number() over (order by v) as w from sw order by w;
1869
+ → [{"w":1},{"w":2}]
1870
+
1871
+ -- An UNALIASED window column keeps its authored expression as its output name,
1872
+ -- like every other unaliased select-list column, rather than the name of the
1873
+ -- window-output column the rewrite substitutes in.
1874
+ select row_number() over (order by v) from sw order by v;
1875
+ → [{"row_number() over (order by v)":1},{"row_number() over (order by v)":2}]
1876
+
1877
+ select v, 1000 - row_number() over (order by v) from sw order by v;
1878
+ → [{"v":1,"1000 - row_number() over (order by v)":999},{"v":2,"1000 - row_number() over (order by v)":998}]
1879
+
1880
+ drop table sw;
1881
+
1882
+ -- =============================================================================
1883
+ -- The window node's advertised row order must describe what the window emitter
1884
+ -- actually yields (bug-window-node-advertises-source-row-order). A window that
1885
+ -- sorts its own rows used to keep advertising the SOURCE's order, and a merge
1886
+ -- join above it walked the reordered stream as if it were still in that order —
1887
+ -- silently dropping every row after the first mismatch.
1888
+ -- =============================================================================
1889
+
1890
+ create table wmj (a text, b text);
1891
+ insert into wmj values ('x','p'),('y','q'),('x','r'),('z','s');
1892
+
1893
+ -- The repro: `order by a desc` inside the window reverses the grouped rows, so
1894
+ -- the join above it must not assume ascending `a`. All four rows must survive.
1895
+ select z.a, z.rn, w2.b
1896
+ from (select a, row_number() over (order by a desc) as rn from wmj group by a) z
1897
+ join wmj w2 on w2.a = z.a
1898
+ order by z.a, w2.b;
1899
+ → [{"a":"x","rn":3,"b":"p"},{"a":"x","rn":3,"b":"r"},{"a":"y","rn":2,"b":"q"},{"a":"z","rn":1,"b":"s"}]
1900
+
1901
+ -- The ascending twin, which was already correct and must stay correct: here the
1902
+ -- window's emit order genuinely does match the source's.
1903
+ select z.a, z.rn, w2.b
1904
+ from (select a, row_number() over (order by a) as rn from wmj group by a) z
1905
+ join wmj w2 on w2.a = z.a
1906
+ order by z.a, w2.b;
1907
+ → [{"a":"x","rn":1,"b":"p"},{"a":"x","rn":1,"b":"r"},{"a":"y","rn":2,"b":"q"},{"a":"z","rn":3,"b":"s"}]
1908
+
1909
+ -- Same shape as a LEFT JOIN: a lost match would show up as NULL fill rather
1910
+ -- than as a missing row.
1911
+ select z.a, z.rn, w2.b
1912
+ from (select a, row_number() over (order by a desc) as rn from wmj group by a) z
1913
+ left join wmj w2 on w2.a = z.a
1914
+ order by z.a, w2.b;
1915
+ → [{"a":"x","rn":3,"b":"p"},{"a":"x","rn":3,"b":"r"},{"a":"y","rn":2,"b":"q"},{"a":"z","rn":1,"b":"s"}]
1916
+
1917
+ drop table wmj;
1918
+
1919
+ -- A PARTITION BY window buffers the source and re-emits it a whole partition at
1920
+ -- a time, so the scan's key order does NOT survive it. The outer ORDER BY must
1921
+ -- be honoured rather than elided against the source's ordering.
1922
+ create table wpo (k integer primary key, g text, v integer);
1923
+ insert into wpo values (1,'a',10),(2,'b',20),(3,'a',30),(4,'b',40);
1924
+
1925
+ select k, g, sum(v) over (partition by g) as s from wpo order by k;
1926
+ → [{"k":1,"g":"a","s":40},{"k":2,"g":"b","s":60},{"k":3,"g":"a","s":40},{"k":4,"g":"b","s":60}]
1927
+
1928
+ -- Same, joined against the partition column: the join above the partitioned
1929
+ -- window may not assume the window preserved the scan's order either.
1930
+ select w.k, w.s, o.g
1931
+ from (select k, g, sum(v) over (partition by g) as s from wpo) w
1932
+ join wpo o on o.k = w.k
1933
+ order by w.k;
1934
+ → [{"k":1,"s":40,"g":"a"},{"k":2,"s":60,"g":"b"},{"k":3,"s":40,"g":"a"},{"k":4,"s":60,"g":"b"}]
1935
+
1936
+ -- An unpartitioned window with a descending ORDER BY, consumed by an outer
1937
+ -- ascending ORDER BY: the outer sort must survive, since the window emits desc.
1938
+ select k, v, row_number() over (order by v desc) as rn from wpo order by k;
1939
+ → [{"k":1,"v":10,"rn":4},{"k":2,"v":20,"rn":3},{"k":3,"v":30,"rn":2},{"k":4,"v":40,"rn":1}]
1940
+
1941
+ drop table wpo;