From 4f7e0bd04709a616dd3f25a1cd0ec4aeffbf794a Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii Date: Tue, 28 Jul 2026 11:04:32 +0900 Subject: [PATCH v50 7/9] Row pattern recognition patch (tests: SQL, data). --- src/test/regress/data/stock.data | 1632 ++++++++ src/test/regress/parallel_schedule | 5 + src/test/regress/sql/rpr.sql | 2236 ++++++++++ src/test/regress/sql/rpr_base.sql | 4827 ++++++++++++++++++++++ src/test/regress/sql/rpr_explain.sql | 3635 ++++++++++++++++ src/test/regress/sql/rpr_integration.sql | 1009 +++++ src/test/regress/sql/rpr_nfa.sql | 3675 ++++++++++++++++ src/test/regress/sql/window.sql | 8 + 8 files changed, 17027 insertions(+) create mode 100644 src/test/regress/data/stock.data create mode 100644 src/test/regress/sql/rpr.sql create mode 100644 src/test/regress/sql/rpr_base.sql create mode 100644 src/test/regress/sql/rpr_explain.sql create mode 100644 src/test/regress/sql/rpr_integration.sql create mode 100644 src/test/regress/sql/rpr_nfa.sql diff --git a/src/test/regress/data/stock.data b/src/test/regress/data/stock.data new file mode 100644 index 00000000000..645bb643df7 --- /dev/null +++ b/src/test/regress/data/stock.data @@ -0,0 +1,1632 @@ +1 1 364.750 15800 0.000 0.000 0.000 +1 2 368.250 21200 0.000 0.000 0.000 +1 3 368.500 24400 0.000 0.000 0.000 +1 4 368.750 22900 0.000 0.000 0.000 +1 5 369.500 35400 0.000 0.000 0.000 +1 6 369.000 29300 0.000 0.000 0.000 +1 7 367.750 19000 0.000 0.000 0.000 +1 8 374.125 37200 0.000 0.000 0.000 +2 9 343.250 56800 0.000 0.000 0.000 +2 10 338.500 45200 0.000 0.000 0.000 +2 11 338.750 40900 0.000 0.000 0.000 +2 12 344.750 35200 0.000 0.000 0.000 +2 13 349.750 28500 0.000 0.000 0.000 +2 14 346.000 25600 0.000 0.000 0.000 +3 15 349.000 24900 0.000 0.000 0.000 +3 16 349.250 20700 0.000 0.000 0.000 +3 17 349.750 22200 0.000 0.000 0.000 +3 18 351.250 19600 0.000 0.000 0.000 +3 19 352.000 32500 0.000 0.000 0.000 +3 20 349.500 32200 0.000 0.000 0.000 +3 21 344.500 18600 0.000 0.000 0.000 +3 22 345.000 61200 0.000 0.000 0.000 +4 23 238.250 37100 0.000 0.000 0.000 +4 24 230.625 61500 0.000 0.000 0.000 +4 25 223.000 161700 0.000 0.000 0.000 +4 26 229.875 55300 0.000 0.000 0.000 +4 27 235.250 38500 0.000 0.000 0.000 +5 28 310.000 36000 0.000 0.000 0.000 +5 29 311.750 26800 0.000 0.000 0.000 +5 30 312.750 31900 0.000 0.000 0.000 +5 31 314.750 21100 0.000 0.000 0.000 +5 32 315.500 27400 0.000 0.000 0.000 +5 33 316.500 40700 0.000 0.000 0.000 +5 34 321.750 53500 0.000 0.000 0.000 +5 35 326.000 48700 0.000 0.000 0.000 +5 36 322.500 46000 0.000 0.000 0.000 +5 37 317.500 35600 0.000 0.000 0.000 +5 38 325.500 34000 0.000 0.000 0.000 +5 39 327.250 30700 0.000 0.000 0.000 +5 40 327.750 36700 0.000 0.000 0.000 +5 41 328.625 23200 0.000 0.000 0.000 +5 42 330.500 29700 0.000 0.000 0.000 +5 43 336.500 39200 0.000 0.000 0.000 +5 44 340.750 34400 0.000 0.000 0.000 +5 45 338.500 32200 0.000 0.000 0.000 +6 46 353.000 27800 0.000 0.000 0.000 +6 47 352.000 55600 0.000 0.000 0.000 +6 48 355.000 31300 0.000 0.000 0.000 +6 49 356.000 26900 0.000 0.000 0.000 +6 50 358.000 18900 0.000 0.000 0.000 +6 51 361.250 26400 0.000 0.000 0.000 +7 52 311.750 25100 0.000 0.000 0.000 +7 53 314.000 37900 0.000 0.000 0.000 +7 54 317.000 34000 0.000 0.000 0.000 +7 55 317.750 23700 0.000 0.000 0.000 +7 56 319.000 23400 0.000 0.000 0.000 +7 57 316.500 24200 0.000 0.000 0.000 +8 58 316.000 37200 0.000 0.000 0.000 +8 59 315.000 31500 0.000 0.000 0.000 +8 60 302.000 124700 0.000 0.000 0.000 +8 61 300.000 196100 0.000 0.000 0.000 +8 62 298.000 70000 0.000 0.000 0.000 +8 63 294.500 88200 0.000 0.000 0.000 +8 64 296.500 53800 0.000 0.000 0.000 +8 65 299.000 47900 0.000 0.000 0.000 +8 66 301.750 33300 0.000 0.000 0.000 +8 67 299.500 31800 0.000 0.000 0.000 +9 68 299.500 76700 0.000 0.000 0.000 +9 69 296.750 41900 0.000 0.000 0.000 +9 70 291.250 46700 0.000 0.000 0.000 +9 71 293.750 31100 0.000 0.000 0.000 +9 72 290.000 48900 0.000 0.000 0.000 +9 73 287.000 56300 0.000 0.000 0.000 +9 74 287.000 40700 0.000 0.000 0.000 +9 75 285.500 27900 0.000 0.000 0.000 +9 76 284.000 27400 0.000 0.000 0.000 +9 77 286.000 23500 0.000 0.000 0.000 +9 78 291.250 38000 0.000 0.000 0.000 +9 79 296.750 42900 0.000 0.000 0.000 +9 80 295.000 19500 0.000 0.000 0.000 +9 81 314.000 102900 0.000 0.000 0.000 +9 82 315.750 84100 0.000 0.000 0.000 +10 83 302.000 44600 0.000 0.000 0.000 +10 84 301.000 45000 0.000 0.000 0.000 +10 85 294.750 27900 0.000 0.000 0.000 +10 86 293.500 17600 0.000 0.000 0.000 +10 87 291.750 21600 0.000 0.000 0.000 +10 88 290.500 30300 0.000 0.000 0.000 +10 89 291.750 22700 0.000 0.000 0.000 +10 90 298.750 16200 0.000 0.000 0.000 +10 91 302.250 49100 0.000 0.000 0.000 +10 92 305.500 35500 0.000 0.000 0.000 +10 93 309.750 63200 0.000 0.000 0.000 +10 94 309.500 43200 0.000 0.000 0.000 +11 95 383.750 19300 0.000 0.000 0.000 +11 96 389.500 28900 0.000 0.000 0.000 +11 97 394.500 41700 0.000 0.000 0.000 +11 98 396.250 28200 0.000 0.000 0.000 +11 99 401.000 55000 0.000 0.000 0.000 +11 100 402.000 41100 0.000 0.000 0.000 +11 101 403.000 38500 0.000 0.000 0.000 +11 102 404.000 33100 0.000 0.000 0.000 +11 103 404.000 33800 0.000 0.000 0.000 +12 104 388.000 25800 0.000 0.000 0.000 +12 105 388.500 44900 0.000 0.000 0.000 +12 106 390.000 38400 0.000 0.000 0.000 +12 107 390.750 23200 0.000 0.000 0.000 +12 108 396.500 25100 0.000 0.000 0.000 +12 109 398.750 38000 0.000 0.000 0.000 +12 110 398.000 33600 0.000 0.000 0.000 +12 111 394.500 54000 0.000 0.000 0.000 +12 112 397.000 20900 0.000 0.000 0.000 +13 113 398.125 28500 0.000 396.000 401.000 +13 114 394.500 39900 0.000 392.500 397.500 +13 115 390.000 37600 0.000 390.000 393.750 +13 116 389.500 31000 0.000 386.000 392.750 +13 117 385.500 25800 0.000 385.500 389.750 +13 118 390.500 18000 0.000 385.000 390.500 +13 119 391.750 14600 0.000 391.500 395.500 +13 120 399.000 27800 0.000 392.250 399.000 +13 121 402.000 46500 0.000 399.000 403.000 +13 122 409.000 36300 0.000 404.500 409.500 +13 123 409.250 54500 0.000 409.250 413.750 +13 124 411.750 46200 0.000 406.000 412.500 +13 125 418.750 60200 0.000 412.750 419.500 +13 126 418.750 32700 0.000 416.000 420.000 +14 127 411.500 24800 0.000 411.250 415.500 +14 128 415.250 53000 0.000 412.000 423.000 +14 129 420.000 49700 0.000 417.250 425.000 +14 130 423.250 68700 0.000 422.750 429.000 +14 131 422.500 45400 0.000 420.750 424.750 +14 132 424.000 20900 0.000 420.750 424.250 +14 133 428.875 37600 0.000 427.000 430.000 +14 134 439.000 59800 0.000 427.250 439.000 +15 135 448.750 29300 0.000 444.750 449.500 +15 136 444.000 27400 0.000 444.000 449.750 +15 137 443.500 30900 0.000 440.500 448.500 +15 138 437.875 26500 0.000 437.500 442.875 +15 139 437.750 29000 0.000 433.500 440.000 +15 140 430.500 27500 0.000 430.250 441.250 +15 141 425.500 52300 0.000 424.250 428.880 +15 142 428.000 59900 0.000 419.500 429.000 +15 143 432.500 34600 0.000 427.250 435.000 +15 144 437.500 33100 0.000 432.000 438.500 +15 145 434.500 24300 0.000 434.000 437.750 +15 146 431.500 28500 0.000 430.500 437.500 +15 147 428.000 15500 0.000 427.000 432.750 +15 148 424.250 44900 0.000 424.000 428.750 +15 149 423.000 34000 0.000 420.000 426.500 +15 150 422.875 37800 0.000 420.000 424.000 +15 151 423.500 24000 0.000 423.250 428.000 +15 152 429.500 23200 0.000 420.500 429.500 +15 153 434.250 31800 0.000 429.750 435.000 +15 154 430.250 36500 0.000 426.500 436.250 +16 155 309.750 41400 0.000 307.125 310.750 +16 156 317.000 44500 0.000 309.750 317.000 +16 157 315.000 39900 0.000 313.125 319.750 +16 158 322.000 40600 0.000 318.500 322.500 +16 159 324.250 35300 0.000 321.000 325.250 +16 160 323.000 32300 0.000 322.500 327.000 +16 161 329.250 47700 0.000 323.250 330.000 +16 162 329.500 56700 0.000 328.500 333.500 +16 163 321.000 45400 0.000 320.000 328.500 +17 164 298.000 31900 0.000 290.500 298.000 +17 165 272.000 165000 0.000 270.750 285.125 +17 166 259.500 290400 0.000 252.750 261.000 +17 167 270.750 136000 0.000 266.000 270.750 +17 168 268.750 96600 0.000 266.000 271.750 +17 169 260.000 99100 0.000 258.000 264.500 +17 170 251.500 100900 0.000 250.500 260.750 +17 171 259.500 78100 0.000 253.000 261.000 +17 172 260.500 64600 0.000 260.500 267.500 +17 173 260.000 51400 0.000 259.000 264.500 +17 174 258.000 40800 0.000 257.000 261.000 +17 175 257.750 39400 0.000 255.000 257.750 +17 176 255.500 47500 0.000 253.500 257.500 +17 177 248.500 147400 0.000 247.250 254.000 +17 178 251.500 107100 0.000 245.000 251.500 +17 179 259.500 70100 0.000 252.500 262.000 +17 180 261.750 49800 0.000 255.000 263.500 +17 181 274.000 146700 0.000 270.500 284.500 +17 182 281.000 82800 0.000 277.500 283.750 +17 183 282.000 52000 0.000 281.000 284.500 +17 184 279.250 39600 0.000 278.000 280.250 +18 185 248.000 38000 0.000 247.750 250.000 +18 186 246.500 25100 0.000 244.000 248.250 +18 187 247.000 27100 0.000 245.000 248.750 +18 188 244.375 30500 0.000 244.000 248.000 +18 189 243.250 31600 0.000 240.500 244.250 +18 190 246.250 34900 0.000 245.250 248.500 +18 191 245.250 35300 0.000 245.130 249.750 +18 192 241.250 30700 0.000 240.250 244.500 +19 193 240.500 34800 0.000 237.500 243.250 +19 194 246.500 43600 0.000 236.500 246.500 +19 195 249.500 68300 0.000 246.500 251.500 +19 196 250.000 35100 0.000 249.125 254.000 +19 197 248.750 56400 0.000 247.000 251.500 +19 198 249.250 32100 0.000 246.750 250.500 +19 199 247.500 29300 0.000 247.375 250.750 +19 200 245.750 20000 0.000 245.250 247.500 +19 201 246.250 22500 0.000 245.125 246.750 +19 202 243.000 38800 0.000 242.500 247.750 +20 203 182.250 84300 0.000 176.500 183.500 +20 204 179.000 63300 0.000 179.000 185.500 +20 205 173.000 59200 0.000 171.750 178.500 +20 206 171.500 69600 0.000 170.250 175.250 +20 207 165.125 70500 0.000 165.125 172.250 +20 208 156.500 113200 0.000 155.250 165.000 +20 209 152.000 131000 0.000 150.500 157.250 +20 210 159.625 109100 0.000 150.500 159.750 +20 211 161.880 87800 0.000 161.750 168.500 +20 212 166.500 75800 0.000 158.000 166.500 +20 213 170.500 95900 0.000 169.250 173.750 +20 214 171.500 80600 0.000 166.500 173.500 +20 215 169.250 59500 0.000 168.250 175.000 +20 216 166.750 47000 0.000 164.750 168.000 +20 217 165.000 59700 0.000 165.000 171.000 +20 218 164.000 25800 0.000 160.250 164.000 +20 219 160.875 51900 0.000 160.000 166.750 +20 220 159.000 84100 0.000 153.500 159.500 +20 221 160.250 125800 0.000 154.250 161.750 +20 222 159.500 63900 0.000 158.500 163.500 +20 223 157.750 56100 0.000 155.625 160.500 +20 224 156.250 74200 0.000 154.250 159.500 +20 225 164.125 60900 0.000 159.250 164.750 +20 226 162.000 87000 0.000 160.750 164.500 +21 227 162.250 51800 0.000 161.750 166.000 +21 228 164.250 65300 0.000 160.130 164.250 +21 229 168.000 48900 0.000 166.000 168.875 +21 230 168.875 45100 0.000 168.250 171.375 +21 231 167.500 78000 0.000 166.750 171.250 +21 232 166.625 53000 0.000 165.500 170.250 +21 233 165.750 49000 0.000 163.500 166.500 +21 234 163.250 58700 0.000 163.250 167.125 +21 235 168.500 56700 0.000 162.630 168.500 +22 236 212.000 89200 0.000 212.000 217.750 +22 237 207.500 117700 0.000 206.000 209.750 +22 238 212.000 95100 0.000 207.500 212.500 +22 239 212.250 74000 0.000 211.500 214.000 +22 240 215.500 68400 0.000 211.880 215.750 +22 241 220.000 79200 0.000 216.000 220.000 +23 242 222.500 45700 0.000 222.250 224.630 +23 243 223.250 43000 0.000 221.500 223.500 +23 244 224.250 47700 0.000 223.500 225.250 +23 245 226.500 43800 0.000 223.380 226.500 +23 246 230.000 88800 0.000 226.250 230.000 +23 247 229.380 118000 0.000 229.000 230.880 +23 248 228.500 127100 0.000 228.375 231.000 +23 249 230.380 136400 0.000 228.500 230.750 +24 250 249.880 97500 0.000 249.630 255.000 +24 251 251.000 90400 0.000 248.130 252.250 +24 252 256.500 90400 0.000 251.000 257.000 +24 253 257.750 111000 0.000 257.625 259.750 +24 254 259.250 87200 0.000 256.000 259.250 +24 255 261.000 111100 0.000 258.250 261.250 +24 256 263.750 75900 0.000 261.250 264.500 +24 257 258.000 121100 0.000 257.750 263.250 +24 258 254.000 125500 0.000 254.000 257.250 +24 259 253.500 78400 0.000 252.750 257.750 +24 260 258.000 74700 0.000 253.000 258.000 +25 261 279.750 62700 0.000 277.130 280.000 +25 262 277.625 85500 0.000 276.750 280.375 +25 263 276.630 99800 0.000 276.630 280.380 +25 264 276.250 113600 0.000 276.000 279.500 +25 265 274.250 187100 0.000 272.750 276.380 +25 266 272.000 105300 0.000 272.000 275.500 +25 267 270.500 111000 0.000 268.000 272.750 +25 268 271.000 66000 0.000 270.500 272.500 +25 269 272.500 43000 0.000 270.250 272.500 +25 270 273.000 56200 0.000 272.000 273.880 +25 271 275.500 55200 0.000 273.750 275.500 +25 272 272.250 73400 0.000 272.000 275.500 +26 273 262.250 89000 0.000 262.000 267.750 +26 274 259.500 146100 0.000 257.500 261.750 +26 275 260.250 90300 0.000 257.500 260.625 +26 276 261.000 74100 0.000 258.880 261.880 +26 277 264.500 44400 0.000 260.250 264.880 +26 278 262.000 78900 0.000 261.250 264.500 +27 279 272.500 132000 0.000 270.130 272.500 +27 280 272.000 79800 0.000 271.375 272.625 +27 281 271.130 75600 0.000 270.750 272.880 +27 282 270.000 113800 0.000 269.880 271.500 +27 283 264.000 179500 0.000 263.500 270.500 +27 284 258.000 215300 0.000 257.500 263.250 +27 285 258.250 154200 0.000 256.500 259.625 +27 286 259.500 95600 0.000 257.500 260.500 +27 287 261.250 96500 0.000 259.250 262.000 +27 288 259.750 119000 0.000 259.250 261.625 +27 289 258.750 81500 0.000 257.130 259.750 +28 290 257.380 53700 0.000 257.250 259.250 +28 291 256.000 52200 0.000 255.375 257.500 +28 292 257.750 76200 0.000 255.630 258.630 +28 293 258.750 93400 0.000 257.380 259.500 +28 294 258.750 99000 0.000 258.500 259.630 +28 295 257.250 123100 0.000 255.750 257.500 +28 296 260.500 188300 0.000 257.000 261.000 +28 297 260.250 103300 0.000 259.000 261.250 +29 298 264.250 110100 0.000 261.630 264.380 +29 299 266.000 98100 0.000 264.250 266.750 +29 300 267.500 146100 0.000 266.625 268.000 +29 301 270.250 199400 0.000 267.500 270.250 +29 302 270.250 89800 0.000 269.000 270.625 +29 303 271.000 78600 0.000 269.250 271.250 +29 304 272.750 107600 0.000 269.880 273.500 +29 305 273.500 119200 0.000 272.750 275.250 +29 306 273.500 0 0.000 0.000 0.000 +30 307 236.250 146100 0.000 236.250 242.000 +30 308 239.500 358100 0.000 235.500 239.500 +30 309 243.500 241500 0.000 240.500 244.000 +30 310 251.125 304800 0.000 248.375 253.625 +30 311 251.250 231000 0.000 248.875 253.000 +30 312 253.000 152800 0.000 249.625 254.000 +30 313 253.500 175100 0.000 252.500 257.880 +30 314 254.000 141800 0.000 252.125 255.500 +30 315 258.000 110000 0.000 253.000 258.250 +30 316 261.750 374200 0.000 261.250 264.000 +30 317 262.380 275100 0.000 261.380 265.000 +30 318 261.500 169700 0.000 259.875 261.750 +31 319 262.500 87100 0.000 262.250 266.500 +31 320 258.250 91100 0.000 257.750 262.500 +31 321 260.880 105600 0.000 258.250 262.000 +31 322 261.750 85000 0.000 258.500 261.880 +31 323 259.630 68400 0.000 259.000 262.250 +31 324 257.250 45500 0.000 257.250 260.250 +31 325 256.750 39900 0.000 255.000 257.500 +31 326 258.000 102400 0.000 255.250 259.500 +32 327 268.125 180700 0.000 267.750 272.000 +32 328 270.250 80900 0.000 267.000 270.250 +32 329 271.000 107200 0.000 267.750 271.000 +32 330 273.000 149400 0.000 269.250 273.000 +32 331 273.500 262900 0.000 273.000 276.750 +32 332 276.250 150300 0.000 273.000 276.250 +32 333 278.630 172800 0.000 275.500 279.500 +32 334 281.000 132700 0.000 278.750 281.500 +32 335 279.125 178400 0.000 279.000 281.750 +33 336 311.000 96900 0.000 308.130 311.000 +33 337 308.250 85000 0.000 307.625 311.000 +33 338 305.750 119200 0.000 304.500 306.625 +33 339 303.500 133200 0.000 303.000 306.500 +33 340 298.500 168400 0.000 297.130 299.750 +33 341 299.630 77900 0.000 299.130 300.630 +33 342 301.750 91100 0.000 300.130 301.880 +33 343 306.630 78700 0.000 299.750 307.000 +33 344 308.125 132600 0.000 306.750 310.000 +33 345 305.500 82400 0.000 304.000 308.250 +34 346 308.875 80600 0.000 308.250 312.875 +34 347 306.750 124000 0.000 306.250 309.250 +34 348 310.000 123300 0.000 307.000 310.250 +34 349 310.250 102600 0.000 309.750 312.875 +34 350 312.000 80400 0.000 309.880 312.250 +34 351 308.880 115900 0.000 308.500 312.000 +34 352 310.500 98300 0.000 307.500 310.500 +34 353 314.250 116900 0.000 309.750 314.250 +34 354 315.625 118600 0.000 314.250 317.500 +34 355 318.000 98700 0.000 315.000 318.500 +34 356 319.750 149000 0.000 318.000 321.500 +34 357 311.250 121900 0.000 311.000 317.500 +34 358 309.000 84400 0.000 308.130 309.880 +34 359 312.000 126500 0.000 308.625 312.250 +35 360 78.000 512100 0.000 77.500 78.500 +35 361 75.375 607700 0.000 75.250 78.000 +35 362 74.625 628300 0.000 73.750 75.250 +35 363 74.250 517300 0.000 73.875 75.000 +35 364 72.750 358300 0.000 72.625 74.000 +35 365 72.630 394600 0.000 72.250 73.500 +35 366 73.000 599200 0.000 72.130 73.750 +35 367 73.500 450200 0.000 72.875 74.500 +35 368 74.130 304400 0.000 73.880 74.250 +35 369 73.875 289200 0.000 73.375 74.500 +36 370 72.250 498300 0.000 69.875 72.375 +36 371 71.630 476800 0.000 71.000 72.500 +36 372 71.250 282600 0.000 71.000 71.880 +36 373 71.630 350300 0.000 70.880 71.880 +36 374 70.880 261500 0.000 70.500 71.630 +36 375 70.500 531700 0.000 70.380 71.130 +36 376 69.625 391900 0.000 69.500 70.750 +36 377 69.625 337000 0.000 69.125 70.500 +36 378 70.000 338900 0.000 69.500 70.625 +36 379 70.000 393400 0.000 69.625 70.125 +36 380 69.630 293200 0.000 69.630 70.250 +36 381 69.500 205300 0.000 69.500 70.000 +36 382 70.000 268300 0.000 69.750 70.000 +36 383 68.630 381500 0.000 68.500 69.880 +36 384 68.250 693200 0.000 67.250 68.500 +36 385 67.630 316100 0.000 67.500 68.250 +36 386 67.750 406600 0.000 67.380 68.000 +36 387 67.500 305800 0.000 67.375 67.875 +36 388 66.500 466500 0.000 66.125 67.750 +36 389 66.250 652800 0.000 65.750 66.625 +36 390 66.750 399300 0.000 66.125 66.875 +36 391 67.880 502100 0.000 66.500 68.380 +36 392 68.500 538900 0.000 68.380 69.250 +36 393 67.880 407700 0.000 67.750 68.500 +36 394 67.750 396500 0.000 67.750 68.630 +36 395 71.250 439900 0.000 67.625 71.375 +36 396 70.000 626100 0.000 69.380 71.000 +37 397 65.000 208900 0.000 64.500 65.125 +37 398 65.380 291800 0.000 64.880 65.380 +37 399 65.750 377600 0.000 65.500 66.380 +37 400 67.250 283500 0.000 65.380 67.250 +37 401 66.630 582400 0.000 66.380 67.630 +37 402 66.375 257200 0.000 66.125 66.625 +37 403 65.625 393900 0.000 65.625 66.750 +37 404 65.250 295500 0.000 65.130 65.750 +37 405 65.000 329700 0.000 64.630 65.250 +37 406 66.000 412400 0.000 64.880 66.130 +38 407 64.875 176300 0.000 64.000 64.875 +38 408 64.875 163300 0.000 64.375 65.125 +38 409 64.750 253800 0.000 64.250 65.000 +38 410 64.250 279200 0.000 64.000 64.630 +38 411 64.380 308600 0.000 64.000 64.630 +38 412 62.500 412000 0.000 62.500 64.500 +38 413 63.500 610300 0.000 61.250 63.500 +38 414 63.380 472800 0.000 63.000 63.750 +39 415 54.625 512900 0.000 53.375 54.750 +39 416 54.250 504900 0.000 54.000 55.250 +39 417 53.500 651700 0.000 53.500 55.000 +39 418 53.000 334100 0.000 52.750 53.750 +39 419 52.000 579900 0.000 51.875 53.500 +39 420 51.125 672300 0.000 51.000 52.875 +39 421 51.250 451000 0.000 50.380 51.630 +39 422 51.250 411900 0.000 50.875 51.750 +39 423 51.250 663800 0.000 50.500 51.625 +39 424 53.875 821800 0.000 52.000 54.500 +39 425 53.500 761400 0.000 53.250 54.375 +40 426 66.000 437200 0.000 64.250 66.000 +40 427 65.130 553000 0.000 64.750 66.130 +40 428 64.880 449000 0.000 64.380 65.380 +40 429 64.750 397400 0.000 64.250 65.000 +40 430 64.380 375800 0.000 63.880 64.630 +40 431 65.000 270800 0.000 64.000 65.250 +40 432 65.125 410600 0.000 64.375 65.500 +40 433 65.500 555700 0.000 65.130 66.000 +40 434 65.250 644100 0.000 64.380 65.380 +41 435 56.750 492500 0.000 56.250 56.880 +41 436 56.630 616200 0.000 55.750 56.630 +41 437 56.380 348000 0.000 56.130 56.630 +41 438 55.625 461800 0.000 55.500 56.375 +41 439 55.500 438400 0.000 55.250 55.875 +41 440 56.130 514900 0.000 55.130 56.380 +41 441 57.880 673900 0.000 55.880 58.130 +41 442 58.000 700200 0.000 57.250 58.500 +41 443 58.750 796900 0.000 57.880 59.130 +41 444 60.250 1089200 0.000 59.750 60.750 +41 445 59.880 747600 0.000 59.880 60.500 +42 446 55.875 387300 0.000 55.000 56.750 +42 447 55.500 396600 0.000 55.500 56.500 +42 448 54.880 372500 0.000 54.500 55.380 +42 449 54.130 551600 0.000 53.500 55.130 +42 450 54.000 346100 0.000 53.875 54.500 +42 451 54.375 427300 0.000 54.125 55.000 +42 452 54.500 389800 0.000 53.380 54.630 +42 453 54.625 497600 0.000 54.250 55.250 +42 454 53.625 524200 0.000 53.250 54.250 +43 455 64.625 1058800 0.000 63.375 64.625 +43 456 64.500 689400 0.000 64.250 64.880 +43 457 63.750 1140600 0.000 63.625 65.125 +43 458 63.250 892800 0.000 63.000 63.880 +43 459 63.875 512200 0.000 63.250 63.875 +43 460 64.375 645800 0.000 64.000 64.750 +43 461 65.750 1214100 0.000 64.500 65.750 +43 462 66.000 911700 0.000 65.000 66.130 +43 463 65.125 685600 0.000 64.625 65.750 +43 464 64.880 717200 0.000 64.500 65.250 +43 465 64.125 491600 0.000 64.125 64.625 +43 466 64.250 759200 0.000 64.000 64.750 +43 467 64.500 384600 0.000 64.000 64.750 +43 468 64.750 497500 0.000 64.500 65.250 +43 469 65.125 855600 0.000 64.750 65.250 +43 470 64.875 777200 0.000 64.750 65.125 +43 471 64.750 487900 0.000 64.750 65.130 +43 472 63.875 569800 0.000 63.875 64.875 +43 473 64.380 372400 0.000 64.000 64.750 +44 474 60.000 290100 0.000 60.000 60.630 +44 475 60.380 358800 0.000 59.380 60.750 +44 476 61.000 638300 0.000 60.380 61.250 +44 477 61.500 963900 0.000 59.750 61.630 +44 478 62.500 1247100 0.000 61.380 62.500 +44 479 63.500 985100 0.000 62.880 63.630 +44 480 64.500 1556300 0.000 63.125 65.000 +44 481 66.875 1688100 0.000 64.750 66.875 +44 482 66.750 1578700 0.000 66.375 67.500 +45 483 76.750 1321700 0.000 74.750 76.750 +45 484 75.500 1604100 0.000 75.500 77.630 +45 485 76.380 1254000 0.000 75.250 76.630 +45 486 76.000 887100 0.000 75.630 76.500 +45 487 76.630 490500 0.000 75.750 77.000 +45 488 76.250 868300 0.000 76.250 77.250 +45 489 74.750 831000 0.000 74.500 76.375 +45 490 73.375 997900 0.000 73.250 74.875 +45 491 74.130 747500 0.000 73.380 74.630 +45 492 74.500 614900 0.000 73.250 74.630 +45 493 75.130 752300 0.000 74.630 76.130 +45 494 79.000 1346100 0.000 75.375 79.000 +45 495 80.380 2207600 0.000 78.380 80.380 +45 496 81.130 1342500 0.000 79.250 81.130 +45 497 83.500 1439300 0.000 81.380 84.500 +45 498 82.750 1500400 0.000 82.375 84.375 +46 499 96.130 725400 0.000 95.750 98.000 +46 500 96.250 373700 0.000 96.125 97.125 +46 501 93.000 760600 0.000 92.500 97.000 +46 502 95.625 915100 0.000 92.375 95.625 +46 503 96.000 950900 0.000 95.000 96.750 +46 504 97.625 1024200 0.000 95.500 98.500 +46 505 96.630 1060600 0.000 96.500 98.380 +46 506 98.000 1114200 0.000 96.000 99.250 +46 507 98.000 850900 0.000 97.000 98.380 +47 508 102.000 711800 0.000 101.880 103.250 +47 509 102.625 858000 0.000 101.250 102.875 +47 510 103.250 607100 0.000 102.000 103.375 +47 511 103.875 894200 0.000 102.625 104.000 +47 512 106.625 1651200 0.000 104.625 106.750 +47 513 107.000 1049000 0.000 106.000 107.380 +47 514 108.750 978800 0.000 107.375 109.000 +47 515 109.500 1007500 0.000 107.250 110.000 +47 516 110.125 794100 0.000 109.625 110.500 +47 517 112.630 1020400 0.000 110.000 112.630 +47 518 111.500 929400 0.000 111.000 112.130 +48 519 116.500 424700 0.000 115.625 116.625 +48 520 115.250 668900 0.000 114.000 115.250 +48 521 114.000 722000 0.000 113.125 115.000 +48 522 111.750 802300 0.000 110.750 114.750 +48 523 111.250 673500 0.000 110.500 112.500 +48 524 110.750 538900 0.000 110.250 111.750 +48 525 112.500 813000 0.000 109.750 112.875 +48 526 114.630 665400 0.000 113.000 115.000 +48 527 115.630 843600 0.000 113.380 115.630 +48 528 114.380 647800 0.000 114.250 115.880 +48 529 113.000 404400 0.000 112.875 115.000 +48 530 111.250 734800 0.000 110.250 112.880 +48 531 111.875 890300 0.000 110.250 111.875 +48 532 113.500 803000 0.000 111.250 113.875 +48 533 114.000 662100 0.000 113.000 114.880 +48 534 115.750 573900 0.000 113.630 116.130 +48 535 113.130 791400 0.000 113.000 116.250 +48 536 113.375 831700 0.000 112.625 114.750 +48 537 113.750 672300 0.000 113.130 114.500 +48 538 114.250 419600 0.000 113.630 114.630 +48 539 117.250 737800 0.000 114.125 117.250 +48 540 118.000 971500 0.000 117.000 119.750 +48 541 121.000 1170300 0.000 118.130 121.000 +48 542 121.750 1233400 0.000 121.500 122.875 +48 543 121.130 625500 0.000 120.250 122.250 +49 544 120.875 542900 0.000 120.250 122.750 +49 545 118.000 865300 0.000 117.750 121.380 +49 546 120.000 669800 0.000 117.880 120.880 +49 547 120.250 556200 0.000 119.750 121.000 +49 548 121.000 373100 0.000 120.375 121.250 +49 549 119.250 943300 0.000 118.625 119.625 +50 550 122.880 814300 0.000 121.630 123.500 +50 551 121.750 691100 0.000 121.500 123.250 +50 552 123.125 819700 0.000 121.500 124.250 +50 553 123.750 911500 0.000 122.625 124.875 +50 554 123.875 942300 0.000 123.125 124.500 +50 555 123.000 1070900 0.000 122.875 124.125 +50 556 124.625 1099300 0.000 122.750 124.750 +50 557 126.625 1111900 0.000 124.250 126.625 +50 558 128.625 2097500 0.000 126.000 129.500 +50 559 127.875 1271500 0.000 126.500 128.375 +51 560 129.875 1190200 0.000 128.875 130.125 +51 561 131.130 1391200 0.000 129.130 131.250 +51 562 132.130 1104600 0.000 131.000 132.250 +51 563 132.250 803300 0.000 131.750 132.500 +51 564 134.250 658400 0.000 131.500 134.250 +51 565 133.000 687100 0.000 132.750 134.000 +52 566 126.750 1413200 0.000 125.000 127.250 +52 567 126.500 946400 0.000 125.500 127.000 +52 568 123.880 922600 0.000 123.500 126.500 +52 569 122.250 1474500 0.000 121.750 123.250 +52 570 122.125 803600 0.000 122.000 123.375 +52 571 122.250 650900 0.000 121.880 122.880 +52 572 123.625 858900 0.000 122.375 123.625 +52 573 125.250 1165800 0.000 124.000 125.750 +52 574 126.875 784800 0.000 125.125 127.250 +52 575 127.500 1030600 0.000 127.000 128.250 +52 576 124.500 1354300 0.000 124.000 127.630 +53 577 111.250 1043200 0.000 109.250 111.500 +53 578 112.880 1458900 0.000 110.750 113.500 +53 579 110.000 1241300 0.000 109.875 112.500 +53 580 110.250 1117000 0.000 109.375 111.750 +53 581 111.500 812900 0.000 109.750 111.625 +53 582 112.000 1005000 0.000 111.625 112.875 +53 583 111.000 779400 0.000 110.375 111.500 +53 584 109.250 868500 0.000 109.125 111.625 +54 585 108.000 729600 0.000 107.375 108.625 +54 586 110.250 1455900 0.000 108.500 110.500 +54 587 110.625 1432300 0.000 110.375 112.000 +54 588 111.000 990700 0.000 110.375 111.500 +54 589 112.000 954700 0.000 111.125 112.500 +54 590 113.750 2110500 0.000 112.750 114.250 +54 591 114.000 817600 0.000 112.500 114.000 +54 592 114.750 1241000 0.000 113.500 115.380 +54 593 114.000 914900 0.000 114.000 115.500 +54 594 112.750 881400 0.000 112.375 114.000 +54 595 112.250 836700 0.000 111.500 113.000 +54 596 112.250 694200 0.000 111.750 113.250 +55 597 112.750 770500 0.000 110.630 112.880 +55 598 112.250 931500 0.000 112.000 113.500 +55 599 111.125 720700 0.000 110.500 112.250 +55 600 110.750 1028700 0.000 110.125 111.125 +55 601 109.880 836400 0.000 109.630 111.380 +55 602 110.750 1104400 0.000 109.500 111.625 +55 603 111.875 899400 0.000 110.625 112.250 +55 604 113.250 1090000 0.000 111.875 113.500 +55 605 113.380 1022900 0.000 112.630 114.250 +55 606 113.750 1122500 0.000 112.750 114.630 +55 607 116.500 1463500 0.000 114.000 116.500 +55 608 116.250 1337900 0.000 116.000 117.250 +56 609 107.250 704700 0.000 105.375 107.500 +56 610 107.125 802400 0.000 106.250 107.375 +56 611 105.250 897500 0.000 105.250 107.130 +56 612 104.375 1076900 0.000 104.125 105.625 +56 613 105.250 856100 0.000 104.000 105.750 +56 614 107.250 925700 0.000 104.500 107.250 +56 615 108.130 1047100 0.000 106.750 108.380 +56 616 107.500 625100 0.000 107.125 108.125 +56 617 107.000 895900 0.000 106.250 107.375 +56 618 106.500 810000 0.000 105.500 107.250 +56 619 106.125 931100 0.000 105.125 106.375 +56 620 105.380 710200 0.000 105.130 107.000 +56 621 107.125 1591800 0.000 104.500 107.750 +56 622 108.250 1002700 0.000 106.750 108.750 +56 623 109.000 1059300 0.000 107.875 109.625 +56 624 108.380 610400 0.000 108.250 109.380 +57 625 124.625 929500 0.000 124.625 126.125 +57 626 125.750 1200100 0.000 125.250 126.875 +57 627 125.880 763200 0.000 125.750 126.500 +57 628 126.750 589700 0.000 123.750 126.875 +57 629 127.500 1270600 0.000 126.500 127.750 +57 630 125.750 1668400 0.000 125.375 127.125 +57 631 124.880 1056700 0.000 124.250 125.750 +57 632 123.250 944300 0.000 123.250 125.500 +57 633 124.000 782700 0.000 122.750 124.000 +58 634 124.250 754200 0.000 123.250 124.500 +58 635 123.750 418300 0.000 123.625 124.250 +58 636 123.125 505200 0.000 122.750 124.250 +58 637 123.500 573600 0.000 122.750 124.000 +58 638 123.125 685000 0.000 122.875 123.875 +58 639 121.000 872500 0.000 120.750 122.630 +58 640 120.000 1388100 0.000 119.875 122.250 +58 641 119.750 1007000 0.000 119.000 120.375 +59 642 123.625 1069600 0.000 123.250 124.625 +59 643 124.130 1163300 0.000 123.630 125.000 +59 644 128.250 2138800 0.000 124.630 128.500 +59 645 129.630 2539000 0.000 129.130 130.630 +59 646 132.500 2270200 0.000 129.375 132.625 +59 647 133.130 1825200 0.000 132.380 134.500 +59 648 133.250 1212300 0.000 132.625 134.125 +59 649 134.625 1936500 0.000 133.250 135.250 +59 650 137.000 1128700 0.000 133.750 137.000 +59 651 136.630 1563600 0.000 136.130 137.630 +60 652 130.500 1037200 0.000 128.250 131.125 +60 653 129.375 1118800 0.000 129.000 130.625 +60 654 128.750 1552200 0.000 128.125 130.000 +60 655 127.750 1197800 0.000 127.750 128.750 +60 656 124.500 2057900 0.000 124.500 128.000 +60 657 125.375 1404400 0.000 124.125 125.625 +60 658 126.380 1098400 0.000 125.500 126.630 +60 659 126.625 993400 0.000 126.375 127.625 +60 660 128.125 840800 0.000 126.875 128.250 +60 661 127.750 949000 0.000 127.500 129.500 +61 662 126.500 1280300 0.000 125.500 126.750 +61 663 125.630 1135000 0.000 125.130 126.880 +61 664 125.375 1258800 0.000 124.750 126.375 +61 665 125.000 770500 0.000 124.630 125.630 +61 666 124.750 579400 0.000 124.750 125.625 +61 667 125.750 772900 0.000 125.000 126.250 +61 668 127.125 925700 0.000 125.375 127.250 +61 669 127.880 978300 0.000 127.000 128.250 +61 670 130.130 2239000 0.000 128.500 130.880 +61 671 130.250 1157600 0.000 129.750 130.625 +61 672 128.000 1551200 0.000 127.750 130.875 +62 673 133.000 1215000 0.000 132.380 133.250 +62 674 132.375 857100 0.000 131.875 132.750 +62 675 131.375 829300 0.000 131.250 132.375 +62 676 130.880 675200 0.000 130.750 131.630 +62 677 130.375 921900 0.000 129.750 131.750 +62 678 129.750 939700 0.000 129.250 130.250 +62 679 128.000 1666600 0.000 127.250 129.500 +62 680 128.625 1371900 0.000 127.250 130.000 +62 681 129.125 1265600 0.000 128.250 129.500 +62 682 129.750 1176100 0.000 128.750 130.130 +62 683 128.500 1644500 0.000 128.500 130.630 +63 684 121.130 1753000 0.000 119.380 121.130 +63 685 120.130 1316600 0.000 119.250 120.500 +63 686 119.875 1819000 0.000 119.125 121.625 +63 687 119.375 1268900 0.000 119.250 120.875 +63 688 118.750 1400300 0.000 117.375 119.750 +63 689 119.380 1365100 0.000 118.380 119.750 +63 690 120.875 1125300 0.000 118.750 121.125 +63 691 122.000 1753000 0.000 121.380 123.380 +63 692 123.130 914300 0.000 122.000 123.250 +63 693 124.000 1151500 0.000 123.125 124.250 +63 694 123.750 820500 0.000 123.500 124.500 +64 695 141.750 1638400 0.000 140.250 141.750 +64 696 144.630 1662500 0.000 142.130 144.880 +64 697 146.625 1940200 0.000 144.250 148.000 +64 698 149.000 1985600 0.000 146.625 149.375 +64 699 148.630 1407600 0.000 147.000 149.380 +64 700 150.250 1890100 0.000 149.130 151.500 +64 701 152.250 1586100 0.000 150.000 153.500 +64 702 152.500 1786100 0.000 151.500 154.500 +64 703 152.875 1524100 0.000 151.625 153.875 +64 704 153.750 1105900 0.000 152.375 154.000 +64 705 154.250 2127800 0.000 153.750 156.625 +64 706 154.000 1220900 0.000 153.380 155.130 +64 707 152.750 1011600 0.000 151.250 154.630 +64 708 153.250 562700 0.000 152.125 153.750 +65 709 141.130 1858600 0.000 138.880 141.250 +65 710 141.125 1561600 0.000 140.500 142.000 +65 711 140.000 1229000 0.000 139.500 140.380 +65 712 138.750 887600 0.000 138.000 140.000 +65 713 136.000 1938300 0.000 135.500 138.875 +65 714 136.125 1628600 0.000 135.125 136.375 +65 715 139.380 1630000 0.000 136.000 139.500 +65 716 140.375 2618100 0.000 138.500 141.000 +65 717 143.750 2629100 0.000 140.875 143.875 +65 718 143.880 2251100 0.000 143.500 145.500 +65 719 144.130 1766900 0.000 142.880 144.500 +65 720 139.250 2866100 0.000 138.750 142.880 +65 721 137.380 4379500 0.000 136.250 139.880 +65 722 137.000 2194500 0.000 136.000 138.500 +65 723 138.250 1413100 0.000 135.625 138.500 +66 724 123.625 2431100 0.000 122.375 124.500 +66 725 122.000 1609300 0.000 121.880 125.250 +66 726 120.880 3142200 0.000 119.750 123.000 +66 727 123.000 2239500 0.000 120.375 123.000 +66 728 121.130 2220400 0.000 120.880 123.380 +66 729 121.875 1947600 0.000 120.500 122.375 +66 730 120.250 1484700 0.000 119.875 121.250 +66 731 121.375 1697000 0.000 120.250 121.375 +66 732 120.375 1283900 0.000 120.125 121.500 +67 733 123.130 2107700 0.000 122.500 124.750 +67 734 121.380 2581200 0.000 119.250 122.000 +67 735 121.500 1103400 0.000 120.000 121.880 +67 736 123.125 998200 0.000 121.250 123.250 +67 737 123.750 751400 0.000 122.875 124.250 +67 738 122.750 1130400 0.000 121.875 124.375 +68 739 151.750 1804700 0.000 151.500 154.130 +68 740 154.250 3308900 0.000 149.500 156.000 +68 741 155.250 2945900 0.000 154.625 156.625 +68 742 158.630 3248800 0.000 155.880 159.630 +68 743 160.125 3272200 0.000 157.750 161.500 +68 744 160.630 2385000 0.000 159.130 162.380 +68 745 163.000 1561700 0.000 159.750 163.000 +68 746 166.875 2792500 0.000 164.250 167.625 +68 747 166.750 2187900 0.000 166.000 167.630 +68 748 164.880 1652200 0.000 163.880 166.750 +68 749 163.750 1430500 0.000 163.250 165.875 +68 750 163.000 2269600 0.000 163.000 167.380 +68 751 165.380 1998200 0.000 162.250 166.130 +69 752 159.250 2057900 0.000 158.750 160.880 +69 753 159.380 1668400 0.000 158.500 160.000 +69 754 160.250 2435800 0.000 159.750 161.000 +69 755 163.000 2284000 0.000 159.125 163.250 +69 756 164.375 2386900 0.000 163.000 165.625 +69 757 166.750 1632200 0.000 164.500 166.750 +69 758 169.000 3203300 0.000 167.130 169.630 +69 759 170.380 3299900 0.000 168.000 172.130 +69 760 173.380 2586600 0.000 170.380 174.000 +69 761 173.250 2109200 0.000 172.000 175.000 +70 762 150.500 2630500 0.000 149.500 153.750 +70 763 150.750 3073300 0.000 148.500 151.880 +70 764 154.630 1908400 0.000 151.000 154.750 +70 765 155.250 1868500 0.000 154.750 156.880 +70 766 156.500 1544300 0.000 154.500 157.250 +70 767 151.000 2044200 0.000 151.000 156.375 +71 768 145.250 2922100 0.000 144.130 147.380 +71 769 140.125 3923900 0.000 140.000 145.750 +71 770 135.000 4665000 0.000 132.000 141.250 +71 771 104.000 6384400 0.000 102.000 130.750 +71 772 115.000 0 0.000 110.130 122.000 +71 773 122.750 5386600 0.000 120.000 125.500 +72 774 118.880 2233600 0.000 115.000 119.250 +72 775 115.500 1824700 0.000 115.000 119.500 +72 776 117.250 2798900 0.000 115.250 118.250 +72 777 118.250 1550200 0.000 116.880 119.500 +72 778 118.250 1217000 0.000 116.500 118.375 +72 779 119.500 1646400 0.000 118.750 120.380 +72 780 119.875 630300 0.000 119.000 119.875 +72 781 115.750 1086800 0.000 115.130 117.630 +73 782 111.630 1230300 0.000 110.750 112.130 +73 783 112.250 769000 0.000 111.500 112.750 +73 784 113.380 867400 0.000 112.750 114.000 +73 785 112.875 896800 0.000 112.625 113.750 +73 786 111.500 932200 0.000 110.750 113.500 +73 787 110.250 1489000 0.000 109.500 111.130 +73 788 114.000 1564900 0.000 111.630 114.750 +73 789 112.875 958200 0.000 112.375 113.875 +74 790 114.125 1652700 0.000 112.500 114.375 +74 791 113.500 947400 0.000 113.375 114.125 +74 792 112.750 1100200 0.000 112.500 114.380 +74 793 112.500 2195900 0.000 110.630 112.630 +74 794 113.000 1558100 0.000 111.880 113.000 +74 795 113.380 1187700 0.000 113.130 114.250 +74 796 115.000 1365300 0.000 113.630 115.250 +74 797 114.625 1992000 0.000 114.500 116.500 +74 798 114.000 1582100 0.000 113.625 115.000 +74 799 113.750 2225000 0.000 112.625 114.000 +74 800 115.500 2406400 0.000 113.250 115.750 +74 801 117.375 2209500 0.000 115.750 117.625 +74 802 118.250 2648200 0.000 117.750 119.375 +74 803 117.125 1619800 0.000 117.125 119.125 +75 804 116.380 1333800 0.000 116.000 118.380 +75 805 116.875 1509800 0.000 116.125 117.500 +75 806 117.130 939000 0.000 116.880 117.630 +75 807 117.380 1049800 0.000 116.880 118.250 +75 808 117.750 1708300 0.000 117.375 118.750 +75 809 116.750 1105600 0.000 116.000 117.630 +75 810 116.250 1141200 0.000 115.875 117.000 +75 811 116.000 1173400 0.000 115.000 116.380 +75 812 116.250 946600 0.000 115.375 116.375 +76 813 98.750 1249700 0.000 98.000 99.380 +76 814 99.500 1127900 0.000 98.500 99.750 +76 815 100.125 1129400 0.000 99.375 100.500 +76 816 100.375 638800 0.000 100.250 100.750 +76 817 99.380 1245400 0.000 98.250 100.750 +76 818 98.880 1157700 0.000 98.500 99.380 +76 819 97.750 956500 0.000 97.500 98.625 +76 820 97.630 1001300 0.000 96.880 98.130 +76 821 97.130 1434200 0.000 96.880 98.130 +76 822 99.250 2026900 0.000 97.380 99.500 +77 823 95.375 1005100 0.000 95.125 95.750 +77 824 94.500 682200 0.000 94.250 95.500 +77 825 94.750 1165700 0.000 94.500 95.375 +77 826 94.130 1192300 0.000 94.000 95.130 +77 827 94.130 1637100 0.000 94.000 94.630 +77 828 98.000 1760400 0.000 94.500 98.130 +77 829 98.875 2366000 0.000 98.375 99.500 +77 830 100.000 2418700 0.000 98.880 100.380 +77 831 99.750 1890500 0.000 99.500 101.630 +78 832 106.625 1050400 0.000 106.000 106.750 +78 833 106.500 1716200 0.000 105.880 107.750 +78 834 106.130 868400 0.000 105.880 106.880 +78 835 105.875 752300 0.000 105.250 106.375 +78 836 105.500 782800 0.000 105.250 105.880 +78 837 105.875 934800 0.000 105.125 106.375 +78 838 106.375 645200 0.000 105.875 106.500 +78 839 107.125 921600 0.000 107.000 108.000 +78 840 110.880 4120700 0.000 110.130 111.250 +78 841 111.125 1916900 0.000 109.875 111.250 +78 842 109.875 1531000 0.000 109.500 110.875 +79 843 102.380 1347000 0.000 101.500 103.500 +79 844 105.500 1634700 0.000 101.880 105.500 +79 845 105.880 1609400 0.000 105.500 107.630 +79 846 107.500 1438200 0.000 105.880 107.750 +79 847 107.880 1192900 0.000 107.130 108.380 +79 848 105.000 1077000 0.000 104.750 107.750 +80 849 107.375 1101700 0.000 107.125 109.000 +80 850 107.880 1162000 0.000 106.380 108.250 +80 851 108.250 1570000 0.000 106.000 109.000 +80 852 109.500 901300 0.000 108.625 109.500 +80 853 105.375 1418600 0.000 105.375 108.625 +80 854 103.500 2258600 0.000 102.875 106.625 +80 855 101.000 3113800 0.000 99.750 103.880 +80 856 100.250 2308100 0.000 99.500 102.000 +80 857 99.250 3121400 0.000 96.250 100.875 +80 858 99.380 1690200 0.000 98.250 100.380 +80 859 100.750 1330300 0.000 99.380 101.630 +80 860 105.375 1866500 0.000 101.750 105.375 +80 861 107.625 2658600 0.000 104.625 109.000 +80 862 107.750 1918300 0.000 106.500 108.750 +80 863 106.750 1270700 0.000 106.625 108.250 +81 864 111.500 3389800 0.000 111.250 116.000 +81 865 112.500 1909400 0.000 110.750 112.875 +81 866 113.380 1173800 0.000 112.130 113.750 +81 867 112.875 997900 0.000 112.375 113.125 +81 868 114.375 1471900 0.000 112.625 114.500 +81 869 112.880 1093100 0.000 112.880 114.630 +81 870 111.250 1435200 0.000 111.000 112.500 +81 871 111.500 954900 0.000 110.625 111.750 +81 872 113.500 1617100 0.000 111.500 113.500 +81 873 112.750 2463900 0.000 112.130 114.000 +81 874 113.750 1896000 0.000 112.250 114.630 +81 875 113.875 2084600 0.000 113.375 114.375 +81 876 113.880 341300 0.000 113.750 114.250 +81 877 113.500 396800 0.000 113.500 114.250 +81 878 113.630 714100 0.000 113.000 113.880 +81 879 113.375 728200 0.000 113.250 113.875 +81 880 113.000 461300 0.000 112.880 113.500 +81 881 112.125 1037700 0.000 112.125 113.750 +81 882 112.500 1307400 0.000 112.250 113.880 +81 883 112.130 1085100 0.000 111.880 113.000 +81 884 110.250 1189400 0.000 110.000 111.875 +81 885 109.000 1715500 0.000 108.750 110.375 +81 886 106.880 2078000 0.000 106.750 110.750 +81 887 108.380 962000 0.000 107.250 108.380 +81 888 108.130 608700 0.000 107.750 108.250 +81 889 106.750 1746500 0.000 105.500 107.130 +81 890 107.500 1168100 0.000 106.750 107.750 +81 891 109.130 1582000 0.000 106.630 109.250 +81 892 115.750 4330400 0.000 114.250 116.130 +81 893 117.625 4592500 0.000 114.875 118.250 +82 894 118.000 1963700 0.000 117.750 119.625 +82 895 119.250 2211000 0.000 117.875 120.125 +82 896 121.000 2841200 0.000 119.880 121.630 +82 897 122.625 2441100 0.000 120.500 122.625 +82 898 124.250 2478400 0.000 122.380 125.000 +82 899 124.500 1949200 0.000 123.000 125.380 +82 900 127.000 2524300 0.000 124.630 127.000 +82 901 126.750 1718300 0.000 126.250 127.130 +82 902 126.880 1919100 0.000 125.630 127.630 +83 903 128.500 2532000 0.000 128.380 131.000 +83 904 129.500 1751700 0.000 128.250 129.875 +83 905 132.375 1865300 0.000 129.625 132.500 +83 906 132.750 2372400 0.000 131.500 133.875 +83 907 134.630 1923800 0.000 132.250 135.000 +83 908 135.000 2485400 0.000 133.880 136.250 +83 909 137.375 2738900 0.000 135.375 137.750 +83 910 139.500 2278100 0.000 137.000 139.750 +83 911 137.880 1854200 0.000 137.880 138.750 +84 912 110.630 2214700 0.000 110.130 111.880 +84 913 108.500 3555500 0.000 108.130 111.000 +84 914 106.875 3026800 0.000 105.625 108.375 +84 915 109.250 2086700 0.000 106.750 109.250 +84 916 109.880 2089000 0.000 109.250 110.750 +84 917 109.750 1463800 0.000 109.375 110.750 +84 918 109.500 2109300 0.000 109.000 110.250 +84 919 108.500 1570400 0.000 108.125 109.375 +85 920 98.000 1438800 0.000 97.630 99.380 +85 921 97.250 1909500 0.000 96.750 98.250 +85 922 99.750 1417200 0.000 97.000 99.875 +85 923 99.880 893600 0.000 99.500 100.500 +85 924 101.250 791000 0.000 99.125 101.625 +85 925 104.250 3033600 0.000 99.750 104.750 +85 926 101.625 2025900 0.000 101.375 103.750 +85 927 99.875 1631900 0.000 99.375 101.250 +85 928 100.380 1315700 0.000 99.630 100.380 +86 929 94.625 1973000 92.000 91.875 94.750 +86 930 92.380 2515900 94.000 92.250 95.000 +86 931 91.250 2040100 92.625 91.250 94.250 +86 932 90.875 1611500 91.375 90.625 92.500 +86 933 90.250 1333300 90.750 89.750 90.875 +86 934 92.375 1338400 90.375 90.250 92.500 +86 935 95.380 3103900 94.000 93.500 95.750 +86 936 95.500 2253200 95.125 95.000 97.125 +86 937 96.375 3582000 95.000 92.625 98.125 +86 938 95.250 1209500 96.250 95.250 96.375 +86 939 92.880 1426400 95.250 92.880 95.500 +86 940 95.630 1273400 92.880 92.880 95.630 +87 941 92.250 1502600 91.125 90.000 92.250 +87 942 91.250 1632500 91.750 91.000 92.130 +87 943 89.880 1401700 90.380 89.630 90.880 +87 944 89.250 1724200 90.000 88.625 90.375 +87 945 89.750 945800 89.625 89.125 89.875 +87 946 90.250 1130700 89.750 89.750 90.875 +87 947 92.125 1581500 90.500 89.875 92.375 +87 948 90.750 1954000 92.750 90.130 93.250 +87 949 89.750 1295100 90.500 89.500 91.130 +87 950 89.625 1314500 90.000 89.125 91.125 +87 951 89.630 1148400 90.000 89.380 90.750 +87 952 89.750 1363200 89.750 89.125 90.625 +87 953 89.875 1446100 89.750 89.250 90.375 +87 954 89.380 1056500 89.880 88.750 90.250 +88 955 82.380 1926400 83.380 82.250 84.880 +88 956 84.875 3020100 82.625 82.625 85.000 +88 957 85.125 2275600 85.375 84.625 85.875 +88 958 86.000 2052900 85.630 84.500 86.130 +88 959 87.500 1712300 86.130 85.880 87.500 +88 960 88.250 3316600 89.500 88.125 89.750 +88 961 88.500 2215600 88.750 87.625 88.875 +88 962 88.625 2063100 88.500 87.500 88.625 +88 963 89.500 1792800 88.500 88.250 89.750 +88 964 90.000 2117200 89.630 89.500 90.250 +88 965 90.125 1324900 89.875 89.625 90.125 +88 966 89.250 1475400 90.000 89.000 90.630 +89 967 89.250 1475400 90.000 89.000 90.630 +89 968 88.130 993100 89.130 88.130 89.250 +89 969 88.125 1000900 87.875 87.750 88.750 +89 970 88.500 1108900 88.130 87.380 88.500 +89 971 88.130 1268600 88.880 87.750 88.880 +89 972 90.750 1681900 88.500 88.250 90.750 +89 973 90.625 2074400 91.250 90.375 92.125 +89 974 92.500 2341200 92.500 92.125 93.000 +89 975 93.875 2044900 92.500 92.375 94.000 +90 976 62.880 2541800 62.380 62.380 63.380 +90 977 56.130 12193500 0.000 56.000 59.880 +90 978 51.875 13421200 53.625 50.750 54.875 +90 979 53.000 8025400 0.000 51.250 53.380 +90 980 51.375 7658000 53.125 50.250 53.250 +90 981 48.875 7384600 51.250 48.750 51.625 +90 982 51.750 6115700 50.000 49.000 52.000 +90 983 51.250 4186200 51.625 51.000 52.625 +91 984 46.380 3638900 47.000 45.880 47.000 +91 985 48.625 3420500 46.500 46.500 48.750 +91 986 48.875 3054500 48.750 47.500 49.250 +91 987 49.000 7802400 52.875 48.750 53.250 +91 988 49.625 3307500 49.125 48.250 50.000 +91 989 50.250 3095700 50.000 49.750 50.875 +91 990 51.500 2981500 51.000 50.875 51.625 +91 991 52.625 3039500 52.000 52.000 52.625 +91 992 52.125 2635400 52.250 52.000 53.000 +92 993 42.250 2494200 43.625 42.000 43.625 +92 994 42.375 3922300 41.875 41.125 42.375 +92 995 45.630 6260300 44.880 44.500 45.880 +92 996 44.000 4216700 46.000 43.875 46.375 +93 997 51.875 2331700 52.250 51.500 52.625 +93 998 52.000 2595300 51.875 51.625 52.500 +93 999 53.125 2067200 52.375 51.875 53.250 +93 1000 55.125 4184000 54.000 53.750 55.250 +93 1001 55.750 1211000 55.500 55.125 56.000 +93 1002 54.380 2487900 56.000 54.380 56.130 +93 1003 53.875 1976800 54.500 53.750 55.125 +93 1004 53.130 2385300 54.750 53.130 54.750 +93 1005 53.750 1865800 53.250 53.250 54.125 +94 1006 59.000 1846800 58.130 58.000 59.130 +94 1007 59.500 2570000 59.000 58.500 59.875 +94 1008 58.500 2379800 60.000 58.500 60.000 +94 1009 58.875 1458600 58.500 58.000 59.375 +94 1010 59.250 1734900 58.875 58.500 59.500 +94 1011 58.630 1690600 59.130 58.500 59.380 +94 1012 58.130 1503100 58.750 58.130 58.880 +94 1013 58.750 1804200 58.250 58.250 59.000 +94 1014 58.630 1586000 58.750 58.380 59.500 +94 1015 57.500 1528100 58.500 57.130 58.500 +94 1016 57.125 2255900 57.375 56.375 57.375 +94 1017 56.000 1959500 56.750 55.750 57.000 +94 1018 55.250 2278500 55.875 55.125 55.875 +94 1019 55.250 3176600 55.125 54.750 55.625 +94 1020 58.630 4219300 55.750 55.750 58.750 +94 1021 58.250 10154900 58.880 55.380 59.380 +95 1022 52.880 1565600 53.380 52.750 53.630 +95 1023 53.250 1985500 52.500 52.000 53.380 +95 1024 54.000 1873700 53.375 53.375 54.625 +95 1025 54.500 1727400 54.380 54.000 54.630 +95 1026 54.625 1495700 55.250 54.500 55.250 +95 1027 52.750 2290200 54.880 52.500 55.000 +96 1028 53.380 2097500 53.500 51.380 53.750 +96 1029 52.250 2207700 53.250 51.750 53.500 +96 1030 58.380 8758100 55.000 54.000 58.630 +96 1031 58.750 5660500 58.375 58.000 59.500 +97 1032 58.500 2217200 58.880 58.500 59.250 +97 1033 57.125 2297500 57.875 56.750 58.250 +97 1034 57.500 1908600 57.375 56.875 57.875 +97 1035 58.000 1608500 57.500 56.875 58.375 +97 1036 58.380 1376600 58.130 57.880 58.630 +97 1037 57.750 1500300 58.250 57.500 58.250 +98 1038 55.625 1817200 56.750 55.625 56.875 +98 1039 55.880 3189300 55.750 54.500 56.250 +98 1040 62.380 8807500 59.000 58.000 62.630 +98 1041 61.380 3714100 62.250 61.130 62.380 +99 1042 72.250 2341600 73.625 72.250 74.125 +99 1043 73.125 2040800 72.500 71.875 73.125 +99 1044 73.250 1536400 72.880 72.750 73.750 +99 1045 74.375 2463400 73.500 73.375 74.500 +99 1046 75.380 3933900 74.000 73.750 75.750 +99 1047 75.000 5216900 75.500 73.875 76.125 +99 1048 74.625 3372500 74.625 74.125 75.375 +99 1049 73.000 2615200 74.625 72.875 74.625 +99 1050 73.880 2829600 72.630 72.630 74.380 +100 1051 71.750 1418500 72.750 71.500 73.000 +100 1052 72.130 2742400 72.000 70.250 72.880 +100 1053 73.625 3292000 72.375 72.250 73.875 +100 1054 74.380 1458600 73.630 73.500 74.380 +100 1055 74.625 2562800 74.875 74.500 75.250 +100 1056 74.380 1352100 74.380 74.130 74.880 +100 1057 74.250 1450700 74.250 73.875 74.375 +100 1058 74.250 1496400 74.125 74.125 74.875 +101 1059 87.130 2576400 87.880 86.000 88.250 +101 1060 89.000 5977700 90.500 88.630 90.500 +101 1061 91.625 5886400 89.375 89.125 92.000 +101 1062 93.500 4318900 91.500 91.000 93.880 +101 1063 93.625 3346300 93.125 92.875 93.875 +101 1064 95.250 3694500 93.125 92.875 95.625 +102 1065 89.375 3394000 90.125 88.375 90.750 +102 1066 89.130 3308400 89.630 88.630 90.250 +102 1067 90.250 2995700 90.500 89.750 90.750 +102 1068 91.750 2905200 91.380 90.880 92.250 +102 1069 93.000 2562500 91.875 91.875 93.375 +102 1070 93.000 2192900 93.630 93.000 94.000 +103 1071 109.000 3285600 106.500 105.625 109.500 +103 1072 108.625 1257200 109.125 108.500 109.750 +103 1073 108.875 1430800 109.125 108.625 109.375 +103 1074 109.630 1795300 108.880 108.880 110.000 +103 1075 109.875 1966800 110.000 109.625 110.375 +103 1076 109.125 1971400 109.875 108.875 110.375 +103 1077 109.250 2327700 109.500 108.380 109.630 +103 1078 110.750 1748300 109.250 109.250 110.875 +103 1079 112.000 2701000 111.000 110.250 112.125 +103 1080 112.375 2946800 112.125 110.750 112.750 +103 1081 113.625 3029700 112.375 112.250 114.625 +103 1082 111.375 3698100 113.875 111.125 114.000 +103 1083 106.750 4700700 111.380 106.500 111.380 +103 1084 107.630 5514300 107.000 105.630 108.250 +103 1085 106.625 3239000 108.125 106.500 108.500 +104 1086 91.880 3744400 89.130 89.130 91.880 +104 1087 89.375 3605800 92.000 89.125 92.000 +104 1088 90.875 2674000 89.875 89.750 91.125 +104 1089 91.250 1877400 90.880 90.880 91.880 +104 1090 91.750 945400 92.000 91.625 92.125 +104 1091 92.000 1572000 91.750 90.750 92.000 +105 1092 87.250 4624600 88.130 86.000 88.500 +105 1093 86.375 2664400 87.250 86.125 87.250 +105 1094 83.125 3742800 86.500 83.125 86.500 +105 1095 87.000 4613000 84.125 83.500 87.750 +105 1096 87.625 4821100 86.750 85.750 89.250 +105 1097 96.250 9856900 90.000 89.625 96.250 +105 1098 102.000 13145900 96.625 96.500 104.125 +105 1099 102.250 5293000 101.500 101.375 103.875 +105 1100 103.000 4959100 100.750 100.750 104.500 +105 1101 107.000 7989000 103.130 103.130 109.000 +105 1102 104.125 4721800 107.625 104.125 108.000 +106 1103 113.625 3467900 114.000 113.500 114.875 +106 1104 114.250 3237400 113.880 113.750 115.000 +106 1105 117.625 5835300 114.375 114.250 118.500 +106 1106 118.000 4985900 118.000 117.500 118.880 +106 1107 119.125 5322200 116.750 116.750 120.875 +106 1108 120.250 4097300 119.000 118.630 120.630 +106 1109 124.125 5355800 121.750 121.250 125.625 +106 1110 125.630 4126200 125.000 124.000 126.130 +106 1111 124.880 2882500 123.880 123.880 125.500 +107 1112 108.750 4722900 114.875 108.375 115.125 +107 1113 110.750 4558900 108.375 107.625 112.000 +107 1114 111.250 3619800 111.130 110.250 113.750 +107 1115 109.750 3046600 111.875 109.500 112.375 +107 1116 111.250 2931600 110.380 110.380 113.750 +107 1117 110.380 2379500 112.130 110.000 112.750 +107 1118 117.625 5407700 111.000 110.500 118.125 +107 1119 119.750 4149900 117.500 116.130 120.500 +108 1120 91.750 3683600 89.630 89.130 92.630 +108 1121 103.630 9014900 96.000 95.630 103.750 +108 1122 103.875 3990400 103.125 101.625 104.125 +108 1123 104.750 3338200 103.500 103.380 105.500 +108 1124 107.380 3992800 105.250 103.750 108.000 +108 1125 107.500 3505300 107.125 107.000 108.875 +108 1126 107.250 3120600 108.000 107.250 109.000 +108 1127 108.875 2265800 108.375 107.750 109.375 +108 1128 109.250 2151100 108.000 108.000 109.375 +109 1129 124.750 1312200 124.750 124.000 125.500 +109 1130 124.500 1864400 124.875 124.125 127.125 +109 1131 123.750 2446500 124.000 123.130 125.250 +109 1132 125.880 2013200 124.250 124.000 125.880 +109 1133 125.000 1845100 125.750 124.625 126.250 +109 1134 126.625 2302300 125.875 125.625 127.125 +109 1135 127.875 1835400 126.875 126.875 128.000 +109 1136 128.380 3148000 128.000 127.630 129.500 +109 1137 126.250 2402400 129.000 126.130 129.380 +109 1138 127.750 2248500 125.500 125.500 128.000 +109 1139 129.875 2858300 128.500 128.250 130.125 +109 1140 130.000 1400900 129.880 129.130 130.000 +109 1141 129.500 2863900 130.750 128.630 131.250 +109 1142 127.625 1875700 129.500 127.375 129.500 +109 1143 125.625 2135400 128.750 125.250 129.125 +109 1144 129.375 3473700 125.500 124.000 129.500 +110 1145 133.375 2886400 130.625 130.250 133.875 +110 1146 133.750 3481600 133.125 132.750 135.125 +110 1147 134.375 1569400 133.375 132.875 134.500 +110 1148 134.500 1429200 134.375 134.000 134.875 +110 1149 133.250 2724100 135.000 133.000 135.750 +110 1150 134.750 2392300 133.500 133.250 135.875 +110 1151 136.880 2774500 134.630 134.630 137.630 +110 1152 145.000 7803800 137.630 137.380 145.500 +110 1153 146.750 5777100 144.750 143.000 150.000 +111 1154 157.625 2833000 158.125 156.125 158.500 +111 1155 158.000 5515400 157.500 157.000 160.750 +111 1156 158.125 2799700 158.250 157.750 160.250 +111 1157 159.375 931300 158.125 158.125 159.625 +111 1158 163.000 3316000 159.630 159.630 163.500 +111 1159 162.625 4677800 163.625 162.625 166.000 +111 1160 162.000 4437400 162.875 159.125 164.000 +111 1161 158.500 3036300 161.375 158.375 162.625 +111 1162 155.625 6051900 151.000 151.000 158.000 +111 1163 160.000 2769900 156.630 156.250 160.130 +112 1164 141.250 3583000 143.130 140.250 143.500 +112 1165 137.625 6308500 140.750 137.125 140.875 +112 1166 143.875 4282700 137.625 0.000 144.000 +112 1167 146.500 4198400 144.380 144.130 148.000 +112 1168 146.875 3670400 147.000 144.000 147.500 +112 1169 143.000 1772200 146.880 143.000 146.880 +113 1170 136.500 2634400 134.500 133.250 136.880 +113 1171 137.880 3931900 138.630 134.380 139.000 +113 1172 137.500 1845300 137.380 135.750 138.380 +113 1173 137.750 2655100 137.375 136.750 139.750 +113 1174 139.750 2343200 138.000 137.750 140.130 +113 1175 137.380 2356700 139.630 136.630 141.250 +113 1176 140.000 3022200 137.250 134.750 140.130 +113 1177 142.380 3917100 140.000 139.000 143.750 +113 1178 153.625 10659100 150.500 149.500 157.000 +113 1179 150.750 3005000 152.250 150.625 152.875 +114 1180 98.625 2949600 102.437 98.625 104.125 +114 1181 99.130 2060000 100.130 99.000 100.810 +114 1182 101.690 1069100 99.380 99.250 101.750 +114 1183 102.750 2307300 102.875 102.750 103.687 +114 1184 103.130 3558000 103.690 102.940 105.500 +114 1185 104.625 2736500 104.000 103.187 105.187 +114 1186 105.625 2635000 104.500 104.062 105.750 +114 1187 106.437 5017000 105.562 105.000 106.812 +114 1188 105.250 3555600 105.375 105.062 107.187 +114 1189 104.250 4308200 105.000 102.625 105.000 +114 1190 104.190 4059400 103.500 102.940 105.500 +114 1191 100.062 7182800 104.312 99.375 104.875 +114 1192 100.130 6410700 98.250 97.500 101.130 +114 1193 102.130 3414500 100.560 100.310 102.440 +114 1194 102.625 4275400 103.500 101.250 104.000 +114 1195 103.437 6321700 103.187 102.375 104.937 +114 1196 105.000 4189100 104.875 104.500 105.187 +114 1197 108.380 4970600 105.190 104.690 108.380 +114 1198 100.125 13907800 100.250 99.625 102.000 +114 1199 99.380 6583500 99.810 98.500 100.630 +114 1200 99.187 3911500 99.812 98.937 100.000 +114 1201 98.125 2807900 99.250 98.000 99.937 +114 1202 96.500 7019700 96.750 95.630 97.880 +114 1203 97.000 5192400 97.130 96.500 98.000 +114 1204 98.187 4413600 97.000 97.000 99.312 +114 1205 98.750 2900100 98.312 98.000 99.562 +114 1206 100.562 3437700 100.000 100.000 101.250 +114 1207 99.380 3585600 100.130 98.690 100.250 +115 1208 98.125 5205100 99.125 98.125 99.812 +115 1209 96.125 6464200 97.125 95.875 98.250 +115 1210 97.500 4167100 97.380 96.690 98.190 +115 1211 99.130 3403400 97.880 97.440 99.880 +115 1212 100.250 2951700 99.187 98.187 100.625 +115 1213 99.630 2925300 100.880 99.000 101.130 +115 1214 101.250 3420700 99.690 99.250 101.380 +115 1215 100.880 3585100 101.310 99.560 101.810 +115 1216 101.560 3594700 100.630 100.560 102.310 +115 1217 102.875 2585000 101.687 101.437 103.000 +115 1218 102.062 4107800 103.000 101.437 103.250 +116 1219 108.812 4240700 109.000 107.625 109.125 +116 1220 106.130 4884000 108.750 106.000 109.250 +116 1221 108.130 4276900 106.250 106.190 109.500 +116 1222 111.750 4039200 110.000 109.940 112.310 +116 1223 112.000 3030000 111.880 110.060 112.130 +116 1224 112.440 3187800 112.500 112.000 114.690 +116 1225 113.000 1636400 112.380 112.310 113.440 +116 1226 114.125 2184600 114.125 113.562 114.812 +116 1227 114.810 4356000 114.130 113.500 116.310 +116 1228 116.750 3166400 116.000 115.500 116.937 +116 1229 115.190 3219800 116.560 114.500 116.630 +117 1230 115.000 2258200 113.750 113.630 115.560 +117 1231 117.062 3189500 114.750 114.625 118.500 +117 1232 118.500 3070100 118.250 117.375 119.250 +117 1233 119.380 2339800 118.500 118.250 120.000 +117 1234 119.625 2268600 119.750 119.000 120.625 +117 1235 117.000 2557800 119.250 117.000 120.000 +117 1236 118.380 2180100 117.130 116.000 118.940 +117 1237 120.187 3168400 118.562 118.500 120.375 +117 1238 122.000 4489200 121.000 120.312 123.000 +117 1239 128.130 12308600 128.250 127.750 131.000 +117 1240 127.440 6049900 126.000 124.500 128.440 +117 1241 123.880 4153100 127.190 123.690 129.000 +117 1242 124.250 2874600 125.875 122.500 125.937 +118 1243 123.500 4994700 125.750 123.375 127.437 +118 1244 122.500 5579800 122.130 118.190 122.500 +118 1245 126.562 4965100 121.375 120.625 126.562 +118 1246 129.130 4557100 128.000 127.630 129.940 +118 1247 130.000 3246800 129.000 128.500 130.687 +118 1248 130.500 3319400 130.000 128.190 130.940 +119 1249 133.625 3820400 134.500 132.187 136.187 +119 1250 131.437 3544900 133.750 130.000 134.125 +119 1251 128.500 4149700 130.750 127.630 131.500 +119 1252 125.375 6268700 125.062 123.375 126.437 +119 1253 124.810 7302600 124.130 118.940 125.250 +119 1254 120.250 6721000 123.750 117.310 123.750 +119 1255 119.250 7183700 124.000 118.750 124.000 +119 1256 120.750 6582400 119.500 118.125 122.875 +119 1257 123.500 9156300 120.750 116.812 123.500 +119 1258 127.312 5841600 124.437 122.500 127.937 +119 1259 130.875 5402500 130.125 129.562 132.875 +119 1260 128.190 4696800 130.500 126.560 130.500 +120 1261 147.380 3456500 148.500 147.190 149.940 +120 1262 147.880 4096300 148.880 145.750 149.750 +120 1263 149.000 3293000 147.690 146.810 149.560 +120 1264 149.940 3569000 148.880 147.500 150.940 +120 1265 151.380 3788700 150.750 149.940 152.190 +120 1266 156.000 4371500 150.880 150.690 156.750 +120 1267 157.125 5109100 157.000 156.500 158.875 +120 1268 157.880 4333900 156.690 156.630 159.880 +120 1269 157.437 2997600 157.000 155.750 157.500 +121 1270 165.000 3885800 163.500 162.500 165.810 +121 1271 164.375 2826400 166.875 164.312 166.875 +121 1272 166.062 4381600 165.437 165.062 169.625 +121 1273 171.560 4908400 166.630 166.630 172.190 +121 1274 176.375 3739300 171.562 171.562 178.937 +121 1275 182.250 4352300 177.500 175.250 183.000 +121 1276 185.000 3535000 182.690 181.130 185.380 +121 1277 187.937 1524900 184.750 184.062 187.937 +121 1278 189.250 2634600 186.500 186.000 189.937 +121 1279 187.125 1881100 188.625 187.000 188.937 +121 1280 186.750 2406600 186.875 186.312 188.625 +121 1281 184.375 1932400 186.750 183.500 187.187 +121 1282 183.000 4074800 185.000 181.500 186.500 +121 1283 189.630 4953800 183.000 182.810 189.880 +122 1284 177.937 4600100 172.250 171.062 177.937 +122 1285 176.940 4554600 178.000 175.750 180.000 +122 1286 173.750 4318700 177.250 173.130 179.690 +122 1287 173.625 5234800 172.000 169.312 173.937 +122 1288 169.750 7399000 171.000 166.500 171.880 +122 1289 168.375 4795400 169.500 165.125 169.500 +122 1290 167.750 4371200 170.500 166.880 170.940 +122 1291 166.750 3871500 168.500 165.062 168.937 +122 1292 171.000 10750500 173.500 170.125 177.000 +122 1293 178.380 5803200 173.880 173.750 178.940 +122 1294 178.875 3779900 178.625 177.062 180.000 +122 1295 182.190 5009500 179.880 178.560 184.880 +122 1296 181.500 3504900 181.630 180.250 182.880 +123 1297 182.000 3951400 179.187 177.125 182.312 +123 1298 181.000 3303000 182.000 180.190 183.500 +123 1299 178.060 3452700 179.500 178.000 180.000 +123 1300 177.630 7252500 177.500 172.940 177.750 +123 1301 168.562 9748400 176.625 168.000 178.000 +123 1302 167.000 5948300 168.687 166.625 170.500 +123 1303 165.380 5007500 167.000 164.000 167.630 +123 1304 169.500 5910500 168.000 166.500 170.500 +123 1305 171.312 4914400 172.000 169.500 173.250 +123 1306 172.380 4133200 171.060 170.560 175.880 +123 1307 178.562 3733400 177.000 176.875 181.250 +123 1308 177.250 2917600 180.000 176.500 181.500 +124 1309 187.000 4265500 186.000 182.312 188.062 +124 1310 186.310 3055900 185.630 184.000 188.060 +124 1311 183.440 4489900 183.500 181.060 183.630 +124 1312 180.000 3423200 184.000 179.940 184.000 +124 1313 179.380 3630500 182.750 177.630 183.880 +124 1314 177.750 4165200 179.500 173.500 179.500 +124 1315 170.375 5964400 177.500 170.125 177.500 +124 1316 166.750 7691800 170.880 164.060 172.310 +124 1317 169.750 7358000 167.000 163.000 169.750 +124 1318 171.875 6580800 169.750 165.125 171.875 +124 1319 194.500 19584900 197.250 192.687 198.750 +124 1320 199.750 10375500 193.380 192.940 206.560 +124 1321 209.880 9333000 204.750 202.310 210.750 +124 1322 212.000 10464600 212.000 208.500 215.250 +124 1323 205.000 5670900 209.250 203.500 210.500 +125 1324 209.250 4169800 211.880 208.000 214.000 +125 1325 217.250 4993400 211.250 211.062 217.500 +125 1326 218.630 4774200 217.250 217.060 221.880 +125 1327 221.000 4186400 220.500 217.500 221.875 +125 1328 225.500 6033400 221.125 215.500 228.437 +125 1329 246.000 12165700 234.000 234.000 246.000 +125 1330 239.250 9036900 241.500 236.000 243.500 +125 1331 237.500 5780200 236.000 232.000 239.625 +125 1332 238.500 4316900 238.250 237.000 240.625 +125 1333 235.875 3895800 238.250 233.062 238.750 +125 1334 232.875 3220900 235.625 232.500 236.375 +125 1335 230.380 3452300 232.750 229.190 233.380 +125 1336 223.750 4299100 230.310 221.750 230.690 +125 1337 221.187 4702500 222.500 221.000 226.000 +125 1338 236.250 8314000 223.000 221.440 236.630 +125 1339 116.000 10552600 116.687 112.625 116.875 +125 1340 116.000 6379500 116.000 114.187 116.750 +126 1341 124.440 6043500 122.750 122.310 124.880 +126 1342 122.000 8442100 123.375 121.000 123.625 +126 1343 122.380 6463000 122.000 120.880 124.060 +126 1344 122.940 6245300 122.630 122.060 125.880 +126 1345 124.000 4354600 125.000 122.750 125.190 +126 1346 123.187 4773000 124.250 122.687 125.625 +127 1347 98.000 18720400 94.000 93.562 98.250 +127 1348 104.750 19673300 99.000 98.380 105.000 +127 1349 107.880 13681500 105.500 104.440 108.440 +127 1350 106.125 10665000 108.812 106.000 109.875 +127 1351 104.060 9710300 105.000 101.810 105.000 +127 1352 105.000 3108400 105.937 104.125 106.000 +127 1353 104.187 5677700 104.937 103.375 104.937 +127 1354 103.060 5719900 103.690 102.130 104.500 +127 1355 103.750 5336000 102.562 102.250 104.437 +127 1356 105.560 6208600 103.440 103.380 106.310 +127 1357 112.000 14679900 109.812 107.937 112.875 +127 1358 116.000 9924400 113.000 112.500 116.500 +128 1359 109.190 7117300 108.000 105.750 109.690 +128 1360 109.125 7680700 110.687 108.625 111.062 +128 1361 108.060 4834900 109.060 107.750 110.440 +128 1362 110.125 4773500 108.500 108.062 110.125 +128 1363 108.375 4005500 109.875 108.250 110.312 +128 1364 108.625 4528700 109.125 108.187 110.437 +128 1365 109.750 3740700 109.687 108.125 110.000 +128 1366 109.250 4081300 109.560 109.060 110.750 +128 1367 109.060 2683100 110.440 108.750 110.500 +128 1368 108.750 3435100 109.690 108.560 110.500 +128 1369 107.875 2870300 109.187 106.625 109.500 +128 1370 115.625 10346800 112.437 111.875 115.812 +128 1371 112.060 8227800 114.000 110.880 114.500 +129 1372 107.000 5665900 107.500 106.130 108.250 +129 1373 109.000 6622500 109.000 107.880 109.810 +129 1374 110.000 8856400 108.250 107.687 111.687 +129 1375 112.750 6563500 110.000 109.940 113.380 +129 1376 113.500 6771400 112.810 111.940 114.690 +129 1377 114.250 6137800 114.500 113.875 115.375 +129 1378 115.250 6492400 115.875 111.625 115.875 +129 1379 121.500 12124300 115.375 115.062 122.250 +129 1380 126.880 11862900 125.000 124.630 128.250 +129 1381 122.500 8163000 125.750 122.060 127.000 +130 1382 102.940 6402800 104.130 102.630 104.810 +130 1383 103.500 7428100 102.380 101.000 104.380 +130 1384 117.000 22927900 112.000 111.750 117.812 +130 1385 114.440 8764700 117.000 113.750 117.000 +131 1386 90.130 7705300 90.190 89.810 94.440 +131 1387 86.000 12579500 85.880 84.810 88.000 +131 1388 81.560 12724900 85.500 80.060 87.940 +131 1389 89.000 9514000 84.500 84.500 89.375 +131 1390 84.810 6052300 89.000 84.250 89.560 +132 1391 92.750 5671800 93.750 91.810 94.000 +132 1392 96.687 9712900 95.375 94.312 97.750 +132 1393 108.312 25243300 104.375 103.500 110.000 +132 1394 111.250 14760400 107.500 107.250 113.937 +133 1395 106.470 6793100 107.170 105.370 107.340 +133 1396 99.290 16688100 105.000 97.905 105.010 +133 1397 95.490 14494800 97.900 95.020 98.400 +133 1398 98.390 12127900 95.100 94.200 98.440 +133 1399 94.960 14246600 95.400 93.340 98.900 +134 1400 99.700 9957300 97.000 97.000 99.880 +134 1401 106.500 19849500 103.000 102.300 110.000 +134 1402 114.470 25822400 112.000 110.450 115.900 +134 1403 114.830 12759100 114.350 113.750 116.400 +135 1404 119.600 7726900 117.500 117.500 119.600 +135 1405 117.800 5629100 119.520 117.760 119.900 +135 1406 115.270 5818000 117.350 115.080 117.590 +135 1407 112.650 9598700 114.000 112.000 114.100 +135 1408 111.800 6757600 112.650 111.640 113.860 +135 1409 112.890 5138900 112.500 111.900 113.600 +135 1410 113.640 3812200 113.250 112.850 114.750 +135 1411 116.970 6154400 113.640 113.560 117.600 +135 1412 117.500 9625700 117.600 116.500 119.000 +135 1413 117.250 6423700 117.000 117.000 118.250 +135 1414 116.100 3099100 117.500 115.600 117.500 +135 1415 117.360 6542700 116.300 116.100 118.140 +136 1416 104.720 9312600 106.500 103.650 106.950 +136 1417 101.960 10266800 104.730 101.600 105.400 +136 1418 103.850 8894800 101.600 101.560 104.130 +136 1419 107.250 8767200 105.900 105.620 107.800 +136 1420 108.530 7841800 106.150 106.110 109.300 +136 1421 107.820 5330100 108.530 107.280 109.380 +137 1422 108.800 5675100 108.500 107.210 109.470 +137 1423 108.180 3704100 107.800 107.100 110.090 +137 1424 106.510 4176800 107.250 105.790 107.370 +137 1425 106.250 4028300 105.800 105.260 107.370 +137 1426 104.190 6398000 105.000 104.100 106.100 +137 1427 104.080 6763200 103.700 102.700 104.540 +137 1428 104.950 5254800 103.850 102.810 104.950 +137 1429 105.860 6024400 105.000 104.650 106.640 +137 1430 106.200 5730200 106.500 105.400 106.850 +137 1431 105.010 4713100 106.350 105.010 106.590 +138 1432 91.300 13783100 94.450 90.300 94.450 +138 1433 90.000 13172200 91.300 87.490 91.500 +138 1434 91.720 11077200 90.200 89.900 92.710 +138 1435 92.710 8676200 91.720 91.340 93.480 +138 1436 93.770 7221200 92.300 92.100 93.900 +138 1437 96.950 11118800 92.900 92.400 97.620 +138 1438 97.310 9785100 97.150 96.800 98.880 +138 1439 98.020 9733000 96.650 95.370 98.450 +138 1440 98.500 7093400 98.030 96.750 99.000 +138 1441 97.140 7525300 98.500 96.760 98.500 +139 1442 123.200 7950400 121.120 120.060 123.210 +139 1443 120.250 8061000 121.550 120.120 122.300 +139 1444 121.100 7036000 120.800 119.410 121.480 +139 1445 121.340 6225500 120.150 120.150 122.140 +139 1446 122.200 5596200 121.510 121.350 122.970 +139 1447 123.890 8054200 121.900 121.010 124.700 +140 1448 75.490 7953000 77.850 75.200 78.250 +140 1449 74.650 13296100 75.400 73.250 75.950 +140 1450 75.600 8909700 75.450 75.300 77.400 +140 1451 76.170 8592000 74.200 73.250 76.770 +140 1452 77.140 7775800 76.180 75.160 77.750 +140 1453 75.940 7240900 75.600 75.470 76.900 +141 1454 72.000 10945600 71.350 71.250 73.620 +141 1455 67.600 12056000 72.010 67.160 72.410 +141 1456 68.580 12012900 67.850 67.700 69.400 +141 1457 70.510 8469900 67.950 67.870 70.710 +141 1458 73.500 5243000 72.450 72.000 73.900 +141 1459 71.300 9488100 73.000 71.030 73.480 +141 1460 69.670 11165100 71.400 69.450 72.250 +141 1461 68.760 10073400 70.600 68.530 71.250 +141 1462 69.410 11034600 68.000 67.550 69.750 +142 1463 69.350 10989800 68.900 67.300 70.000 +142 1464 66.400 11576400 68.750 65.760 69.470 +142 1465 71.180 11918600 70.370 68.000 71.400 +142 1466 71.790 10136400 70.500 70.020 72.700 +142 1467 70.400 12708100 70.750 69.110 71.600 +142 1468 68.250 8832000 70.400 67.220 70.700 +142 1469 67.880 6915000 68.210 67.123 68.230 +142 1470 65.990 7126900 67.940 65.850 68.350 +142 1471 67.900 8370800 67.000 66.610 69.420 +142 1472 69.170 8036800 69.000 66.800 69.900 +142 1473 71.610 8432500 69.200 68.350 72.050 +142 1474 71.830 6940900 70.610 70.400 74.300 +142 1475 71.770 5263300 71.050 70.610 72.640 +143 1476 55.070 12155800 56.050 54.810 56.700 +143 1477 57.580 12672600 54.650 54.010 58.480 +143 1478 63.920 17852500 62.000 61.500 63.920 +143 1479 63.420 7925300 61.540 61.540 63.800 +143 1480 68.480 14600200 67.750 66.580 68.480 +143 1481 64.900 15828900 66.600 64.240 67.005 +143 1482 72.200 21344500 72.800 71.230 73.000 +143 1483 74.250 12532700 71.730 70.260 74.250 +144 1484 72.100 10570000 75.500 71.750 75.550 +144 1485 74.560 7538800 72.500 72.160 74.620 +144 1486 76.560 12544800 75.150 74.560 77.500 +144 1487 76.740 11648500 76.560 74.200 77.050 +144 1488 78.670 12865800 77.000 76.020 79.400 +144 1489 78.940 9844300 78.670 78.500 79.790 +144 1490 80.400 9395200 78.900 78.320 80.500 +144 1491 82.500 12005100 82.000 81.700 83.810 +144 1492 81.680 10466700 81.750 80.470 82.010 +145 1493 80.260 5922700 79.750 79.440 81.507 +145 1494 79.760 2420200 80.200 79.760 81.000 +145 1495 78.500 5784300 79.810 78.260 80.700 +145 1496 77.360 6334400 78.500 76.610 79.180 +145 1497 76.250 8066600 77.000 75.600 77.430 +145 1498 77.500 7871400 77.100 77.100 78.460 +145 1499 80.570 7864500 78.800 78.190 80.570 +145 1500 81.650 5963400 80.700 80.210 81.650 +145 1501 83.590 7923300 81.900 81.810 84.590 +145 1502 86.000 11907000 83.950 83.750 86.180 +145 1503 84.190 9509700 85.550 84.070 85.690 +146 1504 76.500 5617700 77.500 76.500 78.090 +146 1505 75.860 11218600 76.100 74.310 76.350 +146 1506 77.450 8300100 76.400 75.350 77.450 +146 1507 79.330 7793000 78.020 77.910 79.510 +146 1508 79.510 5441300 79.080 78.826 79.700 +146 1509 79.040 6295600 79.570 78.710 80.050 +147 1510 79.850 7024700 81.130 79.800 81.330 +147 1511 79.750 7472200 79.700 78.730 80.680 +147 1512 80.690 5298500 79.690 79.380 80.820 +147 1513 80.880 4615600 81.050 80.350 81.270 +147 1514 81.020 4262200 80.870 80.280 81.500 +147 1515 81.510 4634200 81.450 80.650 81.540 +148 1516 89.700 8842500 91.450 89.010 91.510 +148 1517 90.310 5529700 89.900 89.750 90.460 +148 1518 94.020 19813600 95.070 93.550 95.650 +148 1519 95.320 9302100 95.000 94.710 95.350 +148 1520 97.100 9285300 96.000 95.730 97.440 +148 1521 97.700 6952700 97.230 96.640 98.040 +148 1522 97.510 4371600 97.840 97.320 98.160 +149 1523 99.300 3611700 100.060 99.300 100.300 +149 1524 99.710 3562400 99.100 99.080 100.090 +149 1525 99.370 3930500 99.990 99.320 100.000 +149 1526 98.420 4830500 99.310 98.150 99.770 +149 1527 97.800 5447700 98.420 97.520 99.230 +149 1528 97.310 5690200 98.600 97.190 98.600 +149 1529 95.960 6629600 97.400 95.460 97.510 +149 1530 96.790 7012100 95.200 95.200 97.460 +149 1531 96.540 3362500 96.500 96.230 97.090 +150 1532 96.840 4807000 96.570 95.600 96.890 +150 1533 96.390 3548500 96.580 96.130 96.920 +150 1534 96.450 3971800 95.950 95.560 96.980 +150 1535 94.590 5084200 96.490 94.590 96.880 +150 1536 94.530 6391400 94.300 93.770 95.280 +150 1537 93.060 6536400 94.380 92.680 94.740 +150 1538 91.210 8560800 92.000 91.150 92.980 +150 1539 93.300 6290600 92.000 91.680 93.380 +151 1540 85.350 5677300 85.300 85.050 85.940 +151 1541 83.650 7530100 85.000 83.580 85.010 +151 1542 83.890 6595200 84.400 83.510 84.500 +151 1543 84.950 6009200 84.000 83.420 85.250 +151 1544 85.250 6001500 85.900 85.200 86.090 +151 1545 84.130 6298700 84.840 83.780 85.240 +152 1546 83.690 5268500 83.700 83.100 83.980 +152 1547 82.210 7134900 83.050 81.900 83.050 +152 1548 83.910 5916900 82.540 82.510 83.940 +152 1549 84.020 4361500 83.700 83.510 84.560 +152 1550 84.040 3559500 84.100 83.660 84.540 +152 1551 85.130 4833700 83.600 83.570 85.130 +153 1552 84.160 4650300 84.100 83.980 84.440 +153 1553 84.480 3874200 84.350 83.880 84.650 +153 1554 84.980 4204600 84.480 84.150 84.980 +153 1555 85.740 5198000 85.140 85.010 85.980 +153 1556 86.720 4542700 85.950 85.880 86.980 +153 1557 87.160 5001400 87.000 86.720 88.100 +153 1558 87.320 5150800 87.950 87.130 88.030 +153 1559 88.040 3984400 87.140 87.100 88.100 +153 1560 87.420 3076900 88.040 87.400 88.100 +153 1561 86.710 4090000 87.430 86.510 87.910 +153 1562 86.630 3016300 86.770 86.270 87.200 +153 1563 86.000 4626600 86.020 85.580 86.200 +153 1564 84.980 6665200 86.260 84.430 86.480 +153 1565 84.780 4233700 84.750 84.300 84.980 +153 1566 84.850 5928500 84.780 84.600 85.250 +153 1567 85.920 7275600 84.300 84.290 86.150 +153 1568 89.370 13824500 88.200 88.000 89.730 +153 1569 88.820 7079800 88.450 88.290 89.190 +153 1570 88.100 6272100 88.400 87.660 88.760 +153 1571 87.390 5998900 88.220 87.290 88.450 +153 1572 88.430 5775000 87.360 87.310 88.900 +153 1573 89.000 7337300 88.330 88.250 89.570 +153 1574 90.000 6035100 88.580 88.500 90.270 +153 1575 89.500 4226500 89.800 89.430 90.240 +153 1576 89.750 4518500 89.400 88.950 89.900 +153 1577 90.110 5160900 89.330 89.230 90.600 +153 1578 90.470 5391100 89.550 89.500 91.220 +153 1579 91.200 6553300 91.250 90.970 91.900 +153 1580 92.380 6952100 91.050 90.820 92.700 +153 1581 93.280 6708500 92.400 92.400 93.520 +153 1582 93.370 4907300 92.500 92.500 93.700 +153 1583 93.370 4513100 93.000 93.000 93.950 +154 1584 94.130 3541200 94.280 94.130 94.640 +154 1585 92.700 5664200 94.140 92.540 94.830 +154 1586 92.760 4838700 92.950 92.300 93.100 +154 1587 93.300 4233000 92.700 92.500 93.970 +154 1588 93.570 2868800 93.160 93.160 94.020 +154 1589 94.330 4299200 93.500 93.480 94.670 +155 1590 93.300 3821900 92.640 92.590 93.430 +155 1591 92.920 4541200 92.750 92.750 93.730 +155 1592 92.410 4037900 93.150 92.200 93.210 +155 1593 92.370 4754000 92.940 92.360 93.180 +155 1594 91.600 8051000 92.350 91.590 92.510 +155 1595 92.130 5087800 91.700 91.700 92.560 +155 1596 92.350 4513900 92.020 92.010 93.000 +155 1597 92.410 3424800 92.350 92.090 92.800 +155 1598 91.510 4494300 92.250 91.200 92.410 +156 1599 91.380 4419100 90.460 90.220 91.410 +156 1600 90.440 5740800 91.490 90.040 91.760 +156 1601 90.320 3737900 90.080 89.773 90.620 +156 1602 89.570 4579300 90.230 89.260 90.330 +156 1603 89.000 7836800 89.050 88.706 89.380 +156 1604 88.440 6376500 89.000 88.100 89.200 +156 1605 87.600 5273400 88.280 87.500 88.460 +156 1606 86.200 8296500 87.350 86.090 87.560 +156 1607 85.750 8450000 85.900 85.170 86.240 +156 1608 84.570 7090800 85.760 84.240 85.972 +156 1609 83.640 10708300 84.630 83.470 85.410 +156 1610 76.700 27932400 79.490 76.330 79.660 +156 1611 76.650 13256100 77.150 76.140 77.750 +156 1612 75.480 9292700 76.980 75.250 77.200 +156 1613 72.010 20389800 75.480 71.850 75.870 +156 1614 74.030 16273600 72.990 72.800 74.100 +156 1615 74.210 10985200 74.050 73.260 74.700 +156 1616 74.610 10223400 75.240 74.050 75.720 +156 1617 75.430 12500200 74.680 74.650 76.980 +156 1618 77.050 11706800 75.690 75.500 77.180 +156 1619 75.910 8638300 77.050 75.650 77.110 +157 1620 83.140 5340900 82.930 82.500 83.780 +157 1621 84.000 4490200 83.140 83.000 84.080 +157 1622 84.190 4712900 83.710 83.560 84.630 +157 1623 84.190 5210600 84.150 83.790 84.690 +157 1624 84.700 4661700 84.200 84.100 84.900 +157 1625 86.080 7788600 84.450 84.440 86.210 +157 1626 86.710 8633600 86.080 85.750 87.600 +157 1627 86.950 13328300 86.330 86.220 87.940 +157 1628 89.820 27876200 90.510 89.730 92.040 +157 1629 89.860 10676400 89.800 89.310 90.980 +157 1630 90.480 7391600 90.040 89.550 90.700 +157 1631 91.560 8862300 90.050 90.010 92.000 +157 1632 91.490 9222500 90.910 90.800 91.950 diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 8fa0a6c47fb..fc48ed2afc5 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -104,6 +104,11 @@ test: publication subscription # ---------- test: select_views portals_p2 foreign_key dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite graph_table +# ---------- +# Row Pattern Recognition tests +# ---------- +test: rpr rpr_base rpr_explain rpr_nfa rpr_integration + # ---------- # Another group of parallel tests (JSON related) # ---------- diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql new file mode 100644 index 00000000000..4796d42d9c2 --- /dev/null +++ b/src/test/regress/sql/rpr.sql @@ -0,0 +1,2236 @@ +-- +-- Test for row pattern recognition: WINDOW clause integration and +-- scenario tests using synthetic stock data. +-- +-- Parser/planner tests: rpr_base.sql +-- NFA engine tests: rpr_nfa.sql +-- EXPLAIN statistics tests: rpr_explain.sql +-- + +\getenv abs_srcdir PG_ABS_SRCDIR + +-- Synthetic stock data for RPR pattern matching tests +CREATE TABLE rpr_stock ( + part_id integer, + rn integer, + price numeric(10,3), + volume bigint, + open numeric(10,3), + low numeric(10,3), + high numeric(10,3) +); + +\set filename :abs_srcdir '/data/stock.data' +COPY rpr_stock FROM :'filename'; +ANALYZE rpr_stock; + +CREATE TEMP TABLE stock (company TEXT, tdate DATE, price INTEGER); +INSERT INTO stock VALUES +('company1', '2023-07-01', 100), ('company1', '2023-07-02', 200), +('company1', '2023-07-03', 150), ('company1', '2023-07-04', 140), +('company1', '2023-07-05', 150), ('company1', '2023-07-06', 90), +('company1', '2023-07-07', 110), ('company1', '2023-07-08', 130), +('company1', '2023-07-09', 120), ('company1', '2023-07-10', 130), +('company2', '2023-07-01', 50), ('company2', '2023-07-02', 2000), +('company2', '2023-07-03', 1500), ('company2', '2023-07-04', 1400), +('company2', '2023-07-05', 1500), ('company2', '2023-07-06', 60), +('company2', '2023-07-07', 1100), ('company2', '2023-07-08', 1300), +('company2', '2023-07-09', 1200), ('company2', '2023-07-10', 1300); + +SELECT * FROM stock; + +-- +-- Basic pattern matching with PREV/NEXT +-- + +-- basic test using PREV +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, + nth_value(tdate, 2) OVER w AS nth_second + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- basic test using PREV. UP appears twice +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, + nth_value(tdate, 2) OVER w AS nth_second + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP+ DOWN+ UP+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- basic test using PREV. Use '*' +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, + nth_value(tdate, 2) OVER w AS nth_second + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP* DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- basic test using PREV. Use '?' +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, + nth_value(tdate, 2) OVER w AS nth_second + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP? DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- test using alternation (|) with sequence +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START (UP | DOWN)) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- test using alternation (|) with group quantifier +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START (UP | DOWN)+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- test using nested alternation +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START ((UP DOWN) | FLAT)+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price), + FLAT AS price = PREV(price) +); + +-- test using group with quantifier +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN ((UP DOWN)+) + DEFINE + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- test using absolute threshold values (not relative PREV) +-- HIGH: price > 150, LOW: price < 100, MID: neutral range +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (LOW MID* HIGH) + DEFINE + LOW AS price < 100, + MID AS price >= 100 AND price <= 150, + HIGH AS price > 150 +); + +-- test threshold-based pattern with alternation +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (LOW (MID | HIGH)+) + DEFINE + LOW AS price < 100, + MID AS price >= 100 AND price <= 150, + HIGH AS price > 150 +); + +-- basic test with fixed-length pattern (A A A = exactly 3) +SELECT company, tdate, price, count(*) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A A A) + DEFINE + A AS price >= 140 AND price <= 150 +); + +-- test using {n} quantifier (A A A should be optimized to A{3}) +SELECT company, tdate, price, count(*) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A{3}) + DEFINE + A AS price >= 140 AND price <= 150 +); + +-- test using {n,} quantifier (2 or more) +SELECT company, tdate, price, count(*) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A{2,}) + DEFINE + A AS price > 100 +); + +-- test using {n,m} quantifier (2 to 4) +SELECT company, tdate, price, count(*) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A{2,4}) + DEFINE + A AS price > 100 +); + +-- test prefix/suffix merge optimization with bounded quantifier +-- Pattern A B (A B){1,2} A B should be optimized to (A B){3,4} +CREATE TEMP TABLE rpr_t (id int, val text); +INSERT INTO rpr_t VALUES + (1,'A'),(2,'B'), + (3,'A'),(4,'B'), + (5,'A'),(6,'B'), + (7,'A'),(8,'B'), + (9,'X'); +SELECT id, val, count(*) OVER w AS match_count +FROM rpr_t +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (A B (A B){1,2} A B) + DEFINE + A AS val = 'A', + B AS val = 'B' +); +DROP TABLE rpr_t; + +-- last_value() should remain consistent +SELECT company, tdate, price, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- omit "START" in DEFINE but it is ok because "START AS TRUE" is +-- implicitly defined. per spec. +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w, + nth_value(tdate, 2) OVER w AS nth_second + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- the first row start with less than or equal to 100 +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (LOWPRICE UP+ DOWN+) + DEFINE + LOWPRICE AS price <= 100, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- second row raises 120% +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (LOWPRICE UP+ DOWN+) + DEFINE + LOWPRICE AS price <= 100, + UP AS price > PREV(price) * 1.2, + DOWN AS price < PREV(price) +); + +-- using NEXT +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UPDOWN) + DEFINE + START AS TRUE, + UPDOWN AS price > PREV(price) AND price > NEXT(price) +); + +-- using AFTER MATCH SKIP TO NEXT ROW (same pattern as above; +-- match length is always 2, so result is identical to SKIP PAST LAST ROW. +-- SKIP TO NEXT ROW's distinct effect is tested in backtracking section.) +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (START UPDOWN) + DEFINE + START AS TRUE, + UPDOWN AS price > PREV(price) AND price > NEXT(price) +); + +-- PREV returns NULL at partition's first row (null_slot path) +SELECT company, tdate, price, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (BOUNDARY REST+) + DEFINE + BOUNDARY AS PREV(price) IS NULL, + REST AS PREV(price) IS NOT NULL +); + +-- NEXT returns NULL at partition's last row (null_slot path) +SELECT company, tdate, price, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ BOUNDARY) + DEFINE + A AS NEXT(price) IS NOT NULL, + BOUNDARY AS NEXT(price) IS NULL +); + +-- DESC order: PREV refers to the row with later date +SELECT company, tdate, price, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate DESC + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (START DOWN+ UP+) + DEFINE + START AS TRUE, + DOWN AS price < PREV(price), + UP AS price > PREV(price) +); + +-- Multiple partitions with unequal sizes +WITH multi_part AS ( + SELECT * FROM (VALUES + ('a', 1, 10), ('a', 2, 20), ('a', 3, 15), + ('b', 1, 5), + ('c', 1, 100), ('c', 2, 200), ('c', 3, 150), ('c', 4, 140), ('c', 5, 300) + ) AS t(grp, id, val) +) +SELECT grp, id, val, count(*) OVER w +FROM multi_part +WINDOW w AS ( + PARTITION BY grp + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS val <= NEXT(val), + B AS val > PREV(val) OR val < PREV(val) +); + +-- FLOAT/NUMERIC DEFINE conditions +WITH float_data AS ( + SELECT * FROM (VALUES + (1, 1.0::float8), (2, 1.5), (3, 1.4999), (4, 1.50001), (5, 0.1) + ) AS t(id, val) +) +SELECT id, val, count(*) OVER w +FROM float_data +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS val > PREV(val) * 0.99 +); + +-- +-- Error cases: PREV/NEXT usage restrictions +-- + +-- Nested PREV +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > PREV(PREV(price)) +); + +-- Nested NEXT +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > NEXT(NEXT(price)) +); + +-- PREV nested inside NEXT +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > NEXT(PREV(price)) +); + +-- PREV nested inside expression inside NEXT +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > NEXT(price * PREV(price)) +); + +-- Triple nesting: error reported at outermost PREV +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > PREV(PREV(PREV(price))) +); + +-- No column reference in PREV/NEXT argument +-- PREV(1): constant only, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(1) > 0 +); + +-- NEXT(1 + 2): constant expression, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS NEXT(1 + 2) > 0 +); + +-- 2-arg form: PREV(1, 1): constant expression as first arg +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(1, 1) > 0 +); + +-- Compound navigation without a column reference must be rejected too, +-- consistent with the simple forms above. +-- PREV(FIRST(1)): compound, constant only, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(FIRST(1)) > 0 +); + +-- NEXT(LAST(1 + 2)): compound, constant expression, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS NEXT(LAST(1 + 2)) > 0 +); + +-- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(FIRST(1, 2)) > 0 +); + +-- PREV(FIRST(1), 2): compound, outer offset only, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(FIRST(1), 2) > 0 +); + +-- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(FIRST(1, 2), 3) > 0 +); + +-- Non-constant offset: column reference as offset +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(price, price) > 0 +); + +-- Non-constant offset: column reference in compound inner offset +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(LAST(price, price), 2) > 0 +); + +-- Non-constant offset: column reference in compound outer offset +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(LAST(price, 1), price) > 0 +); + +-- Non-constant offset: volatile function as offset +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(price, random()::int) > 0 +); + +-- Non-constant offset: subquery as offset +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(price, (SELECT 1)) > 0 +); + +-- First arg: subquery (caught by DEFINE-level subquery restriction) +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS PREV(price + (SELECT 1)) > 0 +); + +-- Volatile function inside nav.arg is rejected in the planner +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(price + random() * 0) >= 0 +); + +-- nextval is volatile (per pg_proc), so it is rejected via the FuncExpr +-- path with the "volatile functions" message +CREATE SEQUENCE rpr_seq; +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > nextval('rpr_seq') +); +DROP SEQUENCE rpr_seq; + +-- A volatile DEFINE is now rejected in the planner, not at parse time, so a +-- view that hides one is created successfully and only errors when read. +CREATE TEMP VIEW rpr_volatile_view AS +SELECT company, tdate, price, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A+) + DEFINE A AS price > random() * 0 +); +SELECT * FROM rpr_volatile_view; +DROP VIEW rpr_volatile_view; + +-- DEFINE cannot reference an outer query's column. A correlated outer +-- reference must produce a clean error, not the internal "Upper-level Var" +-- elog that pull_var_clause would otherwise raise. +-- Qualified outer reference (o.threshold): +SELECT * FROM (VALUES (95)) AS o(threshold), +LATERAL ( + SELECT price FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > o.threshold + ) +) s; +-- Unqualified name resolving to the outer column (threshold): +SELECT * FROM (VALUES (95)) AS o(threshold), +LATERAL ( + SELECT price FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS price > threshold + ) +) s; +-- Outer reference inside a navigation argument is rejected too: +SELECT * FROM (VALUES (95)) AS o(threshold), +LATERAL ( + SELECT price FROM stock + WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(o.threshold, 1) > 0 + ) +) s; + +-- DEFINE rejects a schema-qualified column reference (three or more name +-- parts) once it resolves; the qualified form itself is not allowed. (stock +-- is a temp table, so it is qualified with pg_temp here.) +-- 3-part (schema.table.column): +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS pg_temp.stock.price > 0 +); +-- whole-row variant (schema.table.*): +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS (pg_temp.stock.*) IS NOT NULL +); +-- A two-part table-qualified whole-row reference is rejected as well, through +-- a separate range-variable check (a bare relation name is instead accepted +-- as a whole-row Var). +-- 2-part (table.*): +SELECT price FROM stock +WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE A AS (stock.*) IS NOT NULL +); + +-- +-- 2-arg PREV/NEXT: functional tests +-- + +-- PREV(price, 2): match rows where current price > price 2 rows back +-- stock: 100, 90, 80, 95, 110 +-- Pattern (A B+): A=any, B where price > PREV(price, 2) +-- At pos 2 (80): A matches. pos 3 (95): 95 > PREV(95,2)=90 TRUE. +-- pos 4 (110): 110 > PREV(110,2)=80 TRUE. Match! +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS price > PREV(price, 2) +); + +-- NEXT(price, 2): match rows where current price > price 2 rows ahead +-- pos 0 (100): NEXT(100,2)=80, 100>80 TRUE. pos 1 (90): NEXT(90,2)=95, 90>95 FALSE. Match ends. +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price > NEXT(price, 2) +); + +-- Expressions inside PREV/NEXT arg: expr is evaluated on target row +-- PREV(price - 50, 1): fetches (price - 50) from 1 row back +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price > PREV(price - 50, 1) +); + +-- NEXT(price * 2, 1): fetches (price * 2) from 1 row ahead +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price < NEXT(price * 2, 1) +); + +-- Large offset: PREV(val, 999) on 1000-row series matches only last row +-- NEXT(val, 999) matches only first row +SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w +FROM generate_series(1, 1000) AS t(val) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(val, 999) = 1 +) +ORDER BY val DESC LIMIT 3; + +SELECT val, first_value(val) OVER w, last_value(val) OVER w, count(*) OVER w +FROM generate_series(1, 1000) AS t(val) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(val, 999) = 1000 +) +LIMIT 3; + +-- PREV(price, 0): offset 0 means current row, always equal to price +-- A+ matches entire partition as one group; count = partition size +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(price, 0) = price +); + +-- 2-arg PREV/NEXT: negative offset +SELECT company, tdate, price, first_value(price) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(price, -1) IS NOT NULL +); + +-- 2-arg PREV/NEXT: NULL offset (typed) +SELECT company, tdate, price, first_value(price) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(price, NULL::int8) IS NOT NULL +); + +-- 2-arg PREV/NEXT: NULL offset (untyped) +SELECT company, tdate, price, first_value(price) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(price, NULL) IS NOT NULL +); + +-- 2-arg PREV/NEXT: host variable negative and NULL +PREPARE test_prev_offset(int8) AS +SELECT company, tdate, price, first_value(price) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price > PREV(price, $1) +); +EXECUTE test_prev_offset(-1); +EXECUTE test_prev_offset(NULL); +DEALLOCATE test_prev_offset; + +-- 2-arg PREV/NEXT: host variable with expression (0 + $1) +PREPARE test_prev_offset(int8) AS +SELECT company, tdate, price, first_value(price) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS price > PREV(price, 0 + $1) +); +EXECUTE test_prev_offset(-1); +EXECUTE test_prev_offset(NULL); +DEALLOCATE test_prev_offset; + +-- 2-arg PREV/NEXT: host variable with positive value +-- Exercises RPR_NAV_OFFSET_NEEDS_EVAL -> eval_nav_max_offset() path +PREPARE test_prev_offset(int8) AS +SELECT company, tdate, price, first_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS price > PREV(price, $1) +); +EXECUTE test_prev_offset(1); +EXECUTE test_prev_offset(2); +DEALLOCATE test_prev_offset; + +-- 2-arg: two PREV with different offsets in same DEFINE clause +-- B: price exceeds both 1-back and 2-back values +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS price > PREV(price, 1) AND price > PREV(price, 2) +); + +-- 2-arg: PREV and NEXT with explicit offsets in same DEFINE clause +-- A: price exceeds 1-back and is below 1-ahead (ascending interior point) +SELECT company, tdate, price, + first_value(price) OVER w, last_value(price) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS price > PREV(price, 1) AND price < NEXT(price, 1) +); + +-- Pass-by-ref types: two PREV calls targeting different positions. +-- Verifies that datumCopy in RESTORE prevents dangling pointers when +-- nav_slot is re-fetched for the second navigation. +-- tdate::text gives distinct text values per row (e.g. '07-01-2023'). +-- B matches when 1-back date text > 2-back date text (always true for +-- ascending dates), so B+ extends the full partition after A. +SELECT company, tdate, tdate::text AS tdate_text, + first_value(tdate::text) OVER w, last_value(tdate::text) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS PREV(tdate::text, 1) > PREV(tdate::text, 2) +); + +-- numeric: PREV(price::numeric, 1) > PREV(price::numeric, 2) +-- B matches when price 1-back > price 2-back (ascending pair). +SELECT company, tdate, price::numeric AS nprice, + first_value(price::numeric) OVER w, last_value(price::numeric) OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS PREV(price::numeric, 1) > PREV(price::numeric, 2) +); + +-- Typmod coercion over a navigation result: casting PREV(p) (a numeric(10,3) +-- column) to a narrower numeric(8,2) inside DEFINE forces coerce_type_typmod, +-- which calls exprTypmod() on the RPRNavExpr. +CREATE TEMP TABLE rpr_typmod (id int, p numeric(10,3)); +INSERT INTO rpr_typmod VALUES (1, 1.5), (2, 2.5), (3, 3.5); +SELECT id, count(*) OVER w AS cnt +FROM rpr_typmod +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS CAST(PREV(p) AS numeric(8,2)) > 0 +); +DROP TABLE rpr_typmod; + +-- +-- FIRST/LAST navigation +-- + +-- Test data for FIRST/LAST: values cycle back so FIRST(val) = LAST(val) +-- at specific positions. +CREATE TEMP TABLE rpr_nav (id int, val int); +INSERT INTO rpr_nav VALUES (1,10),(2,20),(3,30),(4,10),(5,50),(6,10); + +-- FIRST(val) = constant: B matches when match_start has val=10 +-- match_start=1(10): A=id1, B=id2, FIRST(val)=10 -> match {1,2} +-- match_start=3(30): A=id3, B=id4, FIRST(val)=30!=10 -> no match +-- match_start=4(10): A=id4, B=id5, FIRST(val)=10 -> match {4,5} +SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS TRUE, B AS FIRST(val) = 10 +); + +-- LAST(val): always equals current row's val (offset 0 default) +-- Equivalent to: B AS val > 15 +SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS TRUE, B AS LAST(val) > 15 +); + +-- Reluctant A+? with FIRST(val) = LAST(val): find shortest match where +-- first and last rows have the same val. +-- match_start=1(10): reluctant tries B early: +-- id2(20!=10), id3(30!=10), id4(10=10) -> match {1,2,3,4} +-- match_start=5(50): id6(10!=50) -> no match +SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+? B) + DEFINE A AS TRUE, B AS FIRST(val) = LAST(val) +); + +-- Greedy A+ with FIRST(val) = LAST(val): find longest match where +-- first and last rows have the same val. +-- match_start=1(10): greedy A eats all, B tries last: +-- id6(10=10) -> match {1,2,3,4,5,6} +SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS TRUE, B AS FIRST(val) = LAST(val) +); + +-- SKIP TO NEXT ROW with FIRST(val) = LAST(val): overlapping match attempts. +-- With ONE ROW PER MATCH, each row shows only its first match result. +SELECT id, val, first_value(id) OVER w AS mf, last_value(id) OVER w AS ml +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+? B) + DEFINE A AS TRUE, B AS FIRST(val) = LAST(val) +); + +-- FIRST/LAST 2-arg offset form +-- +-- FIRST(val, 0) = FIRST(val): match_start row +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS FIRST(val, 0) = 10 +); + +-- FIRST(val, 1): match_start + 1 row (second row of match) +-- match_start=1(10): FIRST(val,1)=20, B needs val=20 -> id2(20) match, id3(30) no +-- match_start=3(30): FIRST(val,1)=10, B needs val=10 -> id4(10) match +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS val = FIRST(val, 1) +); + +-- FIRST(val, 99): offset beyond match range -> NULL, no match +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS FIRST(val, 99) IS NOT NULL +); + +-- LAST(val, 0) = LAST(val): current row +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS LAST(val, 0) > 15 +); + +-- LAST(val, 1): one row back from current (previous match row) +-- At B evaluation on id2: LAST(val,1) = val at id1 = 10 +-- B matches when previous row val < 30 +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS LAST(val, 1) < 30 +); + +-- LAST(val, 99): offset before match_start -> NULL +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS LAST(val, 99) IS NOT NULL +); + +-- Error: NULL offset +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS FIRST(val, NULL::int8) IS NULL +); + +-- Error: negative offset +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS LAST(val, -1) IS NULL +); + +-- Functional notation: should access column, not RPR navigation +CREATE TEMP TABLE rpr_names (prev int, next int, first text, last text); +INSERT INTO rpr_names VALUES (1, 2, 'Joe', 'Blow'); +SELECT prev(f), next(f), first(f), last(f) FROM rpr_names f; +DROP TABLE rpr_names; + +-- Compound navigation: PREV(FIRST(val), M) +-- rpr_nav: (1,10),(2,20),(3,30),(4,10),(5,50),(6,10) +-- PREV(FIRST(val), 1): target = match_start + 0 - 1 = match_start - 1 +-- At match_start=1: target=0 -> out of range -> NULL +-- At match_start=3: target=2(val=20) -> 20 > 0 -> true +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0 +); + +-- NEXT(FIRST(val, 1), 1): target = match_start + 1 + 1 = match_start + 2 +-- At match_start=1, B on id2: target=1+1+1=3(val=30), 30>0 -> true +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(FIRST(val, 1), 1) > 0 +); + +-- PREV(LAST(val), 2): LAST(val) is the current row (inner offset 0), so +-- target = currentpos - 0 - 2 = currentpos - 2. Same backward reach as PREV(val, 2). +-- At currentpos=2 (start id=1): target=0 -> out of range -> NULL -> B fails. +-- At currentpos=3 (start id=2): target=1(val=10) -> in range -> B runs on id3..id6, +-- so the match is id2..id6. +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(LAST(val), 2) IS NOT NULL +); + +-- NEXT(LAST(val, 1), 2): LAST(val, 1) is one row back (inner offset 1), then +-- NEXT adds 2, so target = currentpos - 1 + 2 = currentpos + 1. Looks one row +-- ahead: same as NEXT(val, 1). +-- At currentpos=2 (start id=1): target=3(val=30) -> in range -> B true. +-- B stays true through id5 (target=6); at id6 target=7 -> out of range -> NULL, +-- so the match is id1..id5. +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(LAST(val, 1), 2) IS NOT NULL +); + +-- Compound: outer offset beyond partition (PREV far back) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val), 99) IS NOT NULL +); + +-- Compound: outer offset beyond partition (NEXT far forward) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(FIRST(val), 99) IS NOT NULL +); + +-- Compound: inner offset beyond match range (FIRST offset too large) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val, 99), 1) IS NOT NULL +); + +-- Compound: inner offset beyond match range (LAST offset too large) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(LAST(val, 99), 1) IS NOT NULL +); + +-- Compound: NULL outer offset (runtime error) +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(FIRST(val), NULL::int8) IS NULL +); + +-- Compound: negative outer offset (runtime error) +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(LAST(val), -1) IS NULL +); + +-- Outer offset overflows int64: target position out of range -> NULL. +-- Plain NEXT(val, INT64_MAX): currentpos + INT64_MAX overflows. +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(val, 9223372036854775807) IS NULL +); + +-- Compound NEXT(FIRST()): outer offset overflow. Inner offset 1 forces +-- inner_pos >= 1, so inner_pos + INT64_MAX overflows at every match. +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(FIRST(val, 1), 9223372036854775807) IS NULL +); + +-- Compound NEXT(LAST()): outer offset overflow. +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(LAST(val), 9223372036854775807) IS NULL +); + +-- Compound: default offsets on both sides +-- PREV(FIRST(val)): inner=0 (match_start), outer=1 -> target = match_start - 1 +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val)) IS NOT NULL +); + +-- NEXT(LAST(val)): inner=0 (currentpos), outer=1 -> target = currentpos + 1 +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(LAST(val)) IS NOT NULL +); + +-- Compound: inner NULL offset (runtime error) +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(FIRST(val, NULL::int8), 1) IS NULL +); + +-- Compound: inner negative offset (runtime error) +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(LAST(val, -1), 1) IS NULL +); + +-- Offset argument whose type has no implicit cast to bigint (parse error) +SELECT id, val, count(*) OVER w FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS val > PREV(val, 1.5) +); + +-- Compound + host variable offsets +PREPARE test_compound_offset(int8, int8) AS +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val, $1), $2) IS NOT NULL +); +EXECUTE test_compound_offset(0, 1); +EXECUTE test_compound_offset(1, 1); +DEALLOCATE test_compound_offset; + +-- Compound + SKIP TO NEXT ROW: overlapping matches with PREV(FIRST()) +SELECT id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val), 1) > 0 +); + +-- Compound + multiple partitions +CREATE TEMP TABLE rpr_nav_part (gid int, id int, val int); +INSERT INTO rpr_nav_part VALUES + (1,1,10),(1,2,20),(1,3,30), + (2,1,40),(2,2,50),(2,3,60); +SELECT gid, id, val, first_value(id) OVER w AS mf, count(*) OVER w AS cnt +FROM rpr_nav_part WINDOW w AS ( + PARTITION BY gid ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(FIRST(val), 1) > 0 +); +DROP TABLE rpr_nav_part; + +-- Reverse nesting: FIRST wrapping PREV is prohibited +SELECT id, val FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B) + DEFINE A AS TRUE, B AS FIRST(PREV(val)) > 0 +); + +-- Reverse nesting: LAST wrapping NEXT is prohibited +SELECT id, val FROM rpr_nav WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B) + DEFINE A AS TRUE, B AS LAST(NEXT(val)) > 0 +); + +DROP TABLE rpr_nav; + +-- +-- SKIP TO / Backtracking / Frame boundary +-- + +-- match everything +SELECT company, tdate, price, first_value(price) OVER w, last_value(price) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (A+) + DEFINE + A AS TRUE +); + +-- nth_value beyond reduced frame (no IGNORE NULLS) +-- Tests WinGetSlotInFrame/WinGetFuncArgInFrame out-of-frame with RPR +SELECT company, tdate, price, + nth_value(price, 5) OVER w AS nth_5 +FROM stock +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- backtracking with reclassification of rows +-- using AFTER MATCH SKIP PAST LAST ROW +SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (A+ B+) + DEFINE + A AS price > 100, + B AS price > 100 +); + +-- backtracking with reclassification of rows +-- using AFTER MATCH SKIP TO NEXT ROW +SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (A+ B+) + DEFINE + A AS price > 100, + B AS price > 100 +); + +-- SKIP TO NEXT ROW with limited frame (Ishii-san's test case) +-- Each row should produce its own match within its frame +WITH data AS ( + SELECT * FROM (VALUES + ('A', 1), ('A', 2), + ('B', 3), ('B', 4) + ) AS t(gid, id) +) +SELECT gid, id, array_agg(id) OVER w +FROM data +WINDOW w AS ( + PARTITION BY gid + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS id < 10 +); + +-- Limited frame with absorption test +-- Row 0: frame [0,2], can't see B at row 3 -> no match +-- Row 1: frame [1,3], can see A A B -> should match rows 1-3 +WITH frame_absorb_test AS ( + SELECT * FROM (VALUES + (0, 'A'), (1, 'A'), (2, 'A'), (3, 'B') + ) AS t(id, flag) +) +SELECT id, flag, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM frame_absorb_test +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS flag = 'A', + B AS flag = 'B' +); + +-- ROWS BETWEEN CURRENT ROW AND offset FOLLOWING +SELECT company, tdate, price, first_value(tdate) OVER w, last_value(tdate) OVER w, + count(*) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- +-- Aggregates +-- + +-- using AFTER MATCH SKIP PAST LAST ROW +SELECT company, tdate, price, + first_value(price) OVER w, + last_value(price) OVER w, + max(price) OVER w, + min(price) OVER w, + sum(price) OVER w, + avg(price) OVER w, + count(price) OVER w +FROM stock +WINDOW w AS ( +PARTITION BY company +ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +AFTER MATCH SKIP PAST LAST ROW +INITIAL +PATTERN (START UP+ DOWN+) +DEFINE +START AS TRUE, +UP AS price > PREV(price), +DOWN AS price < PREV(price) +); + +-- using AFTER MATCH SKIP TO NEXT ROW +SELECT company, tdate, price, + first_value(price) OVER w, + last_value(price) OVER w, + max(price) OVER w, + min(price) OVER w, + sum(price) OVER w, + avg(price) OVER w, + count(price) OVER w +FROM stock +WINDOW w AS ( +PARTITION BY company +ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +AFTER MATCH SKIP TO NEXT ROW +INITIAL +PATTERN (START UP+ DOWN+) +DEFINE +START AS TRUE, +UP AS price > PREV(price), +DOWN AS price < PREV(price) +); + +-- row_number() within RPR reduced frame +SELECT company, tdate, price, row_number() OVER w, count(*) OVER w +FROM stock +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- +-- SQL Integration: JOIN, CTE, LATERAL +-- + +-- JOIN case +CREATE TEMP TABLE t1 (i int, v1 int); +CREATE TEMP TABLE t2 (j int, v2 int); +INSERT INTO t1 VALUES(1,10); +INSERT INTO t1 VALUES(1,11); +INSERT INTO t1 VALUES(1,12); +INSERT INTO t2 VALUES(2,10); +INSERT INTO t2 VALUES(2,11); +INSERT INTO t2 VALUES(2,12); + +SELECT * FROM t1, t2 WHERE t1.v1 <= 11 AND t2.v2 <= 11; + +SELECT *, count(*) OVER w FROM t1, t2 +WINDOW w AS ( + PARTITION BY t1.i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A) + DEFINE + A AS v1 <= 11 AND v2 <= 11 +); + +-- WITH case +WITH wstock AS ( + SELECT * FROM stock WHERE tdate < '2023-07-08' +) +SELECT tdate, price, +first_value(tdate) OVER w, +count(*) OVER w + FROM wstock + WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- ReScan test: LATERAL join forces WindowAgg rescan with RPR +-- Tests ExecReScanWindowAgg clearing nav_slot +SELECT g.x, sub.* +FROM generate_series(1, 2) g(x), +LATERAL ( + SELECT id, price, count(*) OVER w AS c + FROM (VALUES (1, 100), (2, 200), (3, 150)) AS t(id, price) + WHERE id <= g.x + 1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (START UP+) + DEFINE + START AS TRUE, + UP AS price > PREV(price) + ) +) sub +ORDER BY g.x, sub.id; + +-- PREV has multiple column reference +CREATE TEMP TABLE rpr1 (id INTEGER, i SERIAL, j INTEGER); +INSERT INTO rpr1(id, j) SELECT 1, g*2 FROM generate_series(1, 10) AS g; +SELECT id, i, j, count(*) OVER w + FROM rpr1 + WINDOW w AS ( + PARTITION BY id + ORDER BY i + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (START COND+) + DEFINE + START AS TRUE, + COND AS PREV(i + j + 1) < 10 +); + +-- +-- Large-scale / scalability tests +-- + +-- Smoke test for larger partitions. +WITH s AS ( + SELECT v, count(*) OVER w AS c + FROM (SELECT generate_series(1, 5000) v) + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ( r+ ) + DEFINE r AS TRUE + ) +) +-- Should be exactly one long match across all rows. +SELECT * FROM s WHERE c > 0; + +WITH s AS ( + SELECT v, count(*) OVER w AS c + FROM (SELECT generate_series(1, 5000) v) + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ( r ) + DEFINE r AS TRUE + ) +) +-- Every row should be its own match. +SELECT count(*) FROM s WHERE c > 0; + +-- Large partition test: 100K rows with A+ B* C{10000,} pattern +-- Tests that int32 count doesn't overflow with large repetitions +WITH data AS ( + SELECT generate_series(0, 100000) AS v +), +result AS ( + SELECT v, + count(*) OVER w AS match_len, + first_value(v) OVER w AS match_first, + last_value(v) OVER w AS match_last + FROM data + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (A+ B* C{10000,}) + DEFINE + A AS v < 33333, + B AS v >= 33333 AND v < 66666, + C AS v >= 66666 AND v < 99999 + ) +) +-- Should match: A (33333 rows) + B (33333 rows) + C (33333 rows) = 99999 rows +SELECT match_first, match_last, match_len FROM result WHERE match_len > 0; + +-- JIT PREV/NEXT navigation test: 100K rows with PREV in DEFINE. +-- Exercises EEOP_RPR_NAV_SET/RESTORE JIT code paths (has_rpr_nav reload) +-- at scale. V-shape: price rises then falls, repeated across partition. +SET jit = on; +SET jit_above_cost = 0; +WITH data AS ( + SELECT i, abs(50000 - i) AS price + FROM generate_series(1, 100000) i +), +result AS ( + SELECT i, price, + count(*) OVER w AS match_len, + first_value(price) OVER w AS match_first + FROM data + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (DOWN+ UP+) + DEFINE + DOWN AS price < PREV(price), + UP AS price > PREV(price) + ) +) +SELECT count(*) AS matched_rows, max(match_len) AS longest_match +FROM result WHERE match_len > 0; +RESET jit_above_cost; +RESET jit; + +-- JIT compound navigation test +SET jit = on; +SET jit_above_cost = 0; +SELECT count(*) AS matched_rows +FROM ( + SELECT v, count(*) OVER w AS match_len + FROM generate_series(1, 1000) AS t(v) + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(v), 1) > 0 + ) +) sub WHERE match_len > 0; +RESET jit_above_cost; +RESET jit; + +-- +-- IGNORE NULLS +-- + +-- no NULL rows case. The result should be identical with "basic test using PREV" +SELECT company, tdate, price, first_value(price) IGNORE NULLS OVER w, + last_value(price) IGNORE NULLS OVER w, + nth_value(tdate, 2) IGNORE NULLS OVER w AS nth_second + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- nth_value with IGNORE NULLS option wants to find the second row but +-- due to a NULL in the middle, it returns the third row. +WITH data AS ( + SELECT * FROM (VALUES + (10, 1), (11, NULL), (12, 3), (13, 4) + ) AS t(gid, id)) + SELECT gid, id, nth_value(id, 2) IGNORE NULLS OVER w AS second_val, + array_agg(id) OVER w + FROM data + WINDOW w AS ( + ORDER BY gid + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS gid < 13 + ); + +-- nth_value with IGNORE NULLS option wants to find the third row but +-- due to a NULL in the middle, it reaches the end of reduced frame and +-- returns NULL +WITH data AS ( + SELECT * FROM (VALUES + (10, 1), (11, NULL), (12, 3), (13, 4) + ) AS t(gid, id)) + SELECT gid, id, nth_value(id, 3) IGNORE NULLS OVER w AS thrid_val, + array_agg(id) OVER w + FROM data + WINDOW w AS ( + ORDER BY gid + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS gid < 13 + ); + +-- nth_value beyond reduced frame with IGNORE NULLS +-- Tests ignorenulls_getfuncarginframe early out-of-frame check +SELECT company, tdate, price, + nth_value(price, 5) IGNORE NULLS OVER w AS nth_5_in +FROM stock +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- IGNORE NULLS + first_value where first value in reduced frame is NULL +WITH data AS ( + SELECT * FROM (VALUES + (1, NULL), (2, NULL), (3, 30), (4, 40) + ) AS t(id, val)) +SELECT id, val, + first_value(val) IGNORE NULLS OVER w AS fv_ignull, + count(*) OVER w +FROM data +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS TRUE +); + +-- IGNORE NULLS + all values NULL in reduced frame +WITH data AS ( + SELECT * FROM (VALUES + (1, NULL), (2, NULL), (3, NULL) + ) AS t(id, val)) +SELECT id, val, + first_value(val) IGNORE NULLS OVER w AS fv_ignull, + last_value(val) IGNORE NULLS OVER w AS lv_ignull, + count(*) OVER w +FROM data +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS TRUE +); + +-- +-- last_value IGNORE NULLS with reduced frame containing all NULLs +-- Exercises ignorenulls_getfuncarginframe SEEK_TAIL out-of-frame path +-- when notnull_relpos >= num_reduced_frame. +-- +CREATE TEMP TABLE rpr_nullval (id INT, val INT); +INSERT INTO rpr_nullval VALUES (1, 10), (2, NULL), (3, NULL), (4, 20); + +SELECT id, val, + last_value(val) IGNORE NULLS OVER w AS lv_ignull, + count(*) OVER w AS cnt +FROM rpr_nullval +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS val IS NOT NULL, + B AS val IS NULL +); + +-- +-- nth_value with a NULL offset +-- + +CREATE TABLE rpr_dormant (id int, price int); +INSERT INTO rpr_dormant SELECT g, g*10 FROM generate_series(1,60) g; + +-- reference: first_value(id) is the start row of the match beginning at the +-- current row, count(*) is that match's length over the reduced frame +SELECT * FROM ( + SELECT id, first_value(id) OVER w AS match_start, count(*) OVER w AS match_len + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50)) +) s WHERE id > 50 ORDER BY id; + +-- nth_value with a NULL offset; FIRST navigation in DEFINE, SKIP PAST LAST ROW +SELECT * FROM ( + SELECT id, nv FROM ( + SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50)) + ) s +) t WHERE id > 50 ORDER BY id; + +-- the same window with first_value and count alongside nth_value +SELECT * FROM ( + SELECT id, nv, fv, cnt FROM ( + SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv, + first_value(id) OVER w AS fv, count(*) OVER w AS cnt + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50)) + ) s +) t WHERE id > 50 ORDER BY id; + +-- the same nth_value with a non-navigation DEFINE +SELECT * FROM ( + SELECT id, nv FROM ( + SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > 0) + ) s +) t WHERE id > 50 ORDER BY id; + +-- the same nth_value with a PREV-only DEFINE (no FIRST navigation) +SELECT * FROM ( + SELECT id, nv FROM ( + SELECT id, nth_value(price, CASE WHEN id < 50 THEN NULL ELSE 1 END) OVER w AS nv + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > PREV(price, 50)) + ) s +) t WHERE id > 50 ORDER BY id; + +-- nth_value with a NULL offset band in the middle of the partition +SELECT * FROM ( + SELECT id, nv FROM ( + SELECT id, nth_value(price, CASE WHEN id BETWEEN 20 AND 40 THEN NULL ELSE 1 END) OVER w AS nv + FROM rpr_dormant + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) DEFINE A AS price > PREV(FIRST(price), 50)) + ) s +) t WHERE id BETWEEN 38 AND 46 ORDER BY id; + +DROP TABLE rpr_dormant; + +-- +-- NULL handling +-- + +CREATE TEMP TABLE stock_null (company TEXT, tdate DATE, price INTEGER); +INSERT INTO stock_null VALUES ('c1', '2023-07-01', 100); +INSERT INTO stock_null VALUES ('c1', '2023-07-02', NULL); -- NULL in middle +INSERT INTO stock_null VALUES ('c1', '2023-07-03', 200); +INSERT INTO stock_null VALUES ('c1', '2023-07-04', 150); + +SELECT company, tdate, price, count(*) OVER w AS match_count +FROM stock_null +WINDOW w AS ( + PARTITION BY company + ORDER BY tdate + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (START UP DOWN) + DEFINE START AS TRUE, UP AS price > PREV(price), DOWN AS price < +PREV(price) +); + +-- Consecutive NULLs: PREV navigates through NULL values +CREATE TEMP TABLE rpr_consec_null (id INT, val INT); +INSERT INTO rpr_consec_null VALUES + (1, 100), (2, NULL), (3, NULL), (4, NULL), (5, 200), (6, 300); + +-- PREV(val) IS NULL succeeds for both null_slot (first row) and actual NULL +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_consec_null +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+ C) + DEFINE + A AS val IS NULL, + B AS val IS NULL AND PREV(val) IS NULL, + C AS val IS NOT NULL +); + +-- NEXT(val) through consecutive NULLs +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_consec_null +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+ C) + DEFINE + A AS val IS NOT NULL, + B AS val IS NULL AND NEXT(val) IS NULL, + C AS val IS NULL AND NEXT(val) IS NOT NULL +); + +DROP TABLE rpr_consec_null; + +-- ============================================================ +-- Stock Scenario Tests (1632 rows, partitioned regions) +-- ============================================================ + +-- Consecutive rising days: find streaks of 7+ days +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (UP{7,}) + DEFINE UP AS price > PREV(price) + ) +) t WHERE days > 0 ORDER BY start_rn; + +-- V-shape recovery: 4+ days decline followed by 4+ days rise +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (DECLINE{4,} RISE{4,}) + DEFINE + DECLINE AS price < PREV(price), + RISE AS price > PREV(price) + ) +) t WHERE days > 0 ORDER BY start_rn; + +-- W-bottom: decline, bounce, re-decline, recovery +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (DECLINE{3,} BOUNCE{3,} DIP{3,} RECOVER{3,}) + DEFINE + DECLINE AS price < PREV(price), + BOUNCE AS price > PREV(price), + DIP AS price < PREV(price), + RECOVER AS price > PREV(price) + ) +) t WHERE days > 0 ORDER BY start_rn; + +-- Volume surge streak: 6+ consecutive days of increasing volume +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(volume) OVER w AS start_vol, + last_value(volume) OVER w AS end_vol, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (INIT SURGE{5,}) + DEFINE + SURGE AS volume > PREV(volume) + ) +) t WHERE days > 0 ORDER BY start_rn; + +-- Volatility squeeze: consecutive narrowing of daily price range +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(high - low) OVER w AS start_range, + last_value(high - low) OVER w AS end_range, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (INIT NARROW{5,}) + DEFINE + NARROW AS (high - low) < PREV(high) - PREV(low) + ) +) t WHERE days > 0 ORDER BY start_rn; + +-- Gap up: open significantly higher than previous close (5%+) +SELECT * FROM ( + SELECT first_value(rn) OVER w AS gap_rn, + first_value(price) OVER w AS prev_close, + last_value(open) OVER w AS gap_open, + count(*) OVER w AS cnt + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (PREV_DAY GAP_UP) + DEFINE + GAP_UP AS open > PREV(price) * 1.05 + ) +) t WHERE cnt > 0 ORDER BY gap_rn; + +-- Price-volume divergence: price rising while volume declining (bearish signal) +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (INIT DIVERGE{3,}) + DEFINE + DIVERGE AS price > PREV(price) AND volume < PREV(volume) + ) +) t WHERE days > 0 ORDER BY start_rn; + +-- Consolidation then breakout: sideways movement followed by sharp rise +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (FLAT{5,} BREAKOUT) + DEFINE + FLAT AS price BETWEEN PREV(price) * 0.98 AND PREV(price) * 1.02, + BREAKOUT AS price > PREV(price) * 1.05 + ) +) t WHERE days > 0 ORDER BY start_rn; + +-- Dead cat bounce: decline followed by weak recovery (<1% per day) +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (DECLINE{4,} BOUNCE{3,}) + DEFINE + DECLINE AS price < PREV(price), + BOUNCE AS price > PREV(price) AND price < PREV(price) * 1.01 + ) +) t WHERE days > 0 ORDER BY start_rn; + +-- Uptrend: 7+ consecutive days of higher highs AND higher lows +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (UPTREND{7,}) + DEFINE + UPTREND AS high > PREV(high) AND low > PREV(low) + ) +) t WHERE days > 0 ORDER BY start_rn; + +-- Panic and snap-back: 3%+ daily drops followed by 2%+ rebound +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (PANIC{2,} SNAP) + DEFINE + PANIC AS price < PREV(price) * 0.97, + SNAP AS price > PREV(price) * 1.02 + ) +) t WHERE days > 0 ORDER BY start_rn; + +-- Volume climax reversal: uptrend, volume spike (1.5x), then decline +SELECT * FROM ( + SELECT first_value(rn) OVER w AS start_rn, + last_value(rn) OVER w AS end_rn, + first_value(price) OVER w AS start_price, + last_value(price) OVER w AS end_price, + count(*) OVER w AS days + FROM rpr_stock + WINDOW w AS ( + PARTITION BY part_id + ORDER BY rn + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (RALLY{3,} CLIMAX SELLOFF{2,}) + DEFINE + RALLY AS price > PREV(price), + CLIMAX AS volume > PREV(volume) * 1.5, + SELLOFF AS price < PREV(price) + ) +) t WHERE days > 0 ORDER BY start_rn; diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql new file mode 100644 index 00000000000..3de44c509e8 --- /dev/null +++ b/src/test/regress/sql/rpr_base.sql @@ -0,0 +1,4827 @@ +-- ============================================================ +-- RPR Base Tests +-- Tests for Row Pattern Recognition (ISO/IEC 19075-5) +-- ============================================================ +-- +-- Parser Layer: +-- Keyword Usage Tests +-- DEFINE Clause Tests +-- FRAME Options Tests +-- PARTITION BY + FRAME Tests +-- PATTERN Syntax Tests +-- Quantifiers Tests +-- Navigation Functions Tests +-- SKIP TO / INITIAL Tests +-- Serialization/Deserialization Tests +-- Glued Quantifier / Alternation Tests +-- Error Cases Tests +-- Window Deduplication Tests +-- +-- Planner Layer: +-- Pattern Optimization Tests +-- Absorption Flag Display Tests +-- Absorption Analysis Tests +-- Edge Case Tests +-- Optimization Fallback Tests +-- Planner Integration Tests +-- Subquery and CTE Tests +-- JOIN Tests +-- Complex Expression Tests +-- Set Operations Tests +-- Sorting and Grouping Tests +-- SQL Function Inlining Tests +-- Stress Tests +-- Error Limit Tests +-- +-- Contributed Tests: +-- Basic Pattern Matching +-- Pathological Patterns +-- ============================================================ + +SET client_min_messages = WARNING; + +-- ============================================================ +-- Keyword Usage Tests +-- ============================================================ + +-- RPR keywords as column names +-- Keywords: define, initial, past, pattern, seek + +CREATE TABLE rpr_keywords ( + id INT, + define INT, -- DEFINE keyword + initial INT, -- INITIAL keyword + past INT, -- PAST keyword + pattern INT, -- PATTERN keyword + seek INT, -- SEEK keyword +-- ERROR: SEEK is not supported + skip INT -- SKIP keyword (pre-existing) +); + +INSERT INTO rpr_keywords VALUES (1, 10, 20, 30, 40, 50, 60); + +SELECT id, define, initial, past, pattern, seek, skip +FROM rpr_keywords +ORDER BY id; + +DROP TABLE rpr_keywords; + +-- ============================================================ +-- DEFINE Clause Tests +-- ============================================================ + +-- Simple column references +CREATE TABLE stock_price ( + dt DATE, + symbol TEXT, + price NUMERIC, + volume INT +); + +INSERT INTO stock_price VALUES + ('2024-01-01', 'AAPL', 150, 1000), + ('2024-01-02', 'AAPL', 155, 1200), + ('2024-01-03', 'AAPL', 152, 900), + ('2024-01-04', 'AAPL', 160, 1500), + ('2024-01-05', 'AAPL', 158, 1100); + +-- Simple column reference +SELECT dt, price, COUNT(*) OVER w as cnt +FROM stock_price +WINDOW w AS ( + PARTITION BY symbol + ORDER BY dt + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (UP+) + DEFINE UP AS price > 150 +) +ORDER BY dt; + +-- Multiple column references +SELECT dt, price, volume, COUNT(*) OVER w as cnt +FROM stock_price +WINDOW w AS ( + PARTITION BY symbol + ORDER BY dt + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (GOOD+) + DEFINE GOOD AS price > 150 AND volume > 1000 +) +ORDER BY dt; + +-- Expression in DEFINE +SELECT dt, price, COUNT(*) OVER w as cnt +FROM stock_price +WINDOW w AS ( + PARTITION BY symbol + ORDER BY dt + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (HIGH+) + DEFINE HIGH AS price * 1.1 > 165 +) +ORDER BY dt; + +-- Arithmetic and functions +SELECT dt, price, volume, COUNT(*) OVER w as cnt +FROM stock_price +WINDOW w AS ( + PARTITION BY symbol + ORDER BY dt + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (CALC+) + DEFINE CALC AS (price + volume / 100) > 160 +) +ORDER BY dt; + +DROP TABLE stock_price; + +-- Auto-generated DEFINE +CREATE TABLE rpr_auto (id INT, val INT); +INSERT INTO rpr_auto VALUES (1, 10), (2, 20), (3, 30), (4, 15); + +-- One variable undefined (B auto-generated as "B IS TRUE") +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_auto +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B*) + DEFINE A AS val > 15 +) +ORDER BY id; + +-- Multiple undefined variables +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_auto +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C) + DEFINE A AS val > 0 + -- B and C auto-generated as "B IS TRUE", "C IS TRUE" +) +ORDER BY id; + +-- All variables defined explicitly +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_auto +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (X Y Z) + DEFINE + X AS val > 10, + Y AS val > 20, + Z AS val < 20 +) +ORDER BY id; + +DROP TABLE rpr_auto; + +-- Duplicate variable names +CREATE TABLE rpr_dup (id INT); +INSERT INTO rpr_dup VALUES (1), (2); + +-- Duplicate DEFINE variable name is not allowed +SELECT COUNT(*) OVER w +FROM rpr_dup +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id > 0, A AS id < 10 +); + +DROP TABLE rpr_dup; + +-- Boolean coercion +CREATE TABLE rpr_bool (id INT, flag BOOLEAN); +INSERT INTO rpr_bool VALUES (1, true), (2, false); + +-- DEFINE clause must be a boolean expression +SELECT COUNT(*) OVER w +FROM rpr_bool +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id +); + +-- Boolean column reference +SELECT id, flag, COUNT(*) OVER w as cnt +FROM rpr_bool +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (T+) + DEFINE T AS flag +) +ORDER BY id; + +-- NULL::boolean +SELECT id, COUNT(*) OVER w as cnt +FROM rpr_bool +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (N+) + DEFINE N AS NULL::boolean +) +ORDER BY id; + +-- Implicit cast to boolean via custom type +CREATE TYPE truthyint AS (v int); +CREATE FUNCTION truthyint_to_bool(truthyint) RETURNS boolean AS $$ + SELECT ($1).v <> 0; +$$ LANGUAGE SQL IMMUTABLE STRICT; +CREATE CAST (truthyint AS boolean) + WITH FUNCTION truthyint_to_bool(truthyint) + AS ASSIGNMENT; + +CREATE TABLE rpr_coerce (id int, val truthyint); +INSERT INTO rpr_coerce VALUES (1, ROW(1)), (2, ROW(0)), (3, ROW(5)), (4, ROW(0)); + +SELECT id, val, cnt +FROM (SELECT id, val, + COUNT(*) OVER w AS cnt + FROM rpr_coerce + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val + ) +) s ORDER BY id; + +DROP TABLE rpr_coerce; +DROP CAST (truthyint AS boolean); +DROP FUNCTION truthyint_to_bool(truthyint); +DROP TYPE truthyint; + +DROP TABLE rpr_bool; + +-- Coercion over a boolean domain is not a no-op; the wrapped Var must still +-- propagate when referenced only in DEFINE (flag is not in the select list) +CREATE DOMAIN boolish AS boolean; +CREATE TABLE rpr_domain (id int, flag boolish); +INSERT INTO rpr_domain VALUES (1, true), (2, false), (3, true); +SELECT id, COUNT(*) OVER w AS cnt +FROM rpr_domain +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS flag +) +ORDER BY id; +DROP TABLE rpr_domain; +DROP DOMAIN boolish; + +-- A Var referenced only inside a navigation operation must still propagate +-- (val appears only inside PREV(), not as a bare operand or in the select list) +CREATE TABLE rpr_nav (id int, val int); +INSERT INTO rpr_nav VALUES (1, 0), (2, 1), (3, 0), (4, 2); +SELECT id, COUNT(*) OVER w AS cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (UP+) + DEFINE UP AS id > PREV(val) +) +ORDER BY id; +DROP TABLE rpr_nav; + +-- A non-boolean DEFINE expression is rejected +CREATE TABLE rpr_noncoerce (id int, n int); +INSERT INTO rpr_noncoerce VALUES (1, 1); +SELECT id, COUNT(*) OVER w AS cnt +FROM rpr_noncoerce +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS n +); +DROP TABLE rpr_noncoerce; + +-- A non-boolean later DEFINE is rejected at its own definition even when an +-- earlier DEFINE variable is valid +CREATE TABLE rpr_noncoerce2 (id int, n int); +INSERT INTO rpr_noncoerce2 VALUES (1, 1); +SELECT id, COUNT(*) OVER w AS cnt +FROM rpr_noncoerce2 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS id > 0, B AS n +); +DROP TABLE rpr_noncoerce2; + +-- Complex expressions +CREATE TABLE rpr_complex (id INT, val1 INT, val2 INT); +INSERT INTO rpr_complex VALUES (1, 10, 20), (2, 15, 25), (3, 20, 30); + +-- CASE expression +SELECT id, val1, val2, COUNT(*) OVER w as cnt +FROM rpr_complex +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C+) + DEFINE C AS CASE WHEN val1 > 10 THEN val2 > 20 ELSE false END +) +ORDER BY id; + +DROP TABLE rpr_complex; + +-- Pattern variable not in PATTERN (should be ignored) +CREATE TABLE rpr_unused (id INT); +INSERT INTO rpr_unused VALUES (1), (2); + +-- Extra DEFINE variable +SELECT id, COUNT(*) OVER w as cnt +FROM rpr_unused +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS id > 0, B AS id > 5 -- B not in pattern +) +ORDER BY id; + +DROP TABLE rpr_unused; + +-- ============================================================ +-- FRAME Options Tests +-- ============================================================ + +CREATE TABLE rpr_frame (id INT, val INT); +INSERT INTO rpr_frame VALUES + (1, 10), (2, 10), (3, 10), -- Same val: 10 + (4, 20), (5, 20), -- Same val: 20 + (6, 30); + +-- Valid frame options + +-- ROWS: counts physical rows (1 FOLLOWING = next 1 physical row) +-- Expected result: Each row can see 1 physical row ahead +-- id=1,2,3 (val=10): can see next row -> cnt=2 +-- id=4,5 (val=20): can see next row -> cnt=2 +-- id=6 (val=30): no next row -> cnt=1 +-- Result: [2,2,2,2,2,1] +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY val + ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B?) + DEFINE A AS val >= 0, B AS val >= 0 +) +ORDER BY id; + +-- ERROR: frame must start at current row when row pattern recognition is used +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- EXCLUDE options + +-- EXCLUDE not permitted +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + EXCLUDE CURRENT ROW + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- EXCLUDE GROUP not permitted +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + EXCLUDE GROUP + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- EXCLUDE TIES not permitted +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + EXCLUDE TIES + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- range frame is not allowed with RPR +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- GROUPS frame is not allowed with RPR +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + GROUPS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- ERROR: frame must start at current row when row pattern recognition is used +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN 1 PRECEDING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- ERROR: frame must start at current row with RPR +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN 1 FOLLOWING AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- ERROR: end before start: CURRENT ROW AND 1 PRECEDING +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 1 PRECEDING + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- ERROR: end before start: CURRENT ROW AND UNBOUNDED PRECEDING +SELECT COUNT(*) OVER w +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED PRECEDING + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- Single row frame: CURRENT ROW AND CURRENT ROW is rejected (the standard +-- allows only UNBOUNDED FOLLOWING or a positive offset FOLLOWING). +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND CURRENT ROW + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Zero offset: CURRENT ROW AND 0 FOLLOWING denotes the same one-row frame +-- and is likewise rejected (caught at execution time). +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 0 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- A non-constant frame end offset is allowed; a zero value is still rejected, +-- this time at execution time (a literal cannot exercise that path). +PREPARE rpr_end_offset(int8) AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND $1 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE A AS val > 0 +) +ORDER BY id; +EXECUTE rpr_end_offset(2); +EXECUTE rpr_end_offset(0); +DEALLOCATE rpr_end_offset; + +-- Large offset: CURRENT ROW AND 1000 FOLLOWING +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 1000 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Maximum offset: CURRENT ROW AND 2147483646 FOLLOWING (INT_MAX - 1) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2147483646 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- int64 frame-end overflow: a huge FOLLOWING offset must clamp to the +-- partition end (matchStartRow + offset + 1 overflows int64; the clamp makes +-- it behave like UNBOUNDED FOLLOWING). Guards against signed-integer overflow +-- in the "frameOffset + 1" subexpression (undefined behavior). The cnt values +-- must match the UNBOUNDED FOLLOWING result for the same data. +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 9223372036854775806 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- range frame is not allowed with RPR +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY val + RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B?) + DEFINE A AS val >= 0, B AS val >= 0 +) +ORDER BY id; + +-- GROUPS frame with RPR (not permitted) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_frame +WINDOW w AS ( + ORDER BY val + GROUPS BETWEEN CURRENT ROW AND 1 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B?) + DEFINE A AS val >= 0, B AS val >= 0 +) +ORDER BY id; + +DROP TABLE rpr_frame; + +-- ============================================================ +-- PARTITION BY + FRAME Tests +-- ============================================================ + +-- Test PARTITION BY with RPR to ensure proper partitioning behavior +CREATE TABLE rpr_partition (id INT, grp INT, val INT); +INSERT INTO rpr_partition VALUES + (1, 1, 10), (2, 1, 20), (3, 1, 30), + (4, 2, 15), (5, 2, 25), (6, 2, 35); + +-- PARTITION BY with ROWS frame +SELECT id, grp, val, COUNT(*) OVER w as cnt +FROM rpr_partition +WINDOW w AS ( + PARTITION BY grp + ORDER BY val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B+) + DEFINE A AS val >= 10, B AS val > 15 +) +ORDER BY id; +-- Expected: Pattern matching should reset for each partition + +-- PARTITION BY with RANGE frame +SELECT id, grp, val, COUNT(*) OVER w as cnt +FROM rpr_partition +WINDOW w AS ( + PARTITION BY grp + ORDER BY val + RANGE BETWEEN CURRENT ROW AND 10 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B?) + DEFINE A AS val >= 10, B AS val >= 20 +) +ORDER BY id; + +DROP TABLE rpr_partition; + +-- ============================================================ +-- PATTERN Syntax Tests +-- ============================================================ + +CREATE TABLE rpr_pattern (id INT, val INT); +INSERT INTO rpr_pattern VALUES + (1, 5), (2, 10), (3, 15), (4, 20), (5, 25), + (6, 30), (7, 35), (8, 40), (9, 45), (10, 50); + +-- Alternation (|) + +-- Multiple alternatives +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ | B+ | C+) + DEFINE A AS val > 35, B AS val BETWEEN 15 AND 35, C AS val < 15 +) +ORDER BY id; + +-- Grouping + +-- Nested grouping with quantifier +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B) C)+) + DEFINE A AS val > 10, B AS val > 20, C AS val > 30 +) +ORDER BY id; + +-- Sequence + +-- Multi-element sequence +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C D E) + DEFINE + A AS val < 15, + B AS val BETWEEN 15 AND 25, + C AS val BETWEEN 25 AND 35, + D AS val BETWEEN 35 AND 45, + E AS val >= 45 +) +ORDER BY id; + +-- Complex combinations + +-- Alternation with grouping +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B) | (C D)) + DEFINE A AS val < 20, B AS val >= 20, C AS val < 30, D AS val >= 30 +) +ORDER BY id; + +-- Alternation + sequence + grouping +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (START (UP{2,} DOWN? | FLAT+) FINISH) + DEFINE + START AS val >= 0, + UP AS val > 20, + DOWN AS val <= 30, + FLAT AS val BETWEEN 25 AND 35, + FINISH AS val > 40 +) +ORDER BY id; + +-- Nested alternation in groups +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B) (C | D)) + DEFINE A AS val < 15, B AS val BETWEEN 15 AND 25, C AS val BETWEEN 25 AND 35, D AS val > 35 +) +ORDER BY id; + +DROP TABLE rpr_pattern; + +-- ============================================================ +-- Quantifiers Tests +-- ============================================================ + +CREATE TABLE rpr_quant (id INT, val INT); +INSERT INTO rpr_quant VALUES + (1, 10), (2, 20), (3, 30), (4, 40), (5, 50), + (6, 60), (7, 70), (8, 80), (9, 90), (10, 100); + +-- Basic greedy quantifiers + +-- * (zero or more) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A*) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- + (one or more) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 50 +) +ORDER BY id; + +-- ? (zero or one) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A?) + DEFINE A AS val = 50 +) +ORDER BY id; + +-- Edge case quantifiers + +-- {0} is not allowed (min must be >= 1) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{0} B) + DEFINE A AS val > 1000, B AS val > 0 +) +ORDER BY id; + +-- {0,0} is not allowed (max must be >= 1) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{0,0} B) + DEFINE A AS val > 1000, B AS val > 0 +) +ORDER BY id; + +-- {0,1} (equivalent to ?) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{0,1}) + DEFINE A AS val = 50 +) +ORDER BY id; + +-- Exact quantifiers {n} + +-- {3} (representative exact quantifier) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{3}) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Range quantifiers {n,} + +-- {2,} (representative n or more) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,}) + DEFINE A AS val > 40 +) +ORDER BY id; + +-- Upper bound quantifiers {,m} + +-- {,3} (representative up to m) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,3}) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Range quantifiers {n,m} + +-- {3,7} (representative range) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{3,7}) + DEFINE A AS val > 0 +) +ORDER BY id; + +DROP TABLE rpr_quant; + +-- Reluctant quantifiers +CREATE TABLE rpr_reluctant (id INT, val INT); +INSERT INTO rpr_reluctant VALUES (1, 10), (2, 20), (3, 30); + +-- *? (zero or more, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A*?) + DEFINE A AS val > 0 +); + +-- +? (one or more, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+?) + DEFINE A AS val > 0 +); + +-- ?? (zero or one, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A??) + DEFINE A AS val > 0 +); + +-- {n,}? (n or more, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,}?) + DEFINE A AS val > 0 +); + +-- {n,m}? (n to m, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1,3}?) + DEFINE A AS val > 0 +); + +-- {n}? (exactly n, reluctant) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2}?) + DEFINE A AS val > 0 +); + +-- {,m}? (up to m, reluctant) - COMPLETELY UNTESTED RULE! +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,3}?) + DEFINE A AS val > 0 +); + +-- {2}+ (should be {2}? not {2}+) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2}+) + DEFINE A AS val > 0 +); + +-- {2,}* (should be {2,}? not {2,}*) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,}*) + DEFINE A AS val > 0 +); + +-- {,3}* (should be {,3}? not {,3}*) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,3}*) + DEFINE A AS val > 0 +); + +-- {1,3}+ (should be {1,3}? not {1,3}+) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1,3}+) + DEFINE A AS val > 0 +); + +-- Boundary errors in reluctant quantifiers + +-- negative bound is not allowed +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{-1}?) + DEFINE A AS val > 0 +); + +-- ERROR: quantifier bound exceeds limits +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2147483647}?) + DEFINE A AS val > 0 +); + +-- negative lower bound is not allowed +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{-1,}?) + DEFINE A AS val > 0 +); + +-- ERROR: quantifier lower bound exceeds limits +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2147483647,}?) + DEFINE A AS val > 0 +); + +-- zero upper bound is not allowed +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,0}?) + DEFINE A AS val > 0 +); + +-- ERROR: {,2147483647}? (upper bound in range exceeds limits) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,2147483647}?) + DEFINE A AS val > 0 +); + +-- ERROR: {-1,3}? (negative lower bound in range is not allowed) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{-1,3}?) + DEFINE A AS val > 0 +); + +-- ERROR: {1,2147483647}? (upper bound in range exceeds limits) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1,2147483647}?) + DEFINE A AS val > 0 +); + +-- ERROR: {5,3}? (min > max is not allowed) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{5,3}?) + DEFINE A AS val > 0 +); + +-- Token-separated reluctant quantifiers (space between quantifier and ?) +-- These may be tokenized differently by the lexer + +-- * ? (token separated) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A* ?) + DEFINE A AS val > 0 +); + +-- + ? (token separated) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ ?) + DEFINE A AS val > 0 +); + +-- {2,} ? (token separated) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,} ?) + DEFINE A AS val > 0 +); + +-- * + (invalid combination) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A* +) + DEFINE A AS val > 0 +); + +-- + * (invalid combination) +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ *) + DEFINE A AS val > 0 +); + +-- ? ? (parsed as ?? reluctant quantifier) +-- Reluctant quantifier: prefer shortest match +SELECT COUNT(*) OVER w +FROM rpr_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A? ?) + DEFINE A AS val > 0 +); + +DROP TABLE rpr_reluctant; + +-- Quantifier boundary conditions + +CREATE TABLE rpr_bounds (id INT); +INSERT INTO rpr_bounds VALUES (1), (2); + +-- ERROR: quantifier lower bound must not exceed upper bound +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{5,3}) + DEFINE A AS id > 0 +); + +-- Large bounds +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1000,2000}) + DEFINE A AS id > 0 +); + +-- Very large bound +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{100000}) + DEFINE A AS id > 0 +); + +-- INT_MAX - 1 = 2147483646 (at limit) +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2147483646}) + DEFINE A AS id > 0 +); + +-- ERROR: quantifier bound exceeds limits +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2147483647}) + DEFINE A AS id > 0 +); + +-- {n,} boundary errors + +-- ERROR: negative lower bound in {n,} is not allowed +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{-1,}) + DEFINE A AS id > 0 +); + +-- ERROR: quantifier lower bound exceeds limits +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2147483647,}) + DEFINE A AS id > 0 +); + +-- {,m} boundary errors + +-- Zero upper bound in {,m} +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,0}) + DEFINE A AS id > 0 +); + +-- ERROR: quantifier upper bound exceeds limits +SELECT COUNT(*) OVER w +FROM rpr_bounds +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{,2147483647}) + DEFINE A AS id > 0 +); + +DROP TABLE rpr_bounds; + +-- Pattern element-count boundary at RPR_ELEMIDX_MAX (32767). Alternating +-- distinct variables stop the optimizer from merging consecutive elements, so +-- each "A B" pair contributes two elements; scanRPRPattern adds one FIN marker. +-- ECHO is silenced so the generated multi-thousand-token patterns do not flood +-- the expected output. +-- 16383 pairs -> 32766 + 1 FIN = 32767 = maximum, accepted. +-- 16383 pairs + one A -> 32767 + 1 FIN = 32768 > maximum, rejected. +\set ECHO none +SELECT format($$SELECT count(*) OVER w FROM (SELECT 1 i) t + WINDOW w AS (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL PATTERN (%s) DEFINE A AS TRUE, B AS TRUE)$$, + repeat('A B ', 16383)) \gexec +SELECT format($$SELECT count(*) OVER w FROM (SELECT 1 i) t + WINDOW w AS (ORDER BY i ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL PATTERN (%s A) DEFINE A AS TRUE, B AS TRUE)$$, + repeat('A B ', 16383)) \gexec +\set ECHO all + +-- ============================================================ +-- Navigation Functions Tests (PREV / NEXT / FIRST / LAST) +-- ============================================================ + +CREATE TABLE rpr_nav (id INT, val INT); +INSERT INTO rpr_nav VALUES + (1, 10), (2, 20), (3, 15), (4, 25), (5, 30); + +-- PREV function - reference previous row in pattern +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS val > PREV(val) +) +ORDER BY id; + +-- NEXT function - reference next row in pattern +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B) + DEFINE + A AS val < NEXT(val), + B AS val > 0 +) +ORDER BY id; + +-- Combined PREV and NEXT +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C) + DEFINE + A AS val > 0, + B AS val > PREV(val) AND val < NEXT(val), + C AS val > PREV(val) +) +ORDER BY id; + +-- PREV function cannot be used other than in DEFINE +SELECT PREV(id), id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS val > PREV(val) +) +ORDER BY id; + +-- NEXT function cannot be used other than in DEFINE +SELECT NEXT(id), id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS val > PREV(val) +) +ORDER BY id; + +-- FIRST function - reference match_start row +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS val > FIRST(val) +) +ORDER BY id; + +-- LAST function without offset - equivalent to current row's value +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS LAST(val) > PREV(val) +) +ORDER BY id; + +-- FIRST and LAST combined +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_nav +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE + A AS val > 0, + B AS val > FIRST(val) AND LAST(val) > PREV(val) +) +ORDER BY id; + +-- FIRST function cannot be used other than in DEFINE +SELECT FIRST(id), id, val FROM rpr_nav; + +-- LAST function cannot be used other than in DEFINE +SELECT LAST(id), id, val FROM rpr_nav; + +DROP TABLE rpr_nav; + +-- Name-space: prev/next/first/last are navigation functions, not ordinary functions +CREATE SCHEMA rpr_navns; +SET search_path TO rpr_navns, public; +CREATE TABLE nt (g text, id int, val int); +INSERT INTO nt VALUES ('x', 1, 100), ('x', 2, 200), ('x', 3, 150), + ('x', 4, 140), ('x', 5, 150); + +-- Outside DEFINE these are ordinary identifiers and resolve to nothing +SELECT prev(val) FROM nt; +SELECT next(val) FROM nt; +SELECT prev(val, 2) FROM nt; +SELECT next(val, 2) FROM nt; +SELECT first(val) FROM nt; +SELECT last(val) FROM nt; +SELECT first(val, 1) FROM nt; +-- A schema-qualified call is also a plain (failing) function lookup +SELECT pg_catalog.prev(val) FROM nt; + +-- Outside DEFINE, a user-defined function of that name is callable +CREATE FUNCTION next(numeric) RETURNS numeric AS 'SELECT -999::numeric' + LANGUAGE sql IMMUTABLE; +SELECT next(10); + +-- Inside DEFINE, unqualified PREV is nav whether or not a user prev() exists +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (START UP+) + DEFINE START AS TRUE, UP AS val > PREV(val)) + ORDER BY id; + +-- A qualified call invokes the function, so its volatility still matters +-- VOLATILE: unqualified is nav; qualified is rejected as a volatile function +CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999' + LANGUAGE sql VOLATILE; +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (START UP+) + DEFINE START AS TRUE, UP AS val > PREV(val)) + ORDER BY id; +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) + DEFINE A AS rpr_navns.prev(val) = -999) + ORDER BY id; +DROP FUNCTION prev(integer); +-- IMMUTABLE: unqualified is nav; qualified is the escape hatch and succeeds +CREATE FUNCTION prev(integer) RETURNS integer AS 'SELECT -999' + LANGUAGE sql IMMUTABLE; +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (START UP+) + DEFINE START AS TRUE, UP AS val > PREV(val)) + ORDER BY id; +-- (val).prev is attribute notation, so it calls the ordinary function prev(val) +-- (the IMMUTABLE user prev here), the same as the schema-qualified call below +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) + DEFINE A AS (val).prev = -999) + ORDER BY id; +SELECT id, val, count(*) OVER w AS cnt, last_value(id) OVER w AS last_id + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) + DEFINE A AS rpr_navns.prev(val) = -999) + ORDER BY id; + +-- Zero or more than two arguments is an error, with no function fallback +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV() IS NULL); +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL); +-- the error stands even when a user function of that exact arity exists +CREATE FUNCTION prev(integer, integer, integer) RETURNS integer + AS 'SELECT -999' LANGUAGE sql IMMUTABLE; +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val, 1, 2) IS NULL); +DROP FUNCTION prev(integer, integer, integer); + +-- Syntactic decoration is rejected +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(*) IS NULL); +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(DISTINCT val) IS NULL); +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val ORDER BY val) IS NULL); +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val) FILTER (WHERE true) IS NULL); +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val) WITHIN GROUP (ORDER BY val) IS NULL); +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val) OVER () IS NULL); +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(VARIADIC ARRAY[val]) IS NULL); +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS prev(x => val) IS NULL); +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS PREV(val) IGNORE NULLS IS NULL); + +-- Quoting does not escape: "prev" is nav, "PREV" is an ordinary name +SELECT id, val, count(*) OVER w AS cnt + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (START UP+) + DEFINE START AS TRUE, UP AS val > "prev"(val)) + ORDER BY id; +SELECT count(*) OVER w FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS "PREV"(val) IS NULL); + +-- A view round-trips: bare PREV stays a navigation function, and a qualified +-- user prev() stays schema-qualified so it does not reparse as navigation +CREATE VIEW navns_nav AS + SELECT id, count(*) OVER w AS cnt FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (START UP+) DEFINE START AS TRUE, UP AS val > PREV(val)); +CREATE VIEW navns_fn AS + SELECT id, count(*) OVER w AS cnt FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS rpr_navns.prev(val) = -999); +SELECT pg_get_viewdef('navns_nav'); +SELECT pg_get_viewdef('navns_fn'); +DROP VIEW navns_nav, navns_fn; + +-- A qualified last() in DEFINE must stay schema-qualified on deparse so that +-- it does not reparse as the LAST navigation function (force-qualify path) +CREATE FUNCTION rpr_navns.last(integer) RETURNS integer AS 'SELECT -999' LANGUAGE sql IMMUTABLE; +CREATE VIEW navns_fn_last AS + SELECT id, count(*) OVER w AS cnt FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS rpr_navns.last(val) = -999); +SELECT pg_get_viewdef('navns_fn_last'); +DROP VIEW navns_fn_last; +DROP FUNCTION rpr_navns.last(integer); + +-- Attribute notation is field selection only, never a function fallback +CREATE TYPE rpr_navns_pair AS (first int, last int); +CREATE TABLE ct (id int, p rpr_navns_pair); +INSERT INTO ct VALUES (1, (10, 20)), (2, (30, 40)); +SELECT (p).last FROM ct ORDER BY id; +SELECT count(*) OVER w FROM ct + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS (p).last > 0); +SELECT count(*) OVER w FROM ct + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) DEFINE A AS (p).prev > 0); + +-- Navigation offset must not contain a navigation operation +SELECT id, val + FROM nt + WINDOW w AS (PARTITION BY g ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING INITIAL + PATTERN (A+) + DEFINE A AS PREV(val, FIRST(1)) > 0) + ORDER BY id; + +DROP SCHEMA rpr_navns CASCADE; +RESET search_path; + +-- ============================================================ +-- SKIP TO / INITIAL Tests +-- ============================================================ + +CREATE TABLE rpr_skip (id INT, val INT); +INSERT INTO rpr_skip VALUES + (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), + (6, 6), (7, 7), (8, 8); + +-- SKIP TO NEXT ROW + +-- SKIP TO NEXT ROW +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_skip +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C) + DEFINE A AS val > 0, B AS val > 2, C AS val > 4 +) +ORDER BY id; + +-- SKIP PAST LAST ROW + +-- SKIP PAST LAST ROW +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_skip +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS val > 0, B AS val > 2, C AS val > 4 +) +ORDER BY id; + +-- Default behavior (should be SKIP PAST LAST ROW) + +-- No SKIP TO clause (default) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_skip +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B) + DEFINE A AS val > 0, B AS val > 1 +) +ORDER BY id; + +-- Compare default with explicit PAST LAST ROW +-- Results should be identical +WITH default_skip AS ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_skip + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C) + DEFINE A AS val > 0, B AS val > 2, C AS val > 4 + ) +), +explicit_skip AS ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_skip + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS val > 0, B AS val > 2, C AS val > 4 + ) +) +SELECT 'default' as type, * FROM default_skip +UNION ALL +SELECT 'explicit' as type, * FROM explicit_skip +ORDER BY type, id; + +DROP TABLE rpr_skip; + +CREATE TABLE rpr_init (id INT, val INT); +INSERT INTO rpr_init VALUES (1, 10), (2, 20), (3, 30), (4, 40); + +-- Explicit INITIAL +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_init +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Implicit INITIAL (default) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_init +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +DROP TABLE rpr_init; + +-- SEEK + +CREATE TABLE rpr_seek (id INT, val INT); +INSERT INTO rpr_seek VALUES (1, 10); + +-- SEEK keyword, SEEK mode is not supported +SELECT COUNT(*) OVER w +FROM rpr_seek +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + SEEK + PATTERN (A+) + DEFINE A AS val > 0 +); + +DROP TABLE rpr_seek; + +-- ============================================================ +-- Serialization/Deserialization Tests +-- ============================================================ +-- RPR-defining views and tables here are intentionally left in place (not +-- dropped) so that pg_dump/pg_upgrade exercise the deparse-then-re-parse +-- round-trip of the RPR window clause. + +-- View creation and deparsing + +CREATE TABLE rpr_serial (id INT, val INT); +INSERT INTO rpr_serial VALUES + (1, 10), (2, 20), (3, 15), (4, 25), (5, 30); + +-- Simple pattern +CREATE VIEW rpr_serial_v1 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); + +-- Verify view works (tests deserialization) +SELECT * FROM rpr_serial_v1 ORDER BY id; + +-- Verify deparsing +SELECT pg_get_viewdef('rpr_serial_v1'::regclass); + +-- Complex pattern with alternation +CREATE VIEW rpr_serial_v2 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ | B*) + DEFINE A AS val > 20, B AS val <= 20 +); + +SELECT * FROM rpr_serial_v2 ORDER BY id; +SELECT pg_get_viewdef('rpr_serial_v2'::regclass); + +-- Pattern with grouping and quantifiers +CREATE VIEW rpr_serial_v3 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2,5} | C*) + DEFINE + A AS val > 10, + B AS val > 20, + C AS val <= 10 +); + +SELECT * FROM rpr_serial_v3 ORDER BY id; +SELECT pg_get_viewdef('rpr_serial_v3'::regclass); + +-- All features combined +CREATE VIEW rpr_serial_v4 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (START (MID{1,3} | ALT+) FINISH) + DEFINE + START AS val > 5, + MID AS val BETWEEN 10 AND 25, + ALT AS val > 25, + FINISH AS val > 15 +); + +SELECT * FROM rpr_serial_v4 ORDER BY id; +SELECT pg_get_viewdef('rpr_serial_v4'::regclass); + +-- Additional quantifiers for deparsing coverage + +-- ? quantifier (zero or one) +CREATE VIEW rpr_serial_v5 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B?) + DEFINE A AS val > 10, B AS val > 20 +); + +SELECT * FROM rpr_serial_v5 ORDER BY id; +SELECT pg_get_viewdef('rpr_serial_v5'::regclass); + +-- {n,} quantifier (n or more) +CREATE VIEW rpr_serial_v6 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2,}) + DEFINE A AS val > 15 +); + +SELECT * FROM rpr_serial_v6 ORDER BY id; +SELECT pg_get_viewdef('rpr_serial_v6'::regclass); + +-- {n} quantifier (exactly n) +CREATE VIEW rpr_serial_v7 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{3}) + DEFINE A AS val > 0 +); + +SELECT * FROM rpr_serial_v7 ORDER BY id; +SELECT pg_get_viewdef('rpr_serial_v7'::regclass); + +-- Nested ALT pattern (tests deparse of complex nested structure) +CREATE VIEW rpr_serial_v8 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_serial +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A+ B) | C) D | A B C) + DEFINE A AS val <= 15, B AS val <= 25, C AS val <= 30, D AS val > 30 +); + +SELECT * FROM rpr_serial_v8 ORDER BY id; +SELECT pg_get_viewdef('rpr_serial_v8'::regclass); + +-- Navigation function serialization: PREV with offset +CREATE VIEW rpr_serial_nav1 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS val > PREV(val, 2)); +SELECT pg_get_viewdef('rpr_serial_nav1'::regclass); + +-- Navigation function serialization: FIRST and LAST +CREATE VIEW rpr_serial_nav2 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS FIRST(val) < LAST(val, 1)); +SELECT pg_get_viewdef('rpr_serial_nav2'::regclass); + +-- Navigation function serialization: compound PREV(FIRST()) +CREATE VIEW rpr_serial_nav3 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(FIRST(val, 1), 2) > 0); +SELECT pg_get_viewdef('rpr_serial_nav3'::regclass); + +-- Navigation function serialization: compound NEXT(LAST()) +CREATE VIEW rpr_serial_nav4 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(LAST(val), 2) IS NOT NULL); +SELECT pg_get_viewdef('rpr_serial_nav4'::regclass); + +-- Navigation function serialization: compound PREV(LAST()) +CREATE VIEW rpr_serial_nav5 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS PREV(LAST(val, 1), 2) > 0); +SELECT pg_get_viewdef('rpr_serial_nav5'::regclass); + +-- Navigation function serialization: compound NEXT(FIRST()) +CREATE VIEW rpr_serial_nav6 AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS NEXT(FIRST(val), 3) > 0); +SELECT pg_get_viewdef('rpr_serial_nav6'::regclass); + +-- Pretty deparse: navigation calls are function-like and take no extra parens +CREATE VIEW rpr_nav_pretty_v AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, + B AS val > PREV(val) AND PREV(val) IS NOT NULL + AND NEXT(val) > FIRST(val) + AND PREV(FIRST(val)) > 0); +SELECT pg_get_viewdef('rpr_nav_pretty_v'::regclass, true); + +-- Reluctant {1}? quantifier deparse through ruleutils +CREATE VIEW rpr_quant_reluctant_v AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A{1}? B) + DEFINE A AS val > 0, B AS val > 0); +SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass); + +-- Quoted identifier round-trip: mixed case and reserved words need quoting +CREATE VIEW rpr_serial_quoted AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ("Start" "Up"+) + DEFINE "Start" AS TRUE, "Up" AS val > PREV(val)); +SELECT pg_get_viewdef('rpr_serial_quoted'::regclass); + +-- Inline OVER round-trip: inline window spec (no WINDOW alias) deparses inside OVER (...) +CREATE VIEW rpr_serial_inline_over AS +SELECT id, val, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS val > 10, B AS val > PREV(val)) AS cnt +FROM rpr_serial; +SELECT pg_get_viewdef('rpr_serial_inline_over'::regclass); + +-- Materialized view (if supported) + +CREATE TABLE rpr_mview (id INT, val INT); +INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30); + +CREATE MATERIALIZED VIEW rpr_mview_v1 AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_mview +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +); + +SELECT * FROM rpr_mview_v1 ORDER BY id; +SELECT pg_get_viewdef('rpr_mview_v1'::regclass); + +-- Refresh test +REFRESH MATERIALIZED VIEW rpr_mview_v1; +SELECT * FROM rpr_mview_v1 ORDER BY id; + +-- CREATE TABLE AS SELECT with RPR +CREATE TABLE rpr_ctas (id INT, val INT); +INSERT INTO rpr_ctas VALUES (1, 10), (2, 20), (3, 15), (4, 25); + +CREATE TABLE rpr_ctas_result AS +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_ctas +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS val > PREV(val) +); +SELECT * FROM rpr_ctas_result ORDER BY id; + +-- INSERT INTO ... SELECT with RPR +CREATE TABLE rpr_insert_target (id INT, val INT, cnt BIGINT); +INSERT INTO rpr_insert_target +SELECT id, val, count(*) OVER w +FROM rpr_ctas +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS TRUE, B AS val > PREV(val) +); +SELECT * FROM rpr_insert_target ORDER BY id; + +DROP TABLE rpr_ctas_result; +DROP TABLE rpr_insert_target; +DROP TABLE rpr_ctas; + +-- Prepared statements (tests outfuncs.c / readfuncs.c) + +CREATE TABLE rpr_prep (id INT, val INT); +INSERT INTO rpr_prep VALUES (1, 10), (2, 20), (3, 30); + +-- Simple prepared statement +PREPARE rpr_prep_simple AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_prep +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +EXECUTE rpr_prep_simple; +EXECUTE rpr_prep_simple; + +DEALLOCATE rpr_prep_simple; + +-- Prepared statement with parameters +PREPARE rpr_prep_param(int) AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_prep +WHERE id <= $1 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 10 +) +ORDER BY id; + +EXECUTE rpr_prep_param(2); +EXECUTE rpr_prep_param(3); + +DEALLOCATE rpr_prep_param; + +-- Complex prepared statement +PREPARE rpr_prep_complex AS +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_prep +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A B){1,2} | C+) + DEFINE + A AS val > 5, + B AS val > 15, + C AS val <= 15 +) +ORDER BY id; + +EXECUTE rpr_prep_complex; +EXECUTE rpr_prep_complex; + +DEALLOCATE rpr_prep_complex; + +DROP TABLE rpr_prep; + +-- CTE and Subquery (tests copyfuncs.c) + +CREATE TABLE rpr_copy (id INT, val INT); +INSERT INTO rpr_copy VALUES (1, 10), (2, 20), (3, 30), (4, 40); + +-- Simple CTE +WITH rpr_cte AS ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_copy + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) +SELECT * FROM rpr_cte ORDER BY id; + +-- CTE with multiple references (forces node copy) +WITH rpr_cte AS ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_copy + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 15 + ) +) +SELECT c1.id, c1.cnt as cnt1, c2.cnt as cnt2 +FROM rpr_cte c1 +JOIN rpr_cte c2 ON c1.id = c2.id +ORDER BY c1.id; + +-- Subquery in FROM clause +SELECT * +FROM ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_copy + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B?) + DEFINE A AS val > 10, B AS val > 20 + ) +) sub +WHERE cnt > 0 +ORDER BY id; + +-- Nested subqueries +SELECT * +FROM ( + SELECT * + FROM ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_copy + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val >= 10 + ) + ) inner_sub + WHERE cnt > 0 +) outer_sub +ORDER BY id; + +DROP TABLE rpr_copy; + +-- DISTINCT and set operations (tests equalfuncs.c) + +CREATE TABLE rpr_equal (id INT, val INT); +INSERT INTO rpr_equal VALUES (1, 10), (2, 20), (3, 10), (4, 20); + +-- DISTINCT with RPR +SELECT DISTINCT cnt +FROM ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_equal + WINDOW w AS ( + ORDER BY val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub +ORDER BY cnt; + +-- UNION with RPR in both sides +SELECT id, val, cnt FROM ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_equal + WHERE val = 10 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub1 +UNION +SELECT id, val, cnt FROM ( + SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_equal + WHERE val = 20 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub2 +ORDER BY id; + +-- UNION ALL +SELECT id, cnt FROM ( + SELECT id, COUNT(*) OVER w as cnt + FROM rpr_equal + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 10 + ) +) sub +UNION ALL +SELECT id, cnt FROM ( + SELECT id, COUNT(*) OVER w as cnt + FROM rpr_equal + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B+) + DEFINE B AS val <= 10 + ) +) sub +ORDER BY id, cnt; + +-- INTERSECT +SELECT id, cnt FROM ( + SELECT id, COUNT(*) OVER w as cnt + FROM rpr_equal + WHERE id <= 3 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub1 +INTERSECT +SELECT id, cnt FROM ( + SELECT id, COUNT(*) OVER w as cnt + FROM rpr_equal + WHERE id >= 2 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub2 +ORDER BY id; + +DROP TABLE rpr_equal; + +-- View with multiple window definitions + +CREATE TABLE rpr_multiwin (id INT, val INT); +INSERT INTO rpr_multiwin VALUES (1, 10), (2, 20), (3, 30); + +CREATE VIEW rpr_multiwin_v AS +SELECT + id, + val, + COUNT(*) OVER w1 as cnt1, + COUNT(*) OVER w2 as cnt2 +FROM rpr_multiwin +WINDOW + w1 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 15 + ), + w2 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B*) + DEFINE B AS val <= 15 + ); + +SELECT * FROM rpr_multiwin_v ORDER BY id; +SELECT pg_get_viewdef('rpr_multiwin_v'::regclass); + +-- {n} quantifier display in view +CREATE VIEW rpr_quant_n_v AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A{3}) + DEFINE A AS val > 0); +SELECT pg_get_viewdef('rpr_quant_n_v'::regclass); + +-- {n,} quantifier display in view +CREATE VIEW rpr_quant_n_plus_v AS +SELECT id, val, count(*) OVER w +FROM rpr_serial +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (A{2,}) + DEFINE A AS val > 0); +SELECT pg_get_viewdef('rpr_quant_n_plus_v'::regclass); + +-- ============================================================ +-- Glued Quantifier / Alternation Tests +-- ============================================================ +CREATE TABLE rpr_glue (id INT, val INT); +INSERT INTO rpr_glue VALUES (1, 5), (2, 8), (3, 9), (4, -1), (5, 6), (6, -2); +-- Quantifier glued to the alternation operator '|' without a space (0059). +-- The lexer glues the trailing '|' into one Op token; the grammar reattaches it +-- as the lowest-precedence alternation once the surrounding sequence is built. +-- Deparse is canonical, so the glued, spaced, and mixed-spacing forms all +-- reduce to the same PATTERN -- one deparse per shape proves the parse tree. + +-- Op-char quantifiers (*, +, ?, *?, +?, ??) glued to '|'. +CREATE VIEW rpr_dp_op AS SELECT + count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, + count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6 +FROM rpr_glue +WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0), + w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0), + w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A?|B) DEFINE A AS val > 0, B AS val <= 0), + w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0), + w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0), + w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_op'), E'\n')) AS line WHERE line ~ 'PATTERN'; +DROP VIEW rpr_dp_op; +-- Spaced reference: the fully-spaced canonical forms. Identical deparse to the +-- glued rpr_dp_op w1/w4 above completes the glued = spaced = mixed equivalence. +CREATE VIEW rpr_dp_spc AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2 +FROM rpr_glue +WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* | B) DEFINE A AS val > 0, B AS val <= 0), + w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*? | B) DEFINE A AS val > 0, B AS val <= 0); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_spc'), E'\n')) AS line WHERE line ~ 'PATTERN'; +DROP VIEW rpr_dp_spc; +-- Range quantifiers glued to '|': non-reluctant {n}| (} + char '|') and +-- reluctant {n}?| (} + Op "?|"). +CREATE VIEW rpr_dp_rng AS SELECT + count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, count(*) OVER w4 AS w4, + count(*) OVER w5 AS w5, count(*) OVER w6 AS w6, count(*) OVER w7 AS w7, count(*) OVER w8 AS w8 +FROM rpr_glue +WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}|B) DEFINE A AS val > 0, B AS val <= 0), + w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}|B) DEFINE A AS val > 0, B AS val <= 0), + w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}|B) DEFINE A AS val > 0, B AS val <= 0), + w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}|B) DEFINE A AS val > 0, B AS val <= 0), + w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}?|B) DEFINE A AS val > 0, B AS val <= 0), + w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,}?|B) DEFINE A AS val > 0, B AS val <= 0), + w7 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{,3}?|B) DEFINE A AS val > 0, B AS val <= 0), + w8 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2,3}?|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_rng'), E'\n')) AS line WHERE line ~ 'PATTERN'; +DROP VIEW rpr_dp_rng; +-- Mixed spacing: a space inside the quantifier with '|' still glued. +-- "A* ?|B" = '*' + Op"?|" = reluctant "A*?" plus alternation. +CREATE VIEW rpr_dp_mix AS SELECT count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3 +FROM rpr_glue +WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* ?|B) DEFINE A AS val > 0, B AS val <= 0), + w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+ ?|B) DEFINE A AS val > 0, B AS val <= 0), + w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? ?|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_mix'), E'\n')) AS line WHERE line ~ 'PATTERN'; +DROP VIEW rpr_dp_mix; +-- Structure: precedence (| is lowest, so its right operand is the whole +-- following sequence), chaining, concatenation, and grouping. +CREATE VIEW rpr_dp_struct AS SELECT + count(*) OVER w1 AS w1, count(*) OVER w2 AS w2, count(*) OVER w3 AS w3, + count(*) OVER w4 AS w4, count(*) OVER w5 AS w5, count(*) OVER w6 AS w6 +FROM rpr_glue +WINDOW w1 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100), + w2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B*|C) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100), + w3 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A B*|C D) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100, D AS val > 5), + w4 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)) DEFINE A AS val > 0, B AS val <= 0), + w5 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|(B|C)) DEFINE A AS val > 0, B AS val <= 0, C AS val < 100), + w6 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|B)+) DEFINE A AS val > 0, B AS val <= 0); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_dp_struct'), E'\n')) AS line WHERE line ~ 'PATTERN'; +DROP VIEW rpr_dp_struct; +-- Execution semantics (deparse cannot show reluctant shortest-match). The +-- rpr_glue rows -- an A-run followed by B rows -- make the '|B' alternative +-- reachable: with "*" the greedy form matches the whole run while the +-- reluctant form matches empty; with "+" the greedy form matches the run and +-- the reluctant form matches one row, and on a B row (where "A+" fails) the B +-- alternative fires. +SELECT id, val, + count(*) OVER gs AS gstar, count(*) OVER rs AS rstar, + count(*) OVER gp AS gplus, count(*) OVER rp AS rplus +FROM rpr_glue +WINDOW gs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B) DEFINE A AS val > 0, B AS val <= 0), + rs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?|B) DEFINE A AS val > 0, B AS val <= 0), + gp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+|B) DEFINE A AS val > 0, B AS val <= 0), + rp AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A+?|B) DEFINE A AS val > 0, B AS val <= 0) +ORDER BY id; +-- Patterns that must stay rejected. "&" is an invalid op; a '|' with an empty +-- side (leading, trailing, doubled, or alone in a group) has no operand; "||" +-- and "*||" are doubled pipes; "A* *|B"/"A* *?|B"/"A{2}*?|B" are doubled +-- quantifiers. +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A&B) DEFINE A AS val > 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|) DEFINE A AS val > 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*| |B) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*||B) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A||B) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*|B|) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (|A) DEFINE A AS val > 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A*|)) DEFINE A AS val > 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A* *?|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A? *?|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2}*?|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A{2} *?|B) DEFINE A AS val > 0, B AS val <= 0); +-- Doubled op-char quantifiers lex as one Op token and are unsupported, whether +-- glued to '|' ("**|", "*+|", "???|") or on their own ("**"). +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*+|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A???|B) DEFINE A AS val > 0, B AS val <= 0); +SELECT count(*) OVER w FROM rpr_glue WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A**B) DEFINE A AS val > 0); +DROP TABLE rpr_glue; + +-- ============================================================ +-- Error Cases Tests +-- ============================================================ + +DROP TABLE IF EXISTS rpr_err; +CREATE TABLE rpr_err (id INT, val INT); +INSERT INTO rpr_err VALUES (1, 10), (2, 20); + +-- Invalid quantifier syntax +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+!) + DEFINE A AS val > 0 +); + +-- none of the following queries should be accepted +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ !) DEFINE A AS TRUE); +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+ ?+) DEFINE A AS TRUE); +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A* ?+) DEFINE A AS TRUE); +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A? ??) DEFINE A AS TRUE); +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A {1,2}??) DEFINE A AS TRUE); + +-- none of the following 4 range-quantifier queries should be accepted +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2} !) DEFINE A AS TRUE); +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,} !) DEFINE A AS TRUE); +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{,3} !) DEFINE A AS TRUE); +SELECT FROM rpr_err WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A{2,3} !) DEFINE A AS TRUE); + +-- Unmatched parentheses +SET client_min_messages = NOTICE; +DO $$ +BEGIN + EXECUTE 'SELECT COUNT(*) OVER w FROM rpr_err WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A B) DEFINE A AS val > 0, B AS val > 10)'; + RAISE NOTICE 'Unmatched parentheses: UNEXPECTED SUCCESS'; +EXCEPTION + WHEN syntax_error THEN + RAISE NOTICE 'Unmatched parentheses: EXPECTED ERROR - %', SQLERRM; + WHEN OTHERS THEN + RAISE NOTICE 'Unmatched parentheses: UNEXPECTED ERROR - %', SQLERRM; +END $$; +SET client_min_messages = WARNING; + +-- ERROR: empty DEFINE not allowed +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE +); + +-- ERROR: empty PATTERN not allowed +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN () + DEFINE A AS val > 0 +); + +-- ERROR: DEFINE without PATTERN (PATTERN and DEFINE must be used together) +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + DEFINE A AS val > 0 +); + +-- Qualified column references (NOT SUPPORTED) + +-- Pattern variable qualified name: not supported (valid per ISO/IEC 19075-5 6.15 / 4.16, not yet implemented) +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS A.val > 0 +); + +-- PATTERN-only variable qualified name: not supported even without DEFINE entry +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B+) + DEFINE A AS B.val > 0 +); + +-- DEFINE-only variable qualified name: still a pattern variable, not a range variable +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0, B AS B.val > 0 +); + +-- FROM-clause range variable qualified name: not allowed (prohibited by ISO/IEC 19075-5 6.5) +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS rpr_err.val > 0 +); + +-- Unknown qualifier (neither pattern var nor range var): the DEFINE pre-check +-- must fall through so that normal column resolution produces a sensible error. +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS nosuch.val > 0 +); + +-- Unqualified composite field access in DEFINE works: no qualifier means no +-- pattern/range-var navigation, so the pre-check skips and normal resolution +-- handles "(items).amount" via A_Indirection on the current row. +CREATE TYPE rpr_item AS (name TEXT, amount INT); +CREATE TEMP TABLE rpr_composite (id int, items rpr_item); +INSERT INTO rpr_composite VALUES (1, ROW('a',5)), (2, ROW('b',15)), (3, ROW('c',25)); +SELECT id, (items).amount, COUNT(*) OVER w AS cnt +FROM rpr_composite +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE A AS (items).amount > 10 +); +-- Expected: rows where (items).amount > 10 form matches; counts reflect frame size + +-- Composite type field selection (qualified forms): the ColumnRef portion ("A.items" or +-- "rpr_composite.items") is what gets quoted; the trailing ".amount" lives in +-- the surrounding A_Indirection node and is not visible to the pre-check. +SELECT COUNT(*) OVER w +FROM rpr_composite +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS (A.items).amount > 10 +); +SELECT COUNT(*) OVER w +FROM rpr_composite +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS (rpr_composite.items).amount > 10 +); +DROP TABLE rpr_composite; +DROP TYPE rpr_item; + +-- ERROR: undefined column in DEFINE +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS nonexistent_column > 0 +); + +-- ERROR: type mismatch +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 'string' +); + +-- ERROR: aggregate function in DEFINE is not supported +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS COUNT(*) > 0 +); + +-- ERROR: set-returning function in DEFINE is not supported +SELECT FROM rpr_err +WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > generate_series(1 ,2)); + +-- ERROR: window function in DEFINE is not supported +SELECT FROM rpr_err +WINDOW w AS ( ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING PATTERN (A+) DEFINE A AS 1 > row_number() OVER ()); + +-- Subquery in DEFINE is not supported +SELECT COUNT(*) OVER w +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > (SELECT max(val) FROM rpr_err) +); + +-- Pattern variable not used (should work, extra vars ignored) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_err +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0, B AS val > 5, C AS val > 10 +) +ORDER BY id; + +DROP TABLE rpr_err; + +-- NULL handling + +CREATE TABLE rpr_null (id INT, val INT); +INSERT INTO rpr_null VALUES (1, 10), (2, NULL), (3, 30); + +-- NULL in DEFINE expression +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_null +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 15 +) +ORDER BY id; + +-- IS NULL in DEFINE +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_null +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (N+) + DEFINE N AS val IS NULL +) +ORDER BY id; + +-- IS NOT NULL in DEFINE +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_null +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (NN+) + DEFINE NN AS val IS NOT NULL +) +ORDER BY id; + +DROP TABLE rpr_null; + +-- Compound navigation: inner nav must be direct arg (not nested in expression) +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v + FIRST(v)) > 0 +); + +-- FIRST/LAST wrapping FIRST/LAST: prohibited +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS FIRST(FIRST(v)) > 0 +); + +-- Triple nesting: prohibited (3-level deep navigation) +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(FIRST(PREV(v))) > 0 +); + +-- A navigation offset must be a run-time constant, not a navigation operation +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, FIRST(1)) > 0); +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, FIRST(1) + 1) > 0); +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, NEXT(1, 0)) > 0); +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(FIRST(v), LAST(1)) > 0); +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, FIRST(v)) > 0); +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS NEXT(v, PREV(v, 1)) > 0); +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(FIRST(v, LAST(1)), 2) > 0); +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, FIRST(1::bigint)) > 0); + +-- An unknown literal argument resolves to text; it must still reference a column +SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV('foo') = 'bar'); +SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV('foo')); +SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(NULL) IS NULL); +PREPARE rpr_navarg AS SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV($1) IS NULL); + +-- An int2 offset is coerced to int8 like any implicit cast (same as plain 0) +SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, 0::smallint) = v); +SELECT count(*) OVER w +FROM generate_series(1,5) s(v) +WINDOW w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) DEFINE A AS PREV(v, 0) = v); + +-- ============================================================ +-- Window Deduplication Tests +-- ============================================================ + +-- non-RPR and RPR windows with identical base frame are kept separate. +SELECT id, val, + first_value(id) OVER ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + ) AS fv_normal, + first_value(id) OVER w1 AS fv_rpr +FROM (VALUES (1, 10), (2, 20), (3, 30), (4, 40)) AS t(id, val) +WINDOW w1 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 10 +); + +-- ============================================================ +-- Pattern Optimization Tests +-- ============================================================ +-- Tests for pattern optimization +-- Use EXPLAIN to verify optimized pattern (shown as "Pattern: ...") + +CREATE TABLE rpr_plan (id INT, val INT); +INSERT INTO rpr_plan VALUES + (1, 10), (2, 20), (3, 30), (4, 40), (5, 50), + (6, 60), (7, 70), (8, 80), (9, 90), (10, 100); + +-- Consecutive VAR merge: A A A -> a{3} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A A A) DEFINE A AS val > 0); + +-- Consecutive VAR merge: A{2} A{3} -> a{5} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2} A{3}) DEFINE A AS val > 0); + +-- Consecutive VAR merge: A+ A* -> a+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ A*) DEFINE A AS val > 0); + +-- Consecutive VAR merge: A A+ -> a{2,} +-- where a finite prev (A{1,1}) meets an infinite child (A+). +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A A+) DEFINE A AS val > 0); + +-- Consecutive VAR merge at the boundary: A{1073741823,} A{1073741823,} -> +-- a{2147483646,}. The min sum 2147483646 = INT32_MAX - 1 is the largest +-- still-finite bound, so the merge proceeds; a sum of exactly INF instead +-- falls back (see the Optimization Fallback Tests). +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1073741823,} A{1073741823,}) DEFINE A AS val > 0); + +-- Consecutive GROUP merge with finite quantifiers: ((A B){5}) ((A B){10}) -> merged +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){5}) ((A B){10})) DEFINE A AS val <= 50, B AS val > 50); + +-- Consecutive GROUP merge with unbounded: (A B)+ (A B)+ -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ (A B)+) DEFINE A AS val <= 50, B AS val > 50); + +-- Consecutive GROUP merge: (A B){2} (A B)+ -> (a b){3,} +-- Where a finite prev ((A B){2,2}) meets an infinite child ((A B)+). +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2} (A B)+) DEFINE A AS val <= 50, B AS val > 50); + +-- Consecutive GROUP merge at the boundary: (A B){1073741823,} (A B){1073741823,} +-- -> (a b){2147483646,}. The min sum INT32_MAX - 1 is still finite, so the +-- merge proceeds; a sum of exactly INF instead falls back (see the +-- Optimization Fallback Tests). +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){1073741823,} (A B){1073741823,}) DEFINE A AS val <= 50, B AS val > 50); + +-- PREFIX merge: A B (A B)+ -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+) DEFINE A AS val <= 50, B AS val > 50); + +-- PREFIX and SUFFIX merge: A B (A B)+ A B -> (a b){3,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+ A B) DEFINE A AS val <= 40, B AS val > 40); + +-- Flatten nested: A ((B) (C)) -> a b c +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B) (C))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + +-- Data execution: SEQ flatten produces correct results +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A ((B) (C))) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + +-- ALT flatten: (A | (B | C))+ -> (a | b | c)+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | (B | C))+) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + +-- ALT deduplicate: (A | B | A) -> (a | b) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B | A)+) DEFINE A AS val <= 50, B AS val > 50); + +-- Data execution: ALT dedup produces correct results +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | A)+) DEFINE A AS val <= 50, B AS val > 50); + +-- Quantifier multiply: (A{2}){3} -> a{6} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){3}) DEFINE A AS val > 0); + +-- Quantifier NO multiply: reluctant GROUP child (((A B){2}?){3}) stays nested +-- a reluctant quantifier on a GROUP is not subject to multiplication +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){2}?){3}) DEFINE A AS val > 0, B AS val > 0); + +-- Quantifier multiply control: greedy GROUP (((A B){2}){3}) -> (a b){6} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){2}){3}) DEFINE A AS val > 0, B AS val > 0); + +-- Quantifier multiply with child range: (A{2,3}){3} -> a{6,9} +-- outer exact, child range - optimization applies +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3}){3}) DEFINE A AS val > 0); + +-- Quantifier NO multiply: (A{2}){2,3} stays as (a{2}){2,3} +-- outer range - gaps would occur (4,6 not 4,5,6), no optimization +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){2,3}) DEFINE A AS val > 0); + +-- Quantifier NO multiply: (A{2}){2,} stays as (a{2}){2,} +-- outer unbounded - gaps would occur (4,6,8,... not 4,5,6,...), no optimization +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){2,}) DEFINE A AS val > 0); + +-- Quantifier multiply: (A){2,} -> a{2,} +-- child exact 1 - no gaps, optimization applies +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A){2,}) DEFINE A AS val > 0); + +-- Quantifier multiply: (A)+ -> a+ +-- child exact 1 - no gaps, optimization applies +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A)+) DEFINE A AS val > 0); + +-- Quantifier NO multiply: (A{2}){3,5} stays as (a{2}){3,5} +-- outer range, child exact > 1 - gaps would occur (6,8,10 not 6,7,8,9,10) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){3,5}) DEFINE A AS val > 0); + +-- Quantifier multiply: (A{2,3}){2,3} -> a{4,9} +-- outer range, child range: counts [4,6] U [6,9] = [4,9] are contiguous, so it folds +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3}){2,3}) DEFINE A AS val > 0); + +-- Quantifier NO multiply: (A{4,5}){2,3} stays as (a{4,5}){2,3} +-- outer range, child range with a gap: [8,10] U [12,15] misses 11 +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{4,5}){2,3}) DEFINE A AS val > 0); + +-- Nested unbounded: (A*)* -> a* +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A*)*) DEFINE A AS val > 0); + +-- Nested unbounded: (A+)* -> a* +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+)*) DEFINE A AS val > 0); + +-- Nested unbounded: (A+)+ -> a+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+)+) DEFINE A AS val > 0); + +-- Quantifier multiply with an unbounded child: an exact outer count (m == n) +-- always folds regardless of the child's max - (A+){3} -> a{3,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+){3}) DEFINE A AS val > 0); + +-- (A{2,}){3} -> a{6,} (m == n, unbounded child with min 2) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,}){3}) DEFINE A AS val > 0); + +-- (A+){2,4} -> a{2,} (outer range, unbounded child: every interval reaches INF, +-- so they always touch) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+){2,4}) DEFINE A AS val > 0); + +-- (A{2,3}){2,4} -> a{4,12} (outer range x child range, contiguous: +-- [4,6] U [6,9] U [8,12] = [4,12]) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3}){2,4}) DEFINE A AS val > 0); + +-- Skippable outer (min 0) folds only when the zero case connects to the child +-- range: (A{1,3})? -> a{0,3} (child min <= 1, so {0} U [1,3] = [0,3] is contiguous) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{1,3})?) DEFINE A AS val > 0); + +-- Quantifier NO multiply: (A{2,3})? stays as (a{2,3})? +-- min 0 with child min >= 2: {0} U [2,3] leaves 1 unreachable (intervals touch but +-- the zero case does not connect) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2,3})?) DEFINE A AS val > 0); + +-- Quantifier NO multiply: (A{3,4})? stays as (a{3,4})? +-- min 0 with child min >= 2: {0} U [3,4] leaves 1,2 unreachable +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{3,4})?) DEFINE A AS val > 0); + +-- Unwrap GROUP{1,1}: (A) -> a +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A)) DEFINE A AS val > 0); + +-- Unwrap GROUP{1,1}: (A B) -> a b +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)) DEFINE A AS val <= 50, B AS val > 50); + +-- Combined optimization: A A (B B)+ B B C C C -> a{2} (b{2}){2,} c{3} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A A (B B)+ B B C C C) + DEFINE A AS val <= 20, B AS val > 20 AND val <= 70, C AS val > 70); + +-- Consecutive GROUP merge with unbounded: (A+) (A+) -> a{2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+) (A+)) DEFINE A AS val > 0); + +-- Consecutive GROUP merge finite: (A{10}){20} -> a{200} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{10}){20}) DEFINE A AS val > 0); + +-- Different GROUP prevents merge: (A B){2} (C D){3} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2} (C D){3}) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + +-- Different children count prevents merge: (A B)+ (A B C)+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ (A B C)+) + DEFINE A AS val <= 33, B AS val > 33 AND val <= 66, C AS val > 66); + +-- PREFIX only merge: A B (A B)+ -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+) DEFINE A AS val <= 50, B AS val > 50); + +-- SUFFIX only merge: (A B)+ A B -> (a b){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ A B) DEFINE A AS val <= 50, B AS val > 50); + +-- Multiple SUFFIX absorption with skipUntil: (A B)+ A B A B C +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ A B A B C) + DEFINE A AS val <= 50, B AS val > 50 AND val <= 75, C AS val > 75); + +-- PREFIX merge with remaining prefix: A B C D (C D)+ -> A B (C D) {2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C D (C D)+) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + +-- cannot merge, prefix is different +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w +FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +PATTERN (A B C D C (C D) +) +DEFINE A AS val <= 25, B AS val > 25, + C AS val > 50, D AS val > 75); + +-- PREFIX merge with quantifiers: A B* (A B*)+ -> (a b*){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B* (A B*)+) + DEFINE A AS val <= 50, B AS val > 50); + +-- PREFIX merge with multiple quantifiers: A+ B* C? (A+ B* C?)+ -> (a+ b* c?){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B* C? (A+ B* C?)+) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60); + +-- SUFFIX merge with quantifiers: (A B*)+ A B* -> (a b*){2,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B*)+ A B*) + DEFINE A AS val <= 50, B AS val > 50); + +-- Unwrap GROUP{1,1}: ((A | B | C)) -> (a | b | c) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B | C)) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + +-- Data execution: GROUP unwrap produces correct results +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B | C)) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + +-- Reluctant optimization bypass: VAR merge +-- A+? A stays as a+? a (greedy A+ A merges to a{2,}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+? A) DEFINE A AS val > 0); + +-- Reluctant optimization bypass: GROUP merge +-- (A B)+? (A B) stays separate (greedy merges to (a b){2,}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+? (A B)) DEFINE A AS val <= 50, B AS val > 50); + +-- Reluctant optimization bypass: quantifier multiply (outer reluctant) +-- (A{2}){3}? stays as (a{2}){3}? (greedy merges to a{6}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}){3}?) DEFINE A AS val > 0); + +-- Reluctant optimization bypass: quantifier multiply (inner reluctant) +-- (A{2}?){3} stays as (a{2}?){3} (greedy merges to a{6}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2}?){3}) DEFINE A AS val > 0); + +-- Reluctant optimization bypass: PREFIX merge +-- A B (A B)+? stays separate (greedy merges to (a b){2,}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (A B)+?) DEFINE A AS val <= 50, B AS val > 50); + +-- Reluctant optimization bypass: SUFFIX merge +-- (A B)+? A B stays separate (greedy merges to (a b){2,}) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+? A B) DEFINE A AS val <= 50, B AS val > 50); + +-- GROUP unwrap with quantifier propagation: (A)?? B -> a?? b +-- Single VAR child {1,1} receives GROUP's quantifier and reluctant +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A)?? B) DEFINE A AS val <= 50, B AS val > 50); + +-- Reluctant preserved through ALT flatten +-- (A | (B | C))+? flattens to (a | b | c)+? - inner ALT flattened, reluctant kept +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | (B | C))+?) DEFINE A AS val <= 30, B AS val <= 60, C AS val > 60); + +-- Reluctant optimization bypass: absorption flags +-- A+? with SKIP PAST LAST ROW - no absorption markers (greedy A+ gets a+") +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+?) DEFINE A AS val > 0); + +-- Duplicate GROUP removal: ((A | B)+ | (A | B)+) -> (a | b)+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B)+ | (A | B)+) DEFINE A AS val <= 50, B AS val > 50); + +-- Consecutive VAR merge with zero-min: A* A+ -> a+ +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A* A+) DEFINE A AS val > 0); + +-- Consecutive VAR merge (4-element): A A{2} A+ A{3} -> a{7,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A A{2} A+ A{3}) DEFINE A AS val > 0); + +-- PREFIX+SUFFIX merge (5-way): A B A B (A B)+ A B A B -> (a b){5,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B A B (A B)+ A B A B) + DEFINE A AS val <= 50, B AS val > 50); + +-- PREFIX+SUFFIX merge (5-way): B A B (A B)+ A B A B -> b (a b){4,} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B A B (A B)+ A B A B) + DEFINE A AS val <= 50, B AS val > 50); + +-- Unwrap single-item ALT after dedup: (A | A)+ -> a+ +-- ALT dedup reduces to single-item, then GROUP unwrap +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | A)+) DEFINE A AS val > 0); + +-- GROUP{1,1} to SEQ with flatten: ((A B)(C D)) -> a b c d +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B)(C D))) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + +-- Nested ALT pattern: ((A B) | C) D | A B C +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B) | C) D | A B C) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + +-- Nested ALT with unbounded: ((A+ B) | C) D | A B C +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A+ B) | C) D | A B C) + DEFINE A AS val <= 25, B AS val > 25 AND val <= 50, + C AS val > 50 AND val <= 75, D AS val > 75); + +-- ============================================================ +-- Absorption Flag Display Tests +-- ============================================================ +-- Tests absorption marker display in EXPLAIN output +-- Markers: ' = branch element, " = comparison point +-- Files: explain.c (append_rpr_quantifier, deparse_rpr_pattern) + +-- Simple VAR: A+ -> a+" (comparison point) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0); + +-- GROUP unbounded: (A B)+ -> (a' b')+" (branch + comparison) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B)+) DEFINE A AS val <= 50, B AS val > 50); + +-- ALT both absorbable: A+ | B+ -> (a+" | b+") +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B+) DEFINE A AS val <= 50, B AS val > 50); + +-- ALT one absorbable: A+ | B -> (a+" | b) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ | B) DEFINE A AS val <= 50, B AS val > 50); + +-- Sequence with absorbable start: A+ B -> a+" b +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+ B) DEFINE A AS val <= 50, B AS val > 50); + +-- Complex nested: ((A+ B) | C) D | A B C - deeply nested ALT +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (((A+ B) | C) D | A B C) + DEFINE A AS val <= 30, B AS val <= 60, C AS val <= 80, D AS val > 80); + +-- Nested unbounded: (A+ | B)+ -> (a+" | b)+ (first iteration absorbable) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ | B)+) + DEFINE A AS val <= 50, B AS val > 50); + +-- ALT inside unbounded GROUP: (A+ B | A B)* -> (a+" b | a b)* (first iteration absorbable) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A+ B | A B)*) + DEFINE A AS val <= 50, B AS val > 50); + +-- Fixed-length group absorbable: (A{2} B{3})+ -> (a{2}' b{3}'){2,}" +-- All children have min == max, equivalent to unrolling to {1,1} +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A{2} B{3})+) + DEFINE A AS val <= 50, B AS val > 50); + +-- Nested fixed-length group: (A (B C){2} D)+ -> absorbable +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A (B C){2} D)+) + DEFINE A AS val <= 20, B AS val <= 40, C AS val <= 60, D AS val > 60); + +-- Nested fixed-length with inner quantifier: ((A{2} B{3}){2})+ -> absorbable +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (((A{2} B{3}){2})+) + DEFINE A AS val <= 50, B AS val > 50); + +-- Non-absorbable fixed-length: (A B{2,5})+ -> no markers (min != max) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B{2,5})+) + DEFINE A AS val <= 50, B AS val > 50); + +-- Non-absorbable fixed-length: (A B?)+ -> no markers (min != max) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A B?)+) + DEFINE A AS val <= 50, B AS val > 50); + +-- Non-absorbable (unbounded not at start): A B+ -> a b+ (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A B+) DEFINE A AS val <= 50, B AS val > 50); + +-- Non-absorbable (no unbounded branch): (A | B){2,} -> (a | b){2,} (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN ((A | B){2,}) DEFINE A AS val <= 50, B AS val > 50); + +-- Non-absorbable (SKIP TO NEXT ROW): A+ -> a+ (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW PATTERN (A+) DEFINE A AS val > 0); + +-- Non-absorbable (limited frame): A+ -> a+ (no markers) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_plan +WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING + AFTER MATCH SKIP PAST LAST ROW PATTERN (A+) DEFINE A AS val > 0); + +-- Reluctant {1}? quantifier deparse +-- A{1}? is a reluctant {1,1} quantifier. The deparse code must +-- output "{1}" explicitly to disambiguate from a bare "?" quantifier +-- (which would mean {0,1}). +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM rpr_plan +WINDOW w AS ( + ORDER BY val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1}? B) + DEFINE A AS val > 0, B AS val > 0 +); + +-- ============================================================ +-- Absorption Analysis Tests +-- ============================================================ +-- Tests context absorption optimization (O(n^2) -> O(n)) +-- Files: rpr.c (computeAbsorbability) + +-- Simple Absorbable Pattern: A+ B +-- Pattern starts with unbounded VAR + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + +-- Absorbable GROUP Pattern: (A B)+ C +-- Pattern starts with unbounded GROUP + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ C) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 +) +ORDER BY id; + +-- Non-Absorbable: Unbounded Not at Start +-- Pattern: A B+ (unbounded not at start) + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + +-- ALT with Absorbable Branches +-- Pattern: (A+ | B+) C - both branches absorbable + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ | B+) C) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 +) +ORDER BY id; + +-- ALT with Mixed Branches +-- Pattern: (A+ | B C) - only first branch absorbable + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ | B C)+) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 +) +ORDER BY id; + +-- Non-Absorbable: ALT Inside GROUP +-- Pattern: (A | B){2,} - ALT inside unbounded GROUP + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B){2,}) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + +-- Non-Absorbable: Nested Unbounded +-- Pattern: ((A B)+ C)+ - nested GROUP structure + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B)+ C)+) + DEFINE A AS val <= 30, B AS val > 30 AND val <= 60, C AS val > 60 +) +ORDER BY id; + +-- Non-Absorbable: Unbounded Element Inside GROUP +-- Pattern: (A B+){2,} - unbounded inside GROUP + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B+){2,}) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + +-- Runtime Conditions: SKIP TO NEXT ROW +-- Absorption disabled with SKIP TO NEXT ROW + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + +-- Runtime Conditions: Limited Frame +-- Absorption disabled with limited frame end + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 5 FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS val <= 50, B AS val > 50 +) +ORDER BY id; + +-- ALT Non-Absorbable Branch Match: A+ B | C +-- C match on the non-absorbable branch (id=2, id=5) must survive absorption of +-- the dominating A+ run, which never completes a match (B is never present) + +WITH test_nonabsorbable_branch AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A', 'C']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A', 'C']), + (6, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nonabsorbable_branch +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B | C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- ALT Both Branches Absorbable: A+ C | B+ +-- A+ C never completes (C absent) so its A+ run keeps expanding and dominates; +-- a finalized B+ match on the other branch (id=1, id=6) must survive absorption + +WITH test_absorbable_branches AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', 'B']), + (2, ARRAY['A', 'B']), + (3, ARRAY['A', 'B']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['A', 'B']), + (7, ARRAY['A', 'B']), + (8, ARRAY['A', 'B']), + (9, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorbable_branches +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- ============================================================ +-- Edge Case Tests +-- ============================================================ +-- Tests boundary conditions and complex scenarios + +-- Empty Match Prevention +-- Pattern that could match empty: A* + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A*) + DEFINE A AS val > 1000 -- Never matches +) +ORDER BY id; + +-- All Rows Match +-- Pattern where every row matches + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val >= 0 -- Always true +) +ORDER BY id; + +-- Large Quantifiers +-- Pattern: A{100} (large exact quantifier) + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{100}) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Pattern: A{10,20} (large range quantifier) +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{10,20}) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Complex Multi-Level Nesting +-- Pattern: (((A B) | C)+ D)+ + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((A B) | C)+ D)+) + DEFINE A AS val <= 20, B AS val > 20 AND val <= 40, + C AS val > 40 AND val <= 60, D AS val > 60 +) +ORDER BY id; + +-- Long Alternation Chain +-- Pattern: A | B | C | D | E (5-way ALT) + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A | B | C | D | E) + DEFINE A AS val = 10, B AS val = 30, C AS val = 50, + D AS val = 70, E AS val = 90 +) +ORDER BY id; + +-- Long Sequence +-- Pattern: A B C D E F G H (8-element SEQ) + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B C D E F G H) + DEFINE A AS val >= 10, B AS val >= 20, C AS val >= 30, + D AS val >= 40, E AS val >= 50, F AS val >= 60, + G AS val >= 70, H AS val >= 80 +) +ORDER BY id; + +-- Interleaved Quantifiers +-- Pattern: A{2} B+ C{3,5} D* E{1,} + +SELECT id, val, COUNT(*) OVER w as cnt +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{2} B+ C{3,5} D* E{1,}) + DEFINE A AS val > 0, B AS val > 0, C AS val > 0, + D AS val > 0, E AS val > 0 +) +ORDER BY id; + +-- ============================================================ +-- Optimization Fallback Tests +-- ============================================================ +-- Tests for optimization edge cases and fallback behavior + +CREATE TABLE rpr_fallback (id INT, val INT); +INSERT INTO rpr_fallback VALUES (1, 10), (2, 20); + +-- Test: min quantifier overflow causes optimization fallback (min == max case) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2000000000}){2}) + DEFINE A AS val > 0 +); +-- Expected: Fallback - pattern not merged due to min overflow (4000000000 > INT32_MAX) + +-- Test: max-only quantifier overflow causes optimization fallback +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{1,2000000000}){2}) + DEFINE A AS val > 0 +); +-- Expected: Fallback - min OK (2*1=2), but max overflow (2*2000000000 > INT32_MAX) + +-- Test: max quantifier exceeds valid range (2147483647 = INT_MAX, limit is 2147483646) +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2000000000,2147483647}){2}) + DEFINE A AS val > 0 +); + +-- Test: nested unbounded with large min causes overflow fallback +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A{2000000000,}){2000000000,}) + DEFINE A AS val > 0 +); +-- Expected: Fallback - min overflow (2000000000 * 2000000000 > INT32_MAX) + +-- Test: prefix mismatch causes optimization fallback +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (C D)+) + DEFINE A AS val > 0, B AS val > 5, C AS val > 10, D AS val > 15 +); +-- Expected: Fallback - prefix elements don't match GROUP content + +-- Test: consecutive VAR merge whose min sum is exactly INF causes fallback. +-- 1073741824 + 1073741823 = 2147483647 = INT32_MAX = RPR_QUANTITY_INF. +-- Merging would yield a VAR with min == INF, so the merge must fall back and +-- leave the two VARs unmerged (mirrors the multiply path's >= INF guard). +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A{1073741824,} A{1073741823,}) + DEFINE A AS val > 0 +); +-- Expected: Fallback - VARs not merged (min sum 2147483647 == INF) + +-- Test: consecutive GROUP merge whose min sum is exactly INF causes fallback. +EXPLAIN (COSTS OFF) +SELECT COUNT(*) OVER w FROM rpr_fallback +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){1073741824,} (A B){1073741823,}) + DEFINE A AS val > 0, B AS val > 5 +); +-- Expected: Fallback - GROUPs not merged (min sum 2147483647 == INF) + +DROP TABLE rpr_fallback; + +-- ============================================================ +-- Planner Integration Tests +-- ============================================================ +-- Tests full planning pipeline and WindowAgg plan node creation +-- Files: planner.c, createplan.c + +CREATE TABLE rpr_planner (id INT, category VARCHAR(10), val INT); +INSERT INTO rpr_planner VALUES + (1, 'A', 10), (2, 'A', 20), (3, 'A', 30), + (4, 'B', 40), (5, 'B', 50), (6, 'B', 60), + (7, 'C', 70), (8, 'C', 80), (9, 'C', 90); + +-- Multiple Window Functions in Same Query +SELECT id, category, val, + COUNT(*) OVER w1 as cnt1, + COUNT(*) OVER w2 as cnt2 +FROM rpr_planner +WINDOW w1 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +), +w2 AS ( + PARTITION BY category + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B+) + DEFINE B AS val >= 40 +) +ORDER BY id; + +-- Window Function with PARTITION BY + +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_planner +WINDOW w AS ( + PARTITION BY category + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY category, id; + +-- Window Function with Complex ORDER BY + +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_planner +WINDOW w AS ( + ORDER BY category DESC, val ASC + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY category DESC, val ASC; + +-- Named Window Reference + +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_planner +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Inline Window Definition + +SELECT id, category, val, + COUNT(*) OVER ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) as cnt +FROM rpr_planner +ORDER BY id; + +-- Window with Aggregate Functions +SELECT category, + COUNT(*) OVER w as window_cnt, + COUNT(*) as agg_cnt +FROM rpr_planner +WINDOW w AS ( + PARTITION BY category + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +GROUP BY category +ORDER BY category; +-- (GROUP BY after WINDOW clause is not valid SQL syntax) + +-- ============================================================ +-- Subquery and CTE Tests +-- Files: planner.c, prepjointree.c +-- ============================================================ +-- Tests RPR with subqueries and CTEs + +-- RPR in Subquery (FROM clause) + +SELECT * FROM ( + SELECT id, category, val, + COUNT(*) OVER w as cnt + FROM rpr_planner + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) sub +WHERE cnt > 5 +ORDER BY id; + +-- RPR with Subquery in WHERE + +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_planner +WHERE val > (SELECT AVG(val) FROM rpr_planner) +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 50 +) +ORDER BY id; + +-- CTE with RPR + +WITH rpr_cte AS ( + SELECT id, category, val, + COUNT(*) OVER w as cnt + FROM rpr_planner + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) +SELECT * FROM rpr_cte WHERE cnt > 5 ORDER BY id; + +-- Multiple CTE References + +WITH rpr_cte AS ( + SELECT id, category, val, + COUNT(*) OVER w as cnt + FROM rpr_planner + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) +SELECT c1.id, c1.cnt, c2.cnt as cnt2 +FROM rpr_cte c1 +JOIN rpr_cte c2 ON c1.id = c2.id +ORDER BY c1.id; + +-- Nested CTEs + +WITH cte1 AS ( + SELECT id, category, val FROM rpr_planner WHERE val > 30 +), +cte2 AS ( + SELECT id, category, val, + COUNT(*) OVER w as cnt + FROM cte1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) +) +SELECT * FROM cte2 ORDER BY id; + +-- ============================================================ +-- JOIN Tests +-- Files: prepjointree.c, setrefs.c +-- ============================================================ +-- Tests RPR with JOINs and multiple table references + +CREATE TABLE rpr_join1 (id INT, val1 INT); +CREATE TABLE rpr_join2 (id INT, val2 INT); + +INSERT INTO rpr_join1 VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50); +INSERT INTO rpr_join2 VALUES (1, 100), (2, 200), (3, 300), (4, 400), (5, 500); + +-- RPR After INNER JOIN + +SELECT t1.id, t1.val1, t2.val2, + COUNT(*) OVER w as cnt +FROM rpr_join1 t1 +INNER JOIN rpr_join2 t2 ON t1.id = t2.id +WINDOW w AS ( + ORDER BY t1.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val1 + val2 > 100 +) +ORDER BY t1.id; + +-- RPR After LEFT JOIN + +SELECT t1.id, t1.val1, t2.val2, + COUNT(*) OVER w as cnt +FROM rpr_join1 t1 +LEFT JOIN rpr_join2 t2 ON t1.id = t2.id +WINDOW w AS ( + ORDER BY t1.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val1 > 0 +) +ORDER BY t1.id; + +-- RPR with Multiple Tables in DEFINE + +SELECT t1.id, t1.val1, t2.val2, + COUNT(*) OVER w as cnt +FROM rpr_join1 t1 +INNER JOIN rpr_join2 t2 ON t1.id = t2.id +WINDOW w AS ( + ORDER BY t1.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B) + DEFINE A AS val1 > 20, + B AS val2 > 200 +) +ORDER BY t1.id; + +-- RPR After Cross Join + +SELECT t1.id as id1, t2.id as id2, t1.val1, t2.val2, + COUNT(*) OVER w as cnt +FROM rpr_join1 t1 +CROSS JOIN rpr_join2 t2 +WHERE t1.id <= 2 AND t2.id <= 2 +WINDOW w AS ( + ORDER BY t1.id, t2.id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val1 + val2 > 0 +) +ORDER BY t1.id, t2.id; + +-- Self-Join with RPR + +SELECT id, val1, val1_next, + COUNT(*) OVER w as cnt +FROM (SELECT a.id, a.val1, b.val1 as val1_next + FROM rpr_join1 a + INNER JOIN rpr_join1 b ON a.id + 1 = b.id) sub +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (X+) + DEFINE X AS val1 < val1_next +) +ORDER BY id; + +DROP TABLE rpr_join1, rpr_join2; + +-- ============================================================ +-- Complex Expression Tests +-- Files: createplan.c, setrefs.c +-- ============================================================ +-- Tests complex target list expressions + +CREATE TABLE rpr_target (id INT, val INT); +INSERT INTO rpr_target VALUES (1, 10), (2, 20), (3, 30), (4, 40), (5, 50); + +-- Expressions in Target List + +SELECT id, + val * 2 as doubled, + val + 10 as added, + COUNT(*) OVER w as cnt +FROM rpr_target +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- CASE Expression in Target List + +SELECT id, val, + CASE + WHEN val < 30 THEN 'low' + WHEN val < 50 THEN 'medium' + ELSE 'high' + END as category, + COUNT(*) OVER w as cnt +FROM rpr_target +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Subquery in Target List + +SELECT id, val, + (SELECT MAX(val) FROM rpr_target) as max_val, + COUNT(*) OVER w as cnt +FROM rpr_target +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Function Calls in Target List + +SELECT id, val, + COALESCE(val, 0) as coalesced, + ABS(val - 30) as distance, + COUNT(*) OVER w as cnt +FROM rpr_target +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Column Aliases and References + +SELECT id as row_id, + val as value, + COUNT(*) OVER w as cnt +FROM rpr_target +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY row_id; + +DROP TABLE rpr_target; + +-- ============================================================ +-- Set Operations Tests +-- Files: planner.c +-- ============================================================ +-- Tests RPR with UNION, INTERSECT, EXCEPT + +CREATE TABLE rpr_set1 (id INT, val INT); +CREATE TABLE rpr_set2 (id INT, val INT); + +INSERT INTO rpr_set1 VALUES (1, 10), (2, 20), (3, 30); +INSERT INTO rpr_set2 VALUES (2, 20), (3, 30), (4, 40); + +-- UNION with RPR + +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +UNION +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set2 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +ORDER BY id; + +-- UNION ALL with RPR + +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +UNION ALL +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set2 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +ORDER BY id, val; + +-- INTERSECT with RPR + +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +INTERSECT +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set2 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +ORDER BY id; + +-- EXCEPT with RPR + +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set1 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +EXCEPT +(SELECT id, val, COUNT(*) OVER w as cnt + FROM rpr_set2 + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + )) +ORDER BY id; + +DROP TABLE rpr_set1, rpr_set2; + +-- ============================================================ +-- Sorting and Grouping Tests +-- Files: planner.c, createplan.c +-- ============================================================ +-- Tests RPR interaction with sorting and grouping + +CREATE TABLE rpr_sort (id INT, category VARCHAR(10), val INT); +INSERT INTO rpr_sort VALUES + (1, 'A', 30), (2, 'B', 20), (3, 'A', 10), + (4, 'B', 40), (5, 'A', 50), (6, 'B', 60); + +-- RPR with GROUP BY (aggregate in DEFINE -> ERROR before GROUP BY interaction) + +SELECT category, + COUNT(*) as group_cnt, + MAX(val) as max_val, + COUNT(*) OVER w as window_cnt +FROM rpr_sort +GROUP BY category +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS COUNT(*) > 0 +) +ORDER BY category; + +-- RPR with HAVING (same aggregate-in-DEFINE error) + +SELECT category, + COUNT(*) as group_cnt, + COUNT(*) OVER w as window_cnt +FROM rpr_sort +GROUP BY category +HAVING COUNT(*) > 2 +WINDOW w AS ( + ORDER BY category + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS COUNT(*) > 0 +) +ORDER BY category; + +-- RPR with DISTINCT + +SELECT DISTINCT category, + COUNT(*) OVER w as cnt +FROM rpr_sort +WINDOW w AS ( + PARTITION BY category + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY category; + +-- RPR with ORDER BY (different from window ORDER BY) + +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_sort +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY val DESC; + +-- RPR with LIMIT and OFFSET + +SELECT id, category, val, + COUNT(*) OVER w as cnt +FROM rpr_sort +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id +LIMIT 3 OFFSET 1; + +DROP TABLE rpr_sort; + +-- SQL function inlining: $1 in DEFINE must be substituted by +-- substitute_actual_parameters_in_from via query_tree_mutator. +CREATE TABLE rpr_srf_t (v int); +INSERT INTO rpr_srf_t SELECT generate_series(1, 5); + +CREATE FUNCTION rpr_srf_f(threshold int) +RETURNS TABLE (v int, cnt bigint) +LANGUAGE sql STABLE AS $$ + SELECT v::int, count(*) OVER w + FROM rpr_srf_t + WINDOW w AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > $1 + ) +$$; + +SELECT v, cnt FROM rpr_srf_f(3) ORDER BY v; + +DROP TABLE rpr_srf_t; +DROP FUNCTION rpr_srf_f(int); + +DROP TABLE rpr_planner; + +-- ============================================================ +-- Stress Tests +-- ============================================================ +-- Edge cases and stress scenarios + +CREATE TABLE rpr_stress (id INT, val INT); +INSERT INTO rpr_stress SELECT i, i * 10 FROM generate_series(1, 20) i; + +-- Very Long Query with Many Windows +SELECT id, val, + COUNT(*) OVER w1 as cnt1, + COUNT(*) OVER w2 as cnt2, + COUNT(*) OVER w3 as cnt3 +FROM rpr_stress +WINDOW w1 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +), +w2 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (B+) + DEFINE B AS val > 50 +), +w3 AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C+) + DEFINE C AS val > 100 +) +ORDER BY id; + +-- Deeply Nested Subqueries with RPR + +SELECT * FROM ( + SELECT * FROM ( + SELECT * FROM ( + SELECT id, val, + COUNT(*) OVER w as cnt + FROM rpr_stress + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 + ) + ) sub1 + ) sub2 +) sub3 +WHERE cnt > 10 +ORDER BY id; + +-- Complex Expression in DEFINE Clause + +SELECT id, val, + COUNT(*) OVER w as cnt +FROM rpr_stress +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ B) + DEFINE A AS (val % 3 = 0 OR val % 5 = 0), + B AS (val * 2 > 100 AND val / 2 < 100) +) +ORDER BY id; + +-- Window with No Matching Rows + +SELECT id, val, + COUNT(*) OVER w as cnt +FROM rpr_stress +WHERE val > 1000 -- No rows match +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +-- Window on Single Row + +SELECT id, val, + COUNT(*) OVER w as cnt +FROM rpr_stress +WHERE id = 10 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > 0 +) +ORDER BY id; + +DROP TABLE rpr_stress; + +-- ============================================================ +-- Error Limit Tests +-- ============================================================ +-- Tests for error conditions in rpr.c + +CREATE TABLE rpr_errors (id INT, val INT); +INSERT INTO rpr_errors VALUES (1, 10), (2, 20); + +-- Test: DEFINE variable not in PATTERN (error) +SELECT id, val, COUNT(*) OVER w FROM rpr_errors +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A) + DEFINE + B AS TRUE +); +-- Expected: Error - B is not used in PATTERN + +-- Test: 240 variables in PATTERN and DEFINE (boundary - should succeed) +SELECT COUNT(*) OVER w FROM rpr_errors +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 + V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 + V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 + V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 + V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 + V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 + V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 + V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 + V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 + V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 + V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 + V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240) + DEFINE + V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0, + V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0, + V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0, + V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0, + V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0, + V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0, + V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0, + V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0, + V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0, + V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0, + V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0, + V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0, + V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0, + V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0, + V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0, + V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0, + V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0, + V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0, + V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0, + V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0, + V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0, + V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0, + V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0, + V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0 +); +-- Expected: Success - exactly at RPR_VARID_MAX boundary + +-- ERROR: 241 variables in PATTERN, 240 in DEFINE (exceeds limit with implicit TRUE) +SELECT COUNT(*) OVER w FROM rpr_errors +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (V1 V2 V3 V4 V5 V6 V7 V8 V9 V10 V11 V12 V13 V14 V15 V16 V17 V18 V19 V20 + V21 V22 V23 V24 V25 V26 V27 V28 V29 V30 V31 V32 V33 V34 V35 V36 V37 V38 V39 V40 + V41 V42 V43 V44 V45 V46 V47 V48 V49 V50 V51 V52 V53 V54 V55 V56 V57 V58 V59 V60 + V61 V62 V63 V64 V65 V66 V67 V68 V69 V70 V71 V72 V73 V74 V75 V76 V77 V78 V79 V80 + V81 V82 V83 V84 V85 V86 V87 V88 V89 V90 V91 V92 V93 V94 V95 V96 V97 V98 V99 V100 + V101 V102 V103 V104 V105 V106 V107 V108 V109 V110 V111 V112 V113 V114 V115 V116 V117 V118 V119 V120 + V121 V122 V123 V124 V125 V126 V127 V128 V129 V130 V131 V132 V133 V134 V135 V136 V137 V138 V139 V140 + V141 V142 V143 V144 V145 V146 V147 V148 V149 V150 V151 V152 V153 V154 V155 V156 V157 V158 V159 V160 + V161 V162 V163 V164 V165 V166 V167 V168 V169 V170 V171 V172 V173 V174 V175 V176 V177 V178 V179 V180 + V181 V182 V183 V184 V185 V186 V187 V188 V189 V190 V191 V192 V193 V194 V195 V196 V197 V198 V199 V200 + V201 V202 V203 V204 V205 V206 V207 V208 V209 V210 V211 V212 V213 V214 V215 V216 V217 V218 V219 V220 + V221 V222 V223 V224 V225 V226 V227 V228 V229 V230 V231 V232 V233 V234 V235 V236 V237 V238 V239 V240 + V241) + DEFINE + V1 AS val > 0, V2 AS val > 0, V3 AS val > 0, V4 AS val > 0, V5 AS val > 0, V6 AS val > 0, V7 AS val > 0, V8 AS val > 0, V9 AS val > 0, V10 AS val > 0, + V11 AS val > 0, V12 AS val > 0, V13 AS val > 0, V14 AS val > 0, V15 AS val > 0, V16 AS val > 0, V17 AS val > 0, V18 AS val > 0, V19 AS val > 0, V20 AS val > 0, + V21 AS val > 0, V22 AS val > 0, V23 AS val > 0, V24 AS val > 0, V25 AS val > 0, V26 AS val > 0, V27 AS val > 0, V28 AS val > 0, V29 AS val > 0, V30 AS val > 0, + V31 AS val > 0, V32 AS val > 0, V33 AS val > 0, V34 AS val > 0, V35 AS val > 0, V36 AS val > 0, V37 AS val > 0, V38 AS val > 0, V39 AS val > 0, V40 AS val > 0, + V41 AS val > 0, V42 AS val > 0, V43 AS val > 0, V44 AS val > 0, V45 AS val > 0, V46 AS val > 0, V47 AS val > 0, V48 AS val > 0, V49 AS val > 0, V50 AS val > 0, + V51 AS val > 0, V52 AS val > 0, V53 AS val > 0, V54 AS val > 0, V55 AS val > 0, V56 AS val > 0, V57 AS val > 0, V58 AS val > 0, V59 AS val > 0, V60 AS val > 0, + V61 AS val > 0, V62 AS val > 0, V63 AS val > 0, V64 AS val > 0, V65 AS val > 0, V66 AS val > 0, V67 AS val > 0, V68 AS val > 0, V69 AS val > 0, V70 AS val > 0, + V71 AS val > 0, V72 AS val > 0, V73 AS val > 0, V74 AS val > 0, V75 AS val > 0, V76 AS val > 0, V77 AS val > 0, V78 AS val > 0, V79 AS val > 0, V80 AS val > 0, + V81 AS val > 0, V82 AS val > 0, V83 AS val > 0, V84 AS val > 0, V85 AS val > 0, V86 AS val > 0, V87 AS val > 0, V88 AS val > 0, V89 AS val > 0, V90 AS val > 0, + V91 AS val > 0, V92 AS val > 0, V93 AS val > 0, V94 AS val > 0, V95 AS val > 0, V96 AS val > 0, V97 AS val > 0, V98 AS val > 0, V99 AS val > 0, V100 AS val > 0, + V101 AS val > 0, V102 AS val > 0, V103 AS val > 0, V104 AS val > 0, V105 AS val > 0, V106 AS val > 0, V107 AS val > 0, V108 AS val > 0, V109 AS val > 0, V110 AS val > 0, + V111 AS val > 0, V112 AS val > 0, V113 AS val > 0, V114 AS val > 0, V115 AS val > 0, V116 AS val > 0, V117 AS val > 0, V118 AS val > 0, V119 AS val > 0, V120 AS val > 0, + V121 AS val > 0, V122 AS val > 0, V123 AS val > 0, V124 AS val > 0, V125 AS val > 0, V126 AS val > 0, V127 AS val > 0, V128 AS val > 0, V129 AS val > 0, V130 AS val > 0, + V131 AS val > 0, V132 AS val > 0, V133 AS val > 0, V134 AS val > 0, V135 AS val > 0, V136 AS val > 0, V137 AS val > 0, V138 AS val > 0, V139 AS val > 0, V140 AS val > 0, + V141 AS val > 0, V142 AS val > 0, V143 AS val > 0, V144 AS val > 0, V145 AS val > 0, V146 AS val > 0, V147 AS val > 0, V148 AS val > 0, V149 AS val > 0, V150 AS val > 0, + V151 AS val > 0, V152 AS val > 0, V153 AS val > 0, V154 AS val > 0, V155 AS val > 0, V156 AS val > 0, V157 AS val > 0, V158 AS val > 0, V159 AS val > 0, V160 AS val > 0, + V161 AS val > 0, V162 AS val > 0, V163 AS val > 0, V164 AS val > 0, V165 AS val > 0, V166 AS val > 0, V167 AS val > 0, V168 AS val > 0, V169 AS val > 0, V170 AS val > 0, + V171 AS val > 0, V172 AS val > 0, V173 AS val > 0, V174 AS val > 0, V175 AS val > 0, V176 AS val > 0, V177 AS val > 0, V178 AS val > 0, V179 AS val > 0, V180 AS val > 0, + V181 AS val > 0, V182 AS val > 0, V183 AS val > 0, V184 AS val > 0, V185 AS val > 0, V186 AS val > 0, V187 AS val > 0, V188 AS val > 0, V189 AS val > 0, V190 AS val > 0, + V191 AS val > 0, V192 AS val > 0, V193 AS val > 0, V194 AS val > 0, V195 AS val > 0, V196 AS val > 0, V197 AS val > 0, V198 AS val > 0, V199 AS val > 0, V200 AS val > 0, + V201 AS val > 0, V202 AS val > 0, V203 AS val > 0, V204 AS val > 0, V205 AS val > 0, V206 AS val > 0, V207 AS val > 0, V208 AS val > 0, V209 AS val > 0, V210 AS val > 0, + V211 AS val > 0, V212 AS val > 0, V213 AS val > 0, V214 AS val > 0, V215 AS val > 0, V216 AS val > 0, V217 AS val > 0, V218 AS val > 0, V219 AS val > 0, V220 AS val > 0, + V221 AS val > 0, V222 AS val > 0, V223 AS val > 0, V224 AS val > 0, V225 AS val > 0, V226 AS val > 0, V227 AS val > 0, V228 AS val > 0, V229 AS val > 0, V230 AS val > 0, + V231 AS val > 0, V232 AS val > 0, V233 AS val > 0, V234 AS val > 0, V235 AS val > 0, V236 AS val > 0, V237 AS val > 0, V238 AS val > 0, V239 AS val > 0, V240 AS val > 0 +); + +-- Test: Pattern nesting at maximum depth (depth 253) +-- Note: 253 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not +-- subject to quantifier multiplication, so the nesting (and depth 253) is +-- preserved after optimization. +SELECT id, val, COUNT(*) OVER w FROM rpr_errors +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?) + DEFINE A AS val > 0 +); +-- Expected: Should succeed + +-- Test: Pattern nesting depth exceeds maximum (depth 254) +-- Note: 254 nested GROUP{3,7}? quantifiers; reluctant quantifiers are not +-- subject to quantifier multiplication, so the nesting reaches depth 254 and +-- exceeds the limit. +SELECT id, val, COUNT(*) OVER w FROM rpr_errors +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((A{3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?){3,7}?) + DEFINE A AS val > 0 +); + +DROP TABLE rpr_errors; + +-- ============================================================ +-- Basic Pattern Matching +-- ============================================================ + +-- Test: A? (optional, greedy) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A?) + DEFINE A AS val > 50 +); + +-- Test: A{2} (exact count) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2}) + DEFINE A AS val <= 50 +); + +-- Test: A{1,3} (bounded range, greedy) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{1,3}) + DEFINE A AS val <= 50 +); + +-- Test: A | B (simple alternation) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A | B) + DEFINE A AS val <= 30, B AS val > 70 +); + +-- Test: A | B | C (three-way alternation) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A | B | C) + DEFINE A AS val <= 20, B AS val BETWEEN 40 AND 60, C AS val > 80 +); + +-- Test: A B C (concatenation) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60 +); + +-- Test: A B? C (optional middle) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B? C) + DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 60 +); + +-- Test: (A B)+ (grouped quantifier) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+) + DEFINE A AS val <= 50, B AS val > 50 +); + +-- Test: (A | B)+ C (alternation with quantifier) +SELECT id, val, count(*) OVER w AS c +FROM rpr_plan +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B)+ C) + DEFINE A AS val <= 30, B AS val BETWEEN 31 AND 60, C AS val > 80 +); + +-- Test: (A+ | (A | B)+)* - nested alternation inside quantified group +-- Previously caused infinite recursion in alternation handling when the inner +-- BEGIN(+)'s skip jump was followed as an ALT branch pointer. +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM (VALUES + (1, ARRAY['A', 'B']), + (2, ARRAY['B']), + (3, ARRAY['C']) +) AS t(id, flags) +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+ | (A | B)+)*) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- Pathological Patterns +-- ============================================================ +-- These patterns previously caused issues. Now optimized or handled safely. + +-- Test: (A*)* - nested unbounded (optimized to A*) +SELECT v, count(*) OVER w AS c +FROM (SELECT generate_series(1, 5) v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((A*)*) + DEFINE A AS TRUE +); + +-- Test: (A*)+ - inner nullable (optimized to A*) +SELECT v, count(*) OVER w AS c +FROM (SELECT generate_series(1, 5) v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((A*)+) + DEFINE A AS TRUE +); + +-- Test: (A+)* - outer nullable (optimized to A*) +SELECT v, count(*) OVER w AS c +FROM (SELECT generate_series(1, 5) v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((A+)*) + DEFINE A AS TRUE +); + +-- Test: (A+)+ - both require match (optimized to A+) +SELECT v, count(*) OVER w AS c +FROM (SELECT generate_series(1, 5) v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((A+)+) + DEFINE A AS TRUE +); + +-- Test: (((A)*)*)* - triple nested (optimized to A*) +SELECT v, count(*) OVER w AS c +FROM (SELECT generate_series(1, 3) v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN ((((A)*)*)*) + DEFINE A AS TRUE +); + +-- Optional group with alternation: A ((B | C) (D | E))* F? +-- When only A matches, the * group matches 0 times and F? matches 0 times +SELECT id, val, match_len +FROM (SELECT id, val, + COUNT(*) OVER w AS match_len + FROM (VALUES (1, 1), (2, 99)) AS t(id, val) + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A ((B | C) (D | E))* F?) + DEFINE A AS val = 1, + B AS val = 2, C AS val = 3, + D AS val = 4, E AS val = 5, + F AS val = 6 + ) +) s; + +DROP TABLE rpr_plan; +RESET client_min_messages; diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql new file mode 100644 index 00000000000..d300530216a --- /dev/null +++ b/src/test/regress/sql/rpr_explain.sql @@ -0,0 +1,3635 @@ +-- ============================================================ +-- RPR EXPLAIN Tests +-- Tests for Row Pattern Recognition EXPLAIN output +-- ============================================================ +-- +-- Views and tables in this file are intentionally not dropped, +-- so that pg_upgrade/pg_dump can test RPR syntax serialization. +-- +-- This test suite validates EXPLAIN output for RPR queries, +-- including NFA statistics shown in EXPLAIN ANALYZE: +-- - NFA States: peak, total, merged +-- - NFA Contexts: peak, total, pruned +-- - NFA: matched (len min/max/avg), mismatched (len min/max/avg) +-- - NFA: absorbed (len min/max/avg), skipped (len min/max/avg) +-- - Pattern deparse formatting +-- - Multiple output formats (text, JSON, XML) +-- +-- Test Coverage: +-- Basic NFA Statistics Tests +-- State Statistics Tests +-- Context Statistics Tests +-- Match Length Statistics Tests +-- Mismatch Length Statistics Tests +-- JSON Format Tests +-- XML Format Tests +-- Multiple Partitions Tests +-- Edge Cases +-- Complex Pattern Tests +-- Real-world Pattern Examples +-- Performance-oriented Tests +-- INITIAL vs no INITIAL comparison +-- Quantifier Variations +-- Regression Tests for Statistics Accuracy +-- Alternation Pattern Tests +-- Group Pattern Tests +-- Window Function Combinations +-- DEFINE Expression Variations +-- Large Scale Statistics Verification +-- Nav Mark Lookback/Lookahead (tuplestore trim) +-- ============================================================ + +-- Filter function to normalize platform-dependent memory values (not NFA statistics). +-- NFA statistics should not change between platforms; if they do, it could +-- indicate issues such as uninitialized memory access. +-- Works for text, JSON, and XML formats. +create function rpr_explain_filter(text) returns setof text +language plpgsql as +$$ +declare + ln text; +begin + for ln in execute $1 + loop + -- Normalize platform-dependent memory values + -- Keep NFA statistics numbers unchanged (they are test assertions) + + -- Text format: "Storage: Memory Maximum Storage: 18kB" + if ln ~ 'Storage:.*Maximum Storage:' then + ln := regexp_replace(ln, '\m\d+kB', 'NkB', 'g'); + end if; + + -- JSON format: "Maximum Storage": 17 (number in kB units) + if ln ~ '"Maximum Storage":' then + ln := regexp_replace(ln, '"Maximum Storage": \d+', '"Maximum Storage": 0', 'g'); + end if; + + -- XML format: 17 (number in kB units) + if ln ~ '' then + ln := regexp_replace(ln, '\d+', '0', 'g'); + end if; + + -- Sort Method memory is platform-dependent (32-bit vs 64-bit) + if ln ~ 'Sort Method:.*Memory:' then + ln := regexp_replace(ln, 'Memory: \d+kB', 'Memory: NkB'); + end if; + + return next ln; + end loop; +end; +$$; + +-- Setup: Create test tables +CREATE TABLE rpr_nfa_test ( + id serial, + v int, + cat char(1) +); + +-- Insert test data: 100 rows with predictable pattern +INSERT INTO rpr_nfa_test (v, cat) +SELECT i, + CASE + WHEN i % 5 = 1 THEN 'A' + WHEN i % 5 = 2 THEN 'B' + WHEN i % 5 = 3 THEN 'C' + WHEN i % 5 = 4 THEN 'D' + ELSE 'E' + END +FROM generate_series(1, 100) i; + +-- Additional test table with more complex patterns +CREATE TABLE rpr_nfa_complex ( + id serial, + price int, + trend char(1) -- U=up, D=down, S=stable +); + +INSERT INTO rpr_nfa_complex (price, trend) +VALUES + (100, 'S'), (105, 'U'), (110, 'U'), (108, 'D'), (112, 'U'), + (115, 'U'), (113, 'D'), (111, 'D'), (109, 'D'), (110, 'U'), + (120, 'U'), (125, 'U'), (130, 'U'), (128, 'D'), (126, 'D'), + (124, 'D'), (122, 'D'), (120, 'D'), (118, 'D'), (119, 'U'), + (121, 'U'), (123, 'U'), (125, 'U'), (127, 'U'), (129, 'U'), + (131, 'U'), (133, 'U'), (130, 'D'), (127, 'D'), (124, 'D'); + +-- ============================================================ +-- Basic NFA Statistics Tests +-- ============================================================ + +-- Simple pattern - should show basic statistics +CREATE VIEW rpr_ev_basic_simple AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS cat = 'A', B AS cat = 'B' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_simple'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS cat = ''A'', B AS cat = ''B'' +)'); + +-- Pattern with no matches - 0 matched +CREATE VIEW rpr_ev_basic_nomatch AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (X Y Z) + DEFINE X AS cat = 'X', Y AS cat = 'Y', Z AS cat = 'Z' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_nomatch'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (X Y Z) + DEFINE X AS cat = ''X'', Y AS cat = ''Y'', Z AS cat = ''Z'' +);'); + +-- Pattern matching every row - high match count +CREATE VIEW rpr_ev_basic_allrows AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (R) + DEFINE R AS TRUE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_allrows'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (R) + DEFINE R AS TRUE +);'); + +-- Regression test: Space before parenthesis in pattern deparse +-- Verifies that "A (B | C)" correctly outputs as "a (b | c)" with space +CREATE VIEW rpr_ev_basic_deparse_space AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B | C)) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_space'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B | C)) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + +-- Regression test: Sequential alternations at same depth +-- Verifies that "((B | C) (D | E))" correctly outputs as "(b | c) (d | e)" +-- Previously failed due to missing parentheses on ALT depth decrease +CREATE VIEW rpr_ev_basic_deparse_seqalt AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B | C) (D | E))*) + DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_basic_deparse_seqalt'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B | C) (D | E))*) + DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0 +);'); + +-- Regression test: Quoted identifiers in EXPLAIN pattern deparse +-- Mixed case names must be quoted to preserve round-trip safety +SELECT rpr_explain_filter(' +EXPLAIN (COSTS OFF) +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ("Start" "Up"+) + DEFINE "Start" AS TRUE, "Up" AS v > PREV(v) +);'); + +-- ============================================================ +-- State Statistics Tests (peak, total, merged) +-- ============================================================ + +-- Simple quantifier pattern - A+ with short matches (no merging) +CREATE VIEW rpr_ev_state_simple_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v % 2 = 1 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_simple_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v % 2 = 1 +);'); + +-- Alternation pattern - multiple state branches +CREATE VIEW rpr_ev_state_alt AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C) (D | E)) + DEFINE + A AS cat = 'A', B AS cat = 'B', C AS cat = 'C', + D AS cat = 'D', E AS cat = 'E' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C) (D | E)) + DEFINE + A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'', + D AS cat = ''D'', E AS cat = ''E'' +);'); + +-- Complex pattern with high state count +CREATE VIEW rpr_ev_state_complex AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B* C+) + DEFINE + A AS v % 3 = 1, + B AS v % 3 = 2, + C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_complex'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B* C+) + DEFINE + A AS v % 3 = 1, + B AS v % 3 = 2, + C AS v % 3 = 0 +);'); + +-- Grouped pattern with quantifier - state count with grouping +CREATE VIEW rpr_ev_state_group_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_group_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + +-- State explosion pattern - many alternations +-- Pattern (A|B)(A|B)(A|B)(A|B) can create many parallel states +CREATE VIEW rpr_ev_state_explosion AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B)) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_explosion'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B)) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + +-- Consecutive ALT merge followed by different ALT +-- ((A | B) (A | B) (C | D)) -> (A|B){2} (C|D) +CREATE VIEW rpr_ev_state_alt_merge_alt AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (C | D)) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (C | D)) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Consecutive ALT merge followed by non-ALT element +-- ((A | B) (A | B) C) -> (A|B){2} C +CREATE VIEW rpr_ev_state_alt_merge_nonalt AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_merge_nonalt'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +);'); + +-- ALT prefix/suffix absorbed into GROUP: (A|B) (A|B)+ (A|B) -> (A|B){3,} +CREATE VIEW rpr_ev_state_alt_absorb_group AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B)+ (A | B)) + DEFINE A AS v % 2 = 0, B AS v % 2 = 1 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B)+ (A | B)) + DEFINE A AS v % 2 = 0, B AS v % 2 = 1 +);'); + +-- High state count - alternation with plus quantifier +CREATE VIEW rpr_ev_state_alt_plus AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C)+ D) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_plus'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C)+ D) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3, D AS v % 4 = 0 +);'); + +-- Early termination: first ALT branch (A) reaches FIN immediately, +-- pruning second branch (A B+) before it can accumulate B repetitions. +CREATE VIEW rpr_ev_state_alt_prune AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | A B)+) + DEFINE A AS v = 1, B AS v > 1 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_alt_prune'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | A B)+) + DEFINE A AS v = 1, B AS v > 1 +);'); + +-- Nested quantifiers causing state growth +CREATE VIEW rpr_ev_state_nested_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B)+)+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_state_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B)+)+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2 +);'); + +-- (A{2,})* must NOT flatten to a* (H-1): counts {0} UNION [2, INF) leave 1 +-- unreachable. The planner keeps it as (a{2,})*, not a*. +CREATE VIEW rpr_ev_nested_quant_no_flatten AS +SELECT count(*) OVER w +FROM generate_series(1, 6) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A{2,})*) + DEFINE A AS v % 3 <> 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nested_quant_no_flatten'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 6) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A{2,})*) + DEFINE A AS v % 3 <> 0 +);'); + +-- ============================================================ +-- Context Statistics Tests (peak, total, pruned + absorbed/skipped) +-- ============================================================ + +-- Context absorption with unbounded quantifier at start +CREATE VIEW rpr_ev_ctx_absorb_unbounded AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_unbounded'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- Bare unbounded quantifier: A+ absorbs redundant contexts +-- min=1 commits no match until the run ends, so newer contexts absorb in-progress +CREATE VIEW rpr_ev_ctx_absorb_plus AS +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v > 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_plus'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v > 0 +);'); + +-- Bare min=0 quantifier: A* is skipped, not absorbed +-- min=0 commits an empty match at creation, so SKIP (not absorption) removes them +CREATE VIEW rpr_ev_ctx_absorb_star AS +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A*) + DEFINE A AS v > 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_star'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A*) + DEFINE A AS v > 0 +);'); + +-- No absorption - bounded quantifier +CREATE VIEW rpr_ev_ctx_no_absorb AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- Contexts skipped by SKIP PAST LAST ROW +CREATE VIEW rpr_ev_ctx_skip AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_skip'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v % 10 = 1, B AS v % 10 = 2, C AS v % 10 = 3 +);'); + +-- High context absorption - unbounded group +CREATE VIEW rpr_ev_ctx_absorb_group AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_group'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + +-- Fixed-length group absorption: (A B B)+ C +-- B B merged to B{2}; absorbable with fixed-length check +-- step_size=3 (A + B + B); v % 7 cycle gives 2 iterations per match +CREATE VIEW rpr_ev_ctx_absorb_fixedvar AS +SELECT count(*) OVER w +FROM generate_series(1, 70) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B B)+ C) + DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_fixedvar'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 70) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B B)+ C) + DEFINE A AS v % 7 IN (1, 4), B AS v % 7 IN (2, 3, 5, 6), C AS v % 7 = 0 +);'); + +-- Nested fixed-length group absorption: (A (B C){2} D)+ E +-- step_size = 1 + (1+1)*2 + 1 = 6; v % 13 cycle gives 2 iterations + E +CREATE VIEW rpr_ev_ctx_absorb_nested AS +SELECT count(*) OVER w +FROM generate_series(1, 65) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A (B C){2} D)+ E) + DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10), + C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12), + E AS v % 13 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_nested'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 65) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A (B C){2} D)+ E) + DEFINE A AS v % 13 IN (1, 7), B AS v % 13 IN (2, 4, 8, 10), + C AS v % 13 IN (3, 5, 9, 11), D AS v % 13 IN (6, 12), + E AS v % 13 = 0 +);'); + +-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F +-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; v % 41 cycle gives 2 iterations + F +CREATE VIEW rpr_ev_ctx_absorb_deep AS +SELECT count(*) OVER w +FROM generate_series(1, 82) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A ((B C C C){2} D){2} E)+ F) + DEFINE A AS v % 41 IN (1, 21), + B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35), + C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18, + 23,24,25, 27,28,29, 32,33,34, 36,37,38), + D AS v % 41 IN (10, 19, 30, 39), + E AS v % 41 IN (20, 40), + F AS v % 41 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_deep'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 82) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A ((B C C C){2} D){2} E)+ F) + DEFINE A AS v % 41 IN (1, 21), + B AS v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35), + C AS v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18, + 23,24,25, 27,28,29, 32,33,34, 36,37,38), + D AS v % 41 IN (10, 19, 30, 39), + E AS v % 41 IN (20, 40), + F AS v % 41 = 0 +);'); + +-- 3-level END chain absorption: ((A (B C){2}){2})+ +-- step_size = (1 + (1+1)*2) * 2 = 10; v % 21 cycle gives 2 iterations +-- END chain: END(BC{2}) -> END(A..{2}) -> END(+, ABSORBABLE) +CREATE VIEW rpr_ev_ctx_absorb_endchain AS +SELECT count(*) OVER w +FROM generate_series(1, 42) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A (B C){2}){2})+) + DEFINE A AS v % 21 IN (1, 6, 11, 16), + B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19), + C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_endchain'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 42) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A (B C){2}){2})+) + DEFINE A AS v % 21 IN (1, 6, 11, 16), + B AS v % 21 IN (2, 4, 7, 9, 12, 14, 17, 19), + C AS v % 21 IN (3, 5, 8, 10, 13, 15, 18, 20) +);'); + +-- No absorption when DEFINE uses FIRST (match_start-dependent) +-- Same pattern as rpr_ev_ctx_absorb_unbounded but with FIRST in DEFINE. +-- Compare: absorbed count should be 0 here vs >0 above. +CREATE VIEW rpr_ev_ctx_no_absorb_first AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_no_absorb_first'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND v > FIRST(v) +);'); + +-- Absorption preserved when DEFINE uses only LAST without offset +-- LAST(v) is match_start-independent (always currentpos), so absorption +-- remains active. Compare: absorbed count should be >0, like the +-- PREV-only case above. +CREATE VIEW rpr_ev_ctx_absorb_last AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS LAST(v) % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_last'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS LAST(v) % 5 = 0 +);'); + +-- No absorption with compound PREV(FIRST()) (match_start-dependent) +CREATE VIEW rpr_ev_ctx_no_absorb_compound AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL +); +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 AND PREV(FIRST(v), 1) IS NOT NULL +);'); + +-- Alternation, non-absorbable branch match survives absorption: A+ B | C +-- The dominating A+ run absorbs redundant contexts, but the recorded C matches +-- are not absorbable, so they survive (2 matched, not 0) +CREATE VIEW rpr_ev_ctx_absorb_alt_nonabsorb AS +WITH d(id, flags) AS ( + VALUES (1, ARRAY['A']), (2, ARRAY['A', 'C']), (3, ARRAY['A']), + (4, ARRAY['A']), (5, ARRAY['A', 'C']), (6, ARRAY['A'])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B | C) + DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags), C AS 'C' = ANY(flags) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_alt_nonabsorb'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +WITH d(id, flags) AS ( + VALUES (1, ARRAY[''A'']), (2, ARRAY[''A'', ''C'']), (3, ARRAY[''A'']), + (4, ARRAY[''A'']), (5, ARRAY[''A'', ''C'']), (6, ARRAY[''A''])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B | C) + DEFINE A AS ''A'' = ANY(flags), B AS ''B'' = ANY(flags), C AS ''C'' = ANY(flags) +);'); + +-- Alternation, both branches absorbable: A+ C | B+ +-- A+ C never completes (C absent) so its A+ run absorbs redundant contexts; the +-- finalized B+ matches on the other branch survive (2 matched, not 0) +CREATE VIEW rpr_ev_ctx_absorb_alt_both AS +WITH d(id, flags) AS ( + VALUES (1, ARRAY['A', 'B']), (2, ARRAY['A', 'B']), (3, ARRAY['A', 'B']), + (4, ARRAY['A']), (5, ARRAY['A']), (6, ARRAY['A', 'B']), + (7, ARRAY['A', 'B']), (8, ARRAY['A', 'B']), (9, ARRAY['A'])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags), C AS 'C' = ANY(flags) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_ctx_absorb_alt_both'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +WITH d(id, flags) AS ( + VALUES (1, ARRAY[''A'', ''B'']), (2, ARRAY[''A'', ''B'']), (3, ARRAY[''A'', ''B'']), + (4, ARRAY[''A'']), (5, ARRAY[''A'']), (6, ARRAY[''A'', ''B'']), + (7, ARRAY[''A'', ''B'']), (8, ARRAY[''A'', ''B'']), (9, ARRAY[''A''])) +SELECT id, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM d +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ C | B+) + DEFINE A AS ''A'' = ANY(flags), B AS ''B'' = ANY(flags), C AS ''C'' = ANY(flags) +);'); + +-- ============================================================ +-- Match Length Statistics Tests +-- ============================================================ + +-- Fixed length matches - all same length +CREATE VIEW rpr_ev_mlen_fixed AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E) + DEFINE + A AS cat = 'A', B AS cat = 'B', C AS cat = 'C', + D AS cat = 'D', E AS cat = 'E' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_fixed'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E) + DEFINE + A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'', + D AS cat = ''D'', E AS cat = ''E'' +);'); + +-- Variable length matches - min/max/avg differ +CREATE VIEW rpr_ev_mlen_variable AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_variable'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +);'); + +-- Very long matches +CREATE VIEW rpr_ev_mlen_long AS +SELECT count(*) OVER w +FROM generate_series(1, 200) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v <= 195, B AS v > 195 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 200) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v <= 195, B AS v > 195 +);'); + +-- Uniform match length with mismatches from gap rows (v%20 = 11..15) +CREATE VIEW rpr_ev_mlen_with_mismatch AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15), + B AS v % 20 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_with_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS (v % 20 <> 0) AND (v % 20 <= 10 OR v % 20 > 15), + B AS v % 20 = 0 +);'); + +-- ============================================================ +-- Mismatch Length Statistics Tests +-- ============================================================ + +-- Pattern with complete match every cycle: 0 mismatched +-- A(1,2,3) B(4,5) C(6) repeats perfectly; X rows are pruned, not mismatched +CREATE VIEW rpr_ev_mlen_no_mismatch AS +SELECT count(*) OVER w +FROM ( + SELECT v, + CASE WHEN v % 10 IN (1,2,3) THEN 'A' + WHEN v % 10 IN (4,5) THEN 'B' + WHEN v % 10 = 6 THEN 'C' + ELSE 'X' END AS cat + FROM generate_series(1, 100) AS s(v) +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+ C) + DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_no_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM ( + SELECT v, + CASE WHEN v % 10 IN (1,2,3) THEN ''A'' + WHEN v % 10 IN (4,5) THEN ''B'' + WHEN v % 10 = 6 THEN ''C'' + ELSE ''X'' END AS cat + FROM generate_series(1, 100) AS s(v) +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+ C) + DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'' +);'); + +-- Long partial matches that fail +CREATE VIEW rpr_ev_mlen_long_partial AS +SELECT count(*) OVER w +FROM ( + SELECT i AS v, + CASE + WHEN i <= 20 THEN 'A' + WHEN i <= 25 THEN 'B' + WHEN i = 26 THEN 'X' -- breaks the pattern + WHEN i <= 50 THEN 'A' + WHEN i <= 55 THEN 'B' + WHEN i = 56 THEN 'C' -- completes pattern + ELSE 'Y' + END AS cat + FROM generate_series(1, 60) i +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+ C) + DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_mlen_long_partial'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM ( + SELECT i AS v, + CASE + WHEN i <= 20 THEN ''A'' + WHEN i <= 25 THEN ''B'' + WHEN i = 26 THEN ''X'' -- breaks the pattern + WHEN i <= 50 THEN ''A'' + WHEN i <= 55 THEN ''B'' + WHEN i = 56 THEN ''C'' -- completes pattern + ELSE ''Y'' + END AS cat + FROM generate_series(1, 60) i +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+ C) + DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'' +);'); + +-- ============================================================ +-- JSON Format Tests +-- ============================================================ + +-- JSON format output with all statistics +CREATE VIEW rpr_ev_json_basic AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_basic'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2 +)'); + +-- JSON format with match length statistics +CREATE VIEW rpr_ev_json_matchlen AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +)'); + +-- JSON format with mismatch statistics +-- Pattern A B C expects 1,2,3 but gets 1,2,4 twice causing mismatches +CREATE VIEW rpr_ev_json_mismatch AS +SELECT count(*) OVER w +FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v = 1, B AS v = 2, C AS v = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_mismatch'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON) +SELECT count(*) OVER w +FROM (VALUES (1),(2),(4), (1),(2),(4), (1),(2),(3)) AS t(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v = 1, B AS v = 2, C AS v = 3 +)'); + +-- JSON format with skipped context statistics +-- Alternation pattern with SKIP PAST LAST ROW causes many contexts to be skipped +CREATE VIEW rpr_ev_json_skip AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B)) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_json_skip'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT JSON) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B) (A | B)) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +)'); + +-- ============================================================ +-- XML Format Tests +-- ============================================================ + +-- XML format output +CREATE VIEW rpr_ev_xml_basic AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_xml_basic'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF, FORMAT XML) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +)'); + +-- ============================================================ +-- Multiple Partitions Tests +-- ============================================================ + +-- Statistics across multiple partitions +CREATE VIEW rpr_ev_part_multi AS +SELECT count(*) OVER w +FROM ( + SELECT p, v + FROM generate_series(1, 3) p, + generate_series(1, 30) v +) t +WINDOW w AS ( + PARTITION BY p + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_multi'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM ( + SELECT p, v + FROM generate_series(1, 3) p, + generate_series(1, 30) v +) t +WINDOW w AS ( + PARTITION BY p + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- Different pattern behavior per partition +CREATE VIEW rpr_ev_part_diff AS +SELECT count(*) OVER w +FROM ( + SELECT + CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p, + v % 10 AS val + FROM generate_series(1, 50) v +) t +WINDOW w AS ( + PARTITION BY p + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS val < 5, B AS val >= 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_part_diff'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM ( + SELECT + CASE WHEN v <= 25 THEN 1 ELSE 2 END AS p, + v % 10 AS val + FROM generate_series(1, 50) v +) t +WINDOW w AS ( + PARTITION BY p + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS val < 5, B AS val >= 5 +);'); + +-- ============================================================ +-- Edge Cases +-- ============================================================ + +-- Empty result set +CREATE VIEW rpr_ev_edge_empty AS +SELECT count(*) OVER w +FROM generate_series(1, 0) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v = 1, B AS v = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 0) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v = 1, B AS v = 2 +);'); + +-- Empty matches (length 0): mirror the test_728_* cases in rpr_nfa.sql. +-- Window aggregates over a length-0 frame return 0 / NULL, so the SELECT +-- result alone cannot distinguish "no match" from "empty match"; the +-- "NFA: N matched (len 0/0/0.0)" line in EXPLAIN is the only observable +-- proof that the empty matches were found. + +-- (A?){0,3}: min=0, A never matches -> 3 length-0 matches +CREATE VIEW rpr_ev_edge_empty_match_min0 AS +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){0,3}) + DEFINE A AS FALSE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min0'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){0,3}) + DEFINE A AS FALSE +);'); + +-- (A?){1,3}: min=1, one empty iteration satisfies min -> 3 length-0 matches +CREATE VIEW rpr_ev_edge_empty_match_min1 AS +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){1,3}) + DEFINE A AS FALSE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min1'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){1,3}) + DEFINE A AS FALSE +);'); + +-- (A?){2,3}: min=2 (ISO/IEC 19075-5 7.2.8 STR06 = STRE STRE) -> 3 length-0 matches +CREATE VIEW rpr_ev_edge_empty_match_min2 AS +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE A AS FALSE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_min2'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE A AS FALSE +);'); + +-- (A?){2,3} mixed: rows 1-2 match A (real), rows 3-4 fall back to empty +CREATE VIEW rpr_ev_edge_empty_match_mixed AS +SELECT count(*) OVER w +FROM generate_series(1, 4) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE A AS v <= 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_mixed'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 4) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE A AS v <= 2 +);'); + +-- (A? B?){2,3}: pure empty multi-element body -> 3 length-0 matches +CREATE VIEW rpr_ev_edge_empty_match_multi AS +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){2,3}) + DEFINE A AS FALSE, B AS FALSE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_empty_match_multi'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 3) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){2,3}) + DEFINE A AS FALSE, B AS FALSE +);'); + +-- Single row +CREATE VIEW rpr_ev_edge_single_row AS +SELECT count(*) OVER w +FROM generate_series(1, 1) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A) + DEFINE A AS TRUE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_row'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 1) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A) + DEFINE A AS TRUE +);'); + +-- Pattern longer than data +CREATE VIEW rpr_ev_edge_pattern_longer AS +SELECT count(*) OVER w +FROM generate_series(1, 5) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E F G H I J) + DEFINE + A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5, + F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_pattern_longer'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 5) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E F G H I J) + DEFINE + A AS v = 1, B AS v = 2, C AS v = 3, D AS v = 4, E AS v = 5, + F AS v = 6, G AS v = 7, H AS v = 8, I AS v = 9, J AS v = 10 +);'); + +-- All rows match as single match +CREATE VIEW rpr_ev_edge_single_match AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS TRUE +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_edge_single_match'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS TRUE +);'); + +-- ============================================================ +-- Complex Pattern Tests +-- ============================================================ + +-- Nested groups +CREATE VIEW rpr_ev_cpx_nested AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B) C)+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_nested'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B) C)+) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + +-- Multiple alternations +CREATE VIEW rpr_ev_cpx_multi_alt AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (C | D | E)) + DEFINE + A AS cat = 'A', B AS cat = 'B', C AS cat = 'C', + D AS cat = 'D', E AS cat = 'E' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_multi_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (C | D | E)) + DEFINE + A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'', + D AS cat = ''D'', E AS cat = ''E'' +);'); + +-- Optional elements +CREATE VIEW rpr_ev_cpx_optional AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B? C) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_optional'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B? C) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3 +);'); + +-- Bounded quantifiers +CREATE VIEW rpr_ev_cpx_bounded AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,5} B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,5} B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +);'); + +-- Star quantifier +CREATE VIEW rpr_ev_cpx_star AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B* C) + DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_cpx_star'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B* C) + DEFINE A AS v % 10 = 1, B AS v % 10 IN (2,3,4,5,6,7,8), C AS v % 10 = 9 +);'); + +-- ============================================================ +-- Real-world Pattern Examples +-- ============================================================ + +-- Stock price pattern - V-shape (down then up) +CREATE VIEW rpr_ev_real_vshape AS +SELECT count(*) OVER w +FROM rpr_nfa_complex +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (D+ U+) + DEFINE D AS trend = 'D', U AS trend = 'U' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_vshape'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_complex +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (D+ U+) + DEFINE D AS trend = ''D'', U AS trend = ''U'' +);'); + +-- Stock price pattern - peak (up, stable, down) +CREATE VIEW rpr_ev_real_peak AS +SELECT count(*) OVER w +FROM rpr_nfa_complex +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (U+ S* D+) + DEFINE U AS trend = 'U', S AS trend = 'S', D AS trend = 'D' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_peak'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_complex +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (U+ S* D+) + DEFINE U AS trend = ''U'', S AS trend = ''S'', D AS trend = ''D'' +);'); + +-- Consecutive increasing values (using PREV) +CREATE VIEW rpr_ev_real_increasing AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3,}) + DEFINE A AS v > PREV(v) OR PREV(v) IS NULL +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_real_increasing'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3,}) + DEFINE A AS v > PREV(v) OR PREV(v) IS NULL +);'); + +-- ============================================================ +-- Performance-oriented Tests +-- ============================================================ + +-- Large dataset with simple pattern +CREATE VIEW rpr_ev_perf_large_simple AS +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_simple'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + +-- Large dataset with absorption +CREATE VIEW rpr_ev_perf_large_absorb AS +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 100 <> 0, B AS v % 100 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_large_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 1000) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 100 <> 0, B AS v % 100 = 0 +);'); + +-- High state merge ratio +CREATE VIEW rpr_ev_perf_high_merge AS +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_perf_high_merge'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + +-- ============================================================ +-- INITIAL vs no INITIAL comparison +-- ============================================================ + +-- With INITIAL keyword +CREATE VIEW rpr_ev_initial_with AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_with'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + INITIAL + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- Without INITIAL keyword (same behavior currently) +CREATE VIEW rpr_ev_initial_without AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_initial_without'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- ============================================================ +-- Quantifier Variations +-- ============================================================ + +-- Plus quantifier +CREATE VIEW rpr_ev_quant_plus AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v % 4 <> 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_plus'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE A AS v % 4 <> 0 +);'); + +-- Star quantifier (zero or more) +CREATE VIEW rpr_ev_quant_star AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A* B) + DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_star'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A* B) + DEFINE A AS v % 4 IN (1, 2), B AS v % 4 = 3 +);'); + +-- Question mark (zero or one) +CREATE VIEW rpr_ev_quant_question AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A? B C) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_question'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A? B C) + DEFINE A AS v % 4 = 1, B AS v % 4 = 2, C AS v % 4 = 3 +);'); + +-- Exact count {n} +CREATE VIEW rpr_ev_quant_exact AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_exact'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- Range {n,m} +CREATE VIEW rpr_ev_quant_range AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_range'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4} B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- At least {n,} +CREATE VIEW rpr_ev_quant_atleast AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3,} B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_quant_atleast'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3,} B) + DEFINE A AS v % 10 <> 0, B AS v % 10 = 0 +);'); + +-- ============================================================ +-- Regression Tests for Statistics Accuracy +-- ============================================================ + +-- Verify state count accuracy +-- Pattern A+ B with 20 rows should show predictable state behavior +CREATE VIEW rpr_ev_reg_state_count AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_state_count'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- Verify context count with known absorption +CREATE VIEW rpr_ev_reg_ctx_absorb AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B C) + DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_ctx_absorb'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B C) + DEFINE A AS v % 10 IN (1,2,3,4,5,6,7), B AS v % 10 = 8, C AS v % 10 = 9 +);'); + +-- Verify match length with fixed-length pattern +CREATE VIEW rpr_ev_reg_matchlen AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_reg_matchlen'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + +-- ============================================================ +-- Alternation Pattern Tests +-- ============================================================ + +-- Simple alternation +CREATE VIEW rpr_ev_alt_simple AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) C) + DEFINE A AS cat = 'A', B AS cat = 'B', C AS cat = 'C' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_simple'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) C) + DEFINE A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'' +);'); + +-- Multiple items in alternation +CREATE VIEW rpr_ev_alt_multi_item AS +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C | D) E) + DEFINE + A AS cat = 'A', B AS cat = 'B', C AS cat = 'C', + D AS cat = 'D', E AS cat = 'E' +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_multi_item'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM rpr_nfa_test +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B | C | D) E) + DEFINE + A AS cat = ''A'', B AS cat = ''B'', C AS cat = ''C'', + D AS cat = ''D'', E AS cat = ''E'' +);'); + +-- Alternation with quantifiers +CREATE VIEW rpr_ev_alt_with_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_with_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B)+ C) + DEFINE A AS v % 3 = 1, B AS v % 3 = 2, C AS v % 3 = 0 +);'); + +-- Multiple alternatives (4+) +CREATE VIEW rpr_ev_alt_four_plus AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A | B | C | D | E) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_four_plus'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A | B | C | D | E) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4 +);'); + +-- Alternation at start +CREATE VIEW rpr_ev_alt_at_start AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B) C D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_start'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B) C D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Multiple sequential alternations +CREATE VIEW rpr_ev_alt_sequential AS +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B) C (D | E) F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_sequential'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 100) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B) C (D | E) F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +);'); + +-- Quantified alternatives +CREATE VIEW rpr_ev_alt_quantified AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+ | B+) C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_quantified'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A+ | B+) C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +);'); + +-- Alternation at end +CREATE VIEW rpr_ev_alt_at_end AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (C | D)) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_at_end'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B (C | D)) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Nested ALT at start of branch inside outer ALT +-- Pattern: (A ((B | C) D | E)) - preceding VAR + inner ALT as first branch element +CREATE VIEW rpr_ev_alt_nested_start AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B | C) D | E)) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_start'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A ((B | C) D | E)) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, C AS v % 5 = 2, D AS v % 5 = 3, E AS v % 5 = 4 +);'); + +-- Nested ALT at end of branch inside outer ALT +-- Pattern: (C (A | B) | D) - inner ALT is last element in outer branch +CREATE VIEW rpr_ev_alt_nested_end AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C (A | B) | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_nested_end'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C (A | B) | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Quantified group as the first alternation branch +-- Pattern: ((A B)+ | C) - leading group branch must open the enclosing paren +CREATE VIEW rpr_ev_alt_grp_first AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + +-- Quantified group as the last alternation branch +-- Pattern: (C | (A B)+) - trailing group branch, no separator follows +CREATE VIEW rpr_ev_alt_grp_last AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + +-- Quantified group as the middle branch of a three-way alternation +-- Pattern: (C | (A B)+ | D) - separator before D must survive the group branch +CREATE VIEW rpr_ev_alt_grp_mid AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+ | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_mid'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+ | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Quantified group as the first branch of a three-way alternation +-- Pattern: ((A B)+ | C | D) - leading group branch with two following branches +CREATE VIEW rpr_ev_alt_grp_first3 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | C | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_first3'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | C | D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Bounded-quantifier group as the first alternation branch +-- Pattern: ((A B){2} | C) - leading group branch with a range quantifier +CREATE VIEW rpr_ev_alt_grp_bounded AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2} | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B){2} | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + +-- Two quantified groups in one alternation +-- Pattern: ((A B)+ | (C D)+) - both branches are groups +CREATE VIEW rpr_ev_alt_grp_both AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | (C D)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_both'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B)+ | (C D)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Leading group branch in an alternation nested in a sequence +-- Pattern: (((A B)+ | C) D) - inner alternation opens with a group branch +CREATE VIEW rpr_ev_alt_grp_seq_head AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B)+ | C) D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_head'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B)+ | C) D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Trailing group branch in an alternation nested in a sequence +-- Pattern: ((C | (A B)+) D) - group as last branch, then a sequence element +CREATE VIEW rpr_ev_alt_grp_seq_tail AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((C | (A B)+) D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_seq_tail'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((C | (A B)+) D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Quantified alternation whose first branch is a quantified group +-- Pattern: (((A B){2} | C)+) - single-ALT group wraps a leading group branch +CREATE VIEW rpr_ev_alt_grp_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){2} | C)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B){2} | C)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + +-- Unit (1,1) group as an alternation branch (emits no BEGIN/END) +-- Pattern: ((A B) | C) - control: takes the variable path, not deparse_rpr_group +CREATE VIEW rpr_ev_alt_grp_unit AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B) | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_unit'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A B) | C) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + +-- Quantified variable as the first alternation branch +-- Pattern: (A+ | C) - control: deparse_rpr_var already opens the leading paren +CREATE VIEW rpr_ev_alt_var_first AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ | C) + DEFINE A AS v % 4 = 0, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_var_first'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+ | C) + DEFINE A AS v % 4 = 0, C AS v % 4 = 2 +);'); + +-- Quantified group as the last branch of a three-way alternation +-- Pattern: (C | D | (A B)+) - control: trailing group needs no separator +CREATE VIEW rpr_ev_alt_grp_last3 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | D | (A B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_last3'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | D | (A B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Alternation nested in a leading branch must not swallow the trailing branch +-- Pattern: (D (A | B) | E) - inherited limit bounds the inner alternation +CREATE VIEW rpr_ev_alt_inner_bounded AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (D (A | B) | E) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_inner_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (D (A | B) | E) + DEFINE A AS v % 5 = 0, B AS v % 5 = 1, D AS v % 5 = 2, E AS v % 5 = 3 +);'); + +-- Group mid-branch followed by a sequence element needs no separator before it +-- Pattern: (C | (A B)+ D) - relative-next blocks a spurious separator at D +CREATE VIEW rpr_ev_alt_grp_then_seq AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+ D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_grp_then_seq'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (C | (A B)+ D) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2, D AS v % 4 = 3 +);'); + +-- Quantified group wrapping a lone alternation: the ALT supplies the parens +-- Pattern: ((A | B)+) - loneAlt path, single pair of parens +CREATE VIEW rpr_ev_grp_lone_alt AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_lone_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1 +);'); + +-- Quantified group wrapping a sequence whose last element is an alternation +-- Pattern: ((A (B | C))+) - group paren plus a nested alternation paren +CREATE VIEW rpr_ev_grp_seq_alt AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B | C))+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_seq_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B | C))+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + +-- Quantified group wrapping a sequence whose first element is an alternation +-- Pattern: (((A | B) C)+) - leading nested alternation inside a group sequence +CREATE VIEW rpr_ev_grp_alt_seq AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B) C)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_alt_seq'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B) C)+) + DEFINE A AS v % 4 = 0, B AS v % 4 = 1, C AS v % 4 = 2 +);'); + +-- Alternation non-last in a non-last branch, stacked three deep: each level's +-- inherited limit must bound the inner alternation against the next branch +-- Pattern: (((A | B) C | D) E | F) - three nested inherited-limit boundaries +CREATE VIEW rpr_ev_alt_stack3 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B) C | D) E | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B) C | D) E | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +);'); + +-- Same interaction stacked four deep, to exercise the induction one step further +-- Pattern: ((((A | B) C | D) E | F) G | H) - four nested inherited-limit boundaries +CREATE VIEW rpr_ev_alt_stack4 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((A | B) C | D) E | F) G | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack4'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((A | B) C | D) E | F) G | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +);'); + +-- Three-deep stack whose innermost branch is a quantified group: the group's +-- skip-target jump must not be mistaken for a branch separator at any depth +-- Pattern: (((A | B)+ C | D) E | F) - inherited limit plus loneAlt at the base +CREATE VIEW rpr_ev_alt_stack3_grp AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B)+ C | D) E | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_stack3_grp'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A | B)+ C | D) E | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +);'); + +-- Alternation trailing a paren-less sequence (last element of a non-last +-- branch, no same-depth sibling to bound it), nested three deep +-- Pattern: (A (B (C | D) | E) | F) - each inner alternation is branch-tail +CREATE VIEW rpr_ev_alt_tail3 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C | D) | E) | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail3'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C | D) | E) | F) + DEFINE A AS v % 6 = 0, B AS v % 6 = 1, C AS v % 6 = 2, + D AS v % 6 = 3, E AS v % 6 = 4, F AS v % 6 = 5 +);'); + +-- Same branch-tail alternation nested four deep +-- Pattern: (A (B (C (D | E) | F) | G) | H) - branch-tail alternation x4 +CREATE VIEW rpr_ev_alt_tail4 AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C (D | E) | F) | G) | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail4'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C (D | E) | F) | G) | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +);'); + +-- A nested alternation tail neighbouring a multi-element sequence branch: the +-- branch boundary must split "...branch-tail ALT" from a plain "G A" sequence +-- Pattern: (A (B (C (D | E) | F) | G A) | H) - seq branch beside an ALT tail +CREATE VIEW rpr_ev_alt_tail_seqbranch AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C (D | E) | F) | G A) | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_tail_seqbranch'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A (B (C (D | E) | F) | G A) | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +);'); + +-- A nested alternation that is sibling-bounded by a trailing sequence element +-- at the outer level (the ALT is not the branch tail; G follows it in-branch) +-- Pattern: ((A (B (C | D) | E) | F) G | H) - ALT bounded by a following element +CREATE VIEW rpr_ev_alt_mid_seqtail AS +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B (C | D) | E) | F) G | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_alt_mid_seqtail'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 20) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B (C | D) | E) | F) G | H) + DEFINE A AS v % 8 = 0, B AS v % 8 = 1, C AS v % 8 = 2, D AS v % 8 = 3, + E AS v % 8 = 4, F AS v % 8 = 5, G AS v % 8 = 6, H AS v % 8 = 7 +);'); + +-- ============================================================ +-- Group Pattern Tests +-- ============================================================ + +-- Simple group +CREATE VIEW rpr_ev_grp_simple AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_simple'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + +-- Group with bounded quantifier +CREATE VIEW rpr_ev_grp_bounded AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B){2,4}) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B){2,4}) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + +-- Nested groups +CREATE VIEW rpr_ev_grp_nested AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B){2})+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B){2})+) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + +-- Deep nesting (3+ levels) +CREATE VIEW rpr_ev_grp_deep AS +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((A | B)+)+)+) + DEFINE A AS v % 2 = 0, B AS v % 2 = 1 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_deep'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 40) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((((A | B)+)+)+) + DEFINE A AS v % 2 = 0, B AS v % 2 = 1 +);'); + +-- Bounded quantifier on alternation +CREATE VIEW rpr_ev_grp_bounded_alt AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B){2,3} C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_bounded_alt'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A | B){2,3} C) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +);'); + +-- Nested groups with quantifiers +CREATE VIEW rpr_ev_grp_nested_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B)+ C)*) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_nested_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A B)+ C)*) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +);'); + +-- Partial nested quantification +CREATE VIEW rpr_ev_grp_partial_quant AS +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B C)+)*) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_grp_partial_quant'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 60) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN ((A (B C)+)*) + DEFINE A AS v % 3 = 0, B AS v % 3 = 1, C AS v % 3 = 2 +);'); + +-- ============================================================ +-- Window Function Combinations +-- ============================================================ + +-- count(*) with pattern +CREATE VIEW rpr_ev_wfn_count AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_count'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- first_value with pattern +CREATE VIEW rpr_ev_wfn_first_value AS +SELECT first_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_first_value'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT first_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- last_value with pattern +CREATE VIEW rpr_ev_wfn_last_value AS +SELECT last_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_last_value'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT last_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- Multiple window functions +CREATE VIEW rpr_ev_wfn_multi AS +SELECT + count(*) OVER w, + first_value(v) OVER w, + last_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_wfn_multi'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT + count(*) OVER w, + first_value(v) OVER w, + last_value(v) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v % 5 <> 0, B AS v % 5 = 0 +);'); + +-- ============================================================ +-- DEFINE Expression Variations +-- ============================================================ + +-- Complex boolean expressions +CREATE VIEW rpr_ev_def_complex_bool AS +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS (v % 5 <> 0) AND (v % 3 <> 0), + B AS (v % 5 = 0) OR (v % 3 = 0) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_complex_bool'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 50) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS (v % 5 <> 0) AND (v % 3 <> 0), + B AS (v % 5 = 0) OR (v % 3 = 0) +);'); + +-- Using PREV function +CREATE VIEW rpr_ev_def_prev AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (S U+ D+) + DEFINE + S AS TRUE, + U AS v > PREV(v), + D AS v < PREV(v) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_prev'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (S U+ D+) + DEFINE + S AS TRUE, + U AS v > PREV(v), + D AS v < PREV(v) +);'); + +-- Using 1-arg PREV (implicit offset 1) +CREATE VIEW rpr_ev_nav_prev1 AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS v > PREV(v) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT'; + +-- Using 1-arg NEXT (implicit offset 1) +CREATE VIEW rpr_ev_nav_next1 AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS v < NEXT(v) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next1'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT'; + +-- Using 2-arg PREV (explicit offset) +CREATE VIEW rpr_ev_nav_prev2 AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS v > PREV(v, 2) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_prev2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT'; + +-- Using 2-arg NEXT (explicit offset) +CREATE VIEW rpr_ev_nav_next2 AS +SELECT count(*) OVER w +FROM generate_series(1, 30) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + A AS TRUE, + B AS v < NEXT(v, 2) +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_nav_next2'), E'\n')) AS line WHERE line ~ 'PATTERN|DEFINE|PREV|NEXT'; + +-- Using NULL comparisons +CREATE VIEW rpr_ev_def_null AS +SELECT count(*) OVER w +FROM ( + SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v + FROM generate_series(1, 30) v +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v IS NOT NULL, B AS v IS NULL +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_def_null'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM ( + SELECT CASE WHEN v % 5 = 0 THEN NULL ELSE v END AS v + FROM generate_series(1, 30) v +) t +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE A AS v IS NOT NULL, B AS v IS NULL +);'); + +-- ============================================================ +-- Large Scale Statistics Verification +-- ============================================================ + +-- 500 rows - verify statistics scale correctly +CREATE VIEW rpr_ev_scale_500rows AS +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B C) + DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_500rows'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B C) + DEFINE A AS v % 10 < 7, B AS v % 10 = 7, C AS v % 10 = 8 +);'); + +-- High match count scenario +CREATE VIEW rpr_ev_scale_high_match AS +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_match'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE A AS v % 2 = 1, B AS v % 2 = 0 +);'); + +-- High skip count scenario +CREATE VIEW rpr_ev_scale_high_skip AS +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E) + DEFINE + A AS v % 100 = 1, + B AS v % 100 = 2, + C AS v % 100 = 3, + D AS v % 100 = 4, + E AS v % 100 = 5 +); +SELECT line FROM unnest(string_to_array(pg_get_viewdef('rpr_ev_scale_high_skip'), E'\n')) AS line WHERE line ~ 'PATTERN'; +SELECT rpr_explain_filter(' +EXPLAIN (ANALYZE, BUFFERS OFF, COSTS OFF, TIMING OFF, SUMMARY OFF) +SELECT count(*) OVER w +FROM generate_series(1, 500) AS s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E) + DEFINE + A AS v % 100 = 1, + B AS v % 100 = 2, + C AS v % 100 = 3, + D AS v % 100 = 4, + E AS v % 100 = 5 +);'); + +-- +-- Planner optimization: optimize_window_clauses must not alter RPR frame +-- +-- optimize_window_clauses() replaces frame options via prosupport functions. +-- Affected functions: row_number, rank, dense_rank, percent_rank, cume_dist, +-- ntile. All would change the frame to ROWS UNBOUNDED PRECEDING, breaking +-- RPR's required ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING. +-- Test with row_number() as representative case. +-- + +-- Without RPR: row_number() frame is optimized to ROWS UNBOUNDED PRECEDING +CREATE VIEW rpr_ev_opt_no_rpr AS +SELECT row_number() OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +); + +EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_no_rpr; + +-- With RPR: frame must remain ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +CREATE VIEW rpr_ev_opt_with_rpr AS +SELECT row_number() OVER w +FROM generate_series(1, 10) AS s(v) +WINDOW w AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + B AS v > PREV(v) +); + +EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_with_rpr; + +-- +-- Planner optimization: non-RPR and RPR windows that share the same base frame +-- after frame optimization are kept as separate WindowAgg nodes. +-- +CREATE VIEW rpr_ev_opt_mixed AS +SELECT + row_number() OVER w_normal AS rn_normal, + row_number() OVER w_rpr AS rn_rpr +FROM generate_series(1, 5) AS s(v) +WINDOW + w_normal AS (ORDER BY v RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW), + w_rpr AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > 1 + ); + +EXPLAIN (COSTS OFF) SELECT * FROM rpr_ev_opt_mixed; + +-- +-- Planner optimization: find_window_run_conditions must not push down +-- RPR window function results as Run Conditions. +-- +-- find_window_run_conditions() pushes WHERE filters on monotonic window +-- functions into WindowAgg as Run Conditions for early termination. +-- With RPR's required frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED +-- FOLLOWING), the monotonic direction of the window function determines +-- which comparison operators allow pushdown: +-- INCREASING (<=): row_number, rank, dense_rank, percent_rank, +-- cume_dist, ntile +-- DECREASING (>): count(*). As the current row advances, the frame +-- (which ends at UNBOUNDED FOLLOWING) shrinks, so the +-- count decreases. +-- RPR window function results are match-dependent, not monotonic, so this +-- pushdown does not apply. Test with count(*) > 0 as a representative case. +-- + +-- Without RPR: count(*) > 0 is pushed down as Run Condition +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT count(*) OVER w AS cnt + FROM generate_series(1, 10) AS s(v) + WINDOW w AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + ) +) t WHERE cnt > 0; + +-- With RPR: count(*) > 0 must not be pushed down as Run Condition +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT count(*) OVER w AS cnt + FROM generate_series(1, 10) AS s(v) + WINDOW w AS ( + ORDER BY v + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B+) + DEFINE + B AS v > PREV(v) + ) +) t WHERE cnt > 0; + +-- ============================================================ +-- Nav Mark Lookback/Lookahead Tests +-- Verifies planner-computed navigation offsets for tuplestore trim. +-- Lookback: how far back from currentpos (PREV, LAST, compound PREV_LAST/NEXT_LAST). +-- Lookahead: how far forward from match_start (FIRST, compound PREV_FIRST/NEXT_FIRST). +-- ============================================================ + +-- Prepare statement for host variable offset test below +PREPARE rpr_nav_offset_prep(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > PREV(v, $1) +); + +-- No navigation function: offset 0 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > 0 +); + +-- NEXT only: no backward navigation, offset 0 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v < NEXT(v) +); + +-- PREV(v): implicit offset 1 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > PREV(v) +); + +-- PREV(v, 3): explicit constant offset 3 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > PREV(v, 3) +); + +-- Two PREV with different offsets: max(1, 5) = 5 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v, 1) < v AND PREV(v, 5) < v +); + +-- Host variable offset: custom plan resolves $1=2 to constant 2 +EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2); + +-- Force generic plan: offset becomes "runtime" (Param node) +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE rpr_nav_offset_prep(2); +RESET plan_cache_mode; +DEALLOCATE rpr_nav_offset_prep; + +-- FIRST(v): retain all (references match_start row) +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS v > FIRST(v) +); + +-- LAST(v, 1): backward reach 1, same as PREV(v, 1) +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS LAST(v, 1) > 0 +); + +-- LAST(v) without offset + PREV(v): no match_start dependency, offset 1 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS LAST(v) > PREV(v) +); + +-- Compound PREV(FIRST(val, 1), 2): lookback from match_start, firstOffset = 1-2 = -1 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(FIRST(v, 1), 2) > 0 +); + +-- Compound NEXT(FIRST(val), 3): firstOffset = 0+3 = 3 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(FIRST(v), 3) > 0 +); + +-- Compound PREV(LAST(val), 2): lookback = 0+2 = 2 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(LAST(v), 2) > 0 +); + +-- Compound NEXT(LAST(val, 1), 3): lookback = max(1-3, 0) = 0 +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(LAST(v, 1), 3) > 0 +); + +-- Compound PREV(LAST(val, N), M): constant near-overflow (N+M just fits int64) +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(LAST(v, 4611686018427387903), 4611686018427387903) IS NOT NULL +); + +-- Compound PREV(LAST(val, N), M): constant overflow -> retain all +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(LAST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL +); + +-- Compound NEXT(FIRST(val, N), M): constant lookahead overflow -> infinite +-- N + M overflows int64; forward reach is unbounded, displayed as infinite. +EXPLAIN (COSTS OFF) SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(FIRST(v, 4611686018427387904), 4611686018427387904) IS NOT NULL +); + +-- Compound PREV(LAST(val, $1), $2): parameter lookback overflow -> retain all +-- EXPLAIN shows "runtime" (plan-level); EXPLAIN ANALYZE shows "retain all" +-- (executor-resolved). +PREPARE test_overflow_lookback(int8, int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(LAST(v, $1), $2) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904); +EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF) + EXECUTE test_overflow_lookback(4611686018427387904, 4611686018427387904); +RESET plan_cache_mode; +DEALLOCATE test_overflow_lookback; + +-- Compound NEXT(FIRST(val, $1), $2): parameter lookahead overflow -> infinite +PREPARE test_overflow_lookahead(int8, int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(FIRST(v, $1), $2) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF, ANALYZE, TIMING OFF, SUMMARY OFF) + EXECUTE test_overflow_lookahead(4611686018427387904, 4611686018427387904); +RESET plan_cache_mode; +DEALLOCATE test_overflow_lookahead; + +-- Runtime (non-overflow) lookahead: a small parameter offset under a generic +-- plan keeps the FIRST offset unresolved at plan time, so EXPLAIN reports +-- "Nav Mark Lookahead: runtime" rather than a concrete value. +PREPARE p_first_runtime(int8, int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(FIRST(v, $1), $2) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE p_first_runtime(1, 1); +RESET plan_cache_mode; +DEALLOCATE p_first_runtime; + +-- PREV(v) + PREV(v, $1): NEEDS_EVAL path must account for implicit lookback=1 +-- Previously, eval_nav_max_offset_walker skipped PREV(v) when offset_arg was +-- NULL, causing maxOffset=0 when $1=0, which would trim the row needed by +-- PREV(v). Verify this executes without "cannot fetch row before mark" error. +PREPARE test_prev_implicit_offset(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v) IS NOT NULL AND PREV(v, $1) IS NOT NULL +); +EXECUTE test_prev_implicit_offset(0); +DEALLOCATE test_prev_implicit_offset; + +-- NEEDS_EVAL executor offset paths: a Param nav offset stays non-Const under a +-- generic plan, so the planner marks the offset NEEDS_EVAL and the executor +-- resolves it at init via eval_define_offsets -> visit_nav_exec. Each query +-- below exercises a different navigation arm of that walker. + +-- Simple FIRST(v, $1): forward-reach FIRST arm. +PREPARE test_eval_first(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS FIRST(v, $1) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXECUTE test_eval_first(1); +RESET plan_cache_mode; +DEALLOCATE test_eval_first; + +-- Bare LAST(v, $1): backward-reach LAST-with-offset arm. +PREPARE test_eval_last(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS LAST(v, $1) >= 0 +); +SET plan_cache_mode = force_generic_plan; +EXECUTE test_eval_last(1); +RESET plan_cache_mode; +DEALLOCATE test_eval_last; + +-- Compound NEXT(LAST(v, $1), $2): backward-reach NEXT_LAST arm. +PREPARE test_eval_nextlast(int8, int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS NEXT(LAST(v, $1), $2) > 0 +); +SET plan_cache_mode = force_generic_plan; +EXECUTE test_eval_nextlast(1, 1); +RESET plan_cache_mode; +DEALLOCATE test_eval_nextlast; + +-- Compound PREV(FIRST(v, $1), $2): forward-reach PREV_FIRST arm. +PREPARE test_eval_prevfirst(int8, int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(FIRST(v, $1), $2) IS NOT NULL +); +SET plan_cache_mode = force_generic_plan; +EXECUTE test_eval_prevfirst(1, 1); +RESET plan_cache_mode; +DEALLOCATE test_eval_prevfirst; + +-- Runtime error: negative offset at execution time +PREPARE test_runtime_neg_offset(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v, $1) IS NOT NULL +); +EXECUTE test_runtime_neg_offset(-1); +DEALLOCATE test_runtime_neg_offset; + +-- Runtime error: null offset at execution time +PREPARE test_runtime_null_offset(int8) AS +SELECT count(*) OVER w +FROM generate_series(1,10) s(v) +WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS PREV(v, $1) IS NOT NULL +); +EXECUTE test_runtime_null_offset(NULL); +DEALLOCATE test_runtime_null_offset; diff --git a/src/test/regress/sql/rpr_integration.sql b/src/test/regress/sql/rpr_integration.sql new file mode 100644 index 00000000000..c5f5e850925 --- /dev/null +++ b/src/test/regress/sql/rpr_integration.sql @@ -0,0 +1,1009 @@ +-- ============================================================ +-- RPR Integration Tests +-- Planner optimization interaction tests for Row Pattern Recognition +-- ============================================================ +-- +-- Verifies that each planner optimization correctly handles RPR windows. +-- Even if individual optimizations are tested elsewhere, this file +-- provides a single checkpoint for all planner/RPR interactions. +-- +-- A. Planner Optimization Protection Tests +-- A1. Frame optimization bypass +-- A2. Run condition pushdown bypass +-- A3. Window dedup prevention (RPR vs non-RPR) +-- A4. Window dedup prevention (same PATTERN, different DEFINE) +-- A5. Unused window removal prevention +-- A6. Inverse transition bypass +-- A7. Cost estimation RPR awareness +-- A8. Subquery flattening prevention +-- A9. DEFINE expression non-propagation +-- A10. RPR + LIMIT +-- +-- B. Integration Scenario Tests +-- B1. RPR + CTE +-- B2. RPR + JOIN +-- B3. RPR + Set operations +-- B4. RPR + Prepared statements +-- B5. RPR + Partitioned table +-- B6. RPR + LATERAL +-- B7. RPR + Recursive CTE +-- B8. RPR + Incremental sort +-- B9. RPR + Volatile function in DEFINE +-- B10. RPR + Correlated subquery +-- + +CREATE TABLE rpr_integ (id INT, val INT); +INSERT INTO rpr_integ VALUES + (1, 10), (2, 20), (3, 15), (4, 25), (5, 5), + (6, 30), (7, 35), (8, 20), (9, 40), (10, 45); + +-- ============================================================ +-- A1. Frame optimization bypass +-- ============================================================ +-- Verify that optimize_window_clauses() does not apply frame +-- optimization to RPR windows. Both queries below use the same input +-- frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) with +-- row_number(), whose prosupport handles +-- SupportRequestOptimizeWindowClause and triggers frame rewriting. +-- In the non-RPR baseline the planner rewrites the frame to ROWS +-- UNBOUNDED PRECEDING, while in the RPR case the guard in +-- optimize_window_clauses() blocks the rewrite and the frame is +-- preserved as specified. + +-- Non-RPR baseline: the planner rewrites the frame to ROWS UNBOUNDED PRECEDING. +EXPLAIN (COSTS OFF) +SELECT row_number() OVER w FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING); + +-- RPR case: the frame is preserved as specified. +EXPLAIN (COSTS OFF) +SELECT row_number() OVER w FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)); + +-- ============================================================ +-- A2. Run condition pushdown bypass +-- ============================================================ +-- Verify that find_window_run_conditions() does not push a monotonic +-- filter down as a Run Condition on RPR windows. RPR match counts are +-- determined by pattern matching rather than by a monotonic +-- accumulation over the frame, so a filter such as "cnt > 0" cannot be +-- used to stop evaluating the window function early. + +-- Non-RPR baseline: the filter is expected to appear as a Run Condition. +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) +) t WHERE cnt > 0; + +-- RPR case: the filter must appear as a Filter above the WindowAgg, +-- not as a Run Condition. +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t WHERE cnt > 0; + +-- Verify that the RPR query still returns every row whose match count is +-- greater than zero, confirming the filter is evaluated above the +-- WindowAgg rather than cutting off pattern matching prematurely. +SELECT * FROM ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t WHERE cnt > 0 +ORDER BY id; + +-- ============================================================ +-- A3. Window dedup prevention (RPR vs non-RPR) +-- ============================================================ +-- Verify that PostgreSQL does not merge an RPR window with a non-RPR +-- window even when both share the same ORDER BY and frame +-- specification. RPR pattern matching produces results that are +-- semantically different from a plain frame-based aggregate, so the +-- two windows must remain as separate WindowAgg nodes. Inline window +-- specs are used throughout this section because only inline windows +-- are subject to the dedup path; distinct named windows are always +-- kept separate regardless of equivalence. + +-- Non-RPR baseline: two inline windows with identical spec are +-- deduped by the planner into a single WindowAgg node, confirming +-- that the dedup path is active for non-RPR windows. +EXPLAIN (COSTS OFF) +SELECT + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS cnt, + sum(val) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS total +FROM rpr_integ; + +-- An inline RPR window and an inline non-RPR window share the same +-- ORDER BY and frame but must remain as distinct WindowAgg nodes. +EXPLAIN (COSTS OFF) +SELECT + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS rpr_cnt, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS normal_cnt +FROM rpr_integ; + +-- Verify that the two windows return independent counts per row, +-- confirming they were not merged into a single WindowAgg. +SELECT + id, val, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS rpr_cnt, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS normal_cnt +FROM rpr_integ +ORDER BY id; + +-- ============================================================ +-- A4. Window dedup prevention (same PATTERN, different DEFINE) +-- ============================================================ +-- Verify that inline-window dedup does not merge two RPR windows +-- that share the same PATTERN structure but have different DEFINE +-- conditions. Even though the ORDER BY, frame, and PATTERN coincide, +-- the differing DEFINE expressions classify rows differently and +-- must therefore yield two separate WindowAgg nodes. Inline specs +-- are used here because dedup only applies to inline windows. + +-- Baseline: two inline RPR windows that are structurally identical +-- (same PARTITION BY, ORDER BY, frame, PATTERN, and DEFINE) are deduped by the +-- parser into a single WindowAgg node, confirming that parser-level dedup is +-- active for RPR windows whose DEFINE matches. +EXPLAIN (COSTS OFF) +SELECT + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS cnt, + sum(val) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS total +FROM rpr_integ; + +-- Two inline RPR windows with the same PATTERN but opposite DEFINE +-- conditions must remain as separate WindowAgg nodes. +EXPLAIN (COSTS OFF) +SELECT + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS cnt_up, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val < PREV(val)) AS cnt_down +FROM rpr_integ; + +-- Verify that the two windows return different counts per row, +-- confirming the DEFINE conditions were not collapsed by dedup. +SELECT + id, val, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) AS cnt_up, + count(*) OVER (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val < PREV(val)) AS cnt_down +FROM rpr_integ +ORDER BY id; + +-- ============================================================ +-- A5. Unused window removal prevention +-- ============================================================ +-- Verify that remove_unused_subquery_outputs() does not drop an RPR +-- window function when the outer query does not reference its result. +-- The WindowAgg node performs the pattern match itself; without it, +-- the match would be silently skipped. The plan must contain a +-- WindowAgg node beneath the outer Aggregate. +EXPLAIN (COSTS OFF) +SELECT count(*) FROM ( + SELECT count(*) OVER w FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > PREV(val)) +) t; + +SELECT count(*) FROM ( + SELECT count(*) OVER w FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > PREV(val)) +) t; + +-- The DEFINE expression references PREV(val), so the window must be +-- preserved even if the outer query only aggregates over the count. +-- The plan must still contain a WindowAgg with the PATTERN/DEFINE +-- intact. +EXPLAIN (COSTS OFF) +SELECT count(*), sum(c) FROM ( + SELECT count(*) OVER w AS c FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > PREV(val)) +) t; + +SELECT count(*), sum(c) FROM ( + SELECT count(*) OVER w AS c FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS val > PREV(val)) +) t; + +-- The DEFINE expression contains no navigation, but the RPR window +-- must still be preserved because the match structure itself affects +-- the count. The plan must retain the WindowAgg. +EXPLAIN (COSTS OFF) +SELECT count(*), sum(c) FROM ( + SELECT count(*) OVER w AS c FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS TRUE) +) t; + +SELECT count(*), sum(c) FROM ( + SELECT count(*) OVER w AS c FROM rpr_integ + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A+) + DEFINE A AS TRUE) +) t; + +-- XXX: "val" is non-resjunk in the subquery output and is not +-- referenced by the outer query. Without a guard, +-- remove_unused_subquery_outputs() would replace it with NULL in +-- the subquery output, and that replacement propagates to the +-- scan's targetlist -- DEFINE would then evaluate with NULL +-- inputs. The targetlist has no way to distinguish "exposed to +-- the outer query" from "referenced only by DEFINE", so the +-- optimization cannot be applied selectively. The column guard +-- in allpaths.c blocks this replacement for any column referenced +-- by an RPR DEFINE clause, keeping the WindowAgg with DEFINE +-- active in the plan. +EXPLAIN (COSTS OFF) +SELECT count(*) FROM ( + SELECT val, count(*) OVER w FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t; + +SELECT count(*) FROM ( + SELECT val, count(*) OVER w FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t; + +-- The same guard must also cover a whole-row Var. Writing the bare +-- relation name (rpr_integ) in DEFINE resolves to a whole-row Var, whose +-- attribute number is 0. remove_unused_subquery_outputs() matches the +-- guard on attribute number, so the whole-row Var is retained as a single +-- entry while the unused scalar "val" output is still replaced with NULL; +-- DEFINE evaluates against the intact row, and the match is unchanged. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT sum(c) FROM ( + SELECT val, count(*) OVER w AS c FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS rpr_integ IS NOT NULL) +) t; + +SELECT sum(c) FROM ( + SELECT val, count(*) OVER w AS c FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS rpr_integ IS NOT NULL) +) t; + +-- ============================================================ +-- A6. Inverse transition bypass +-- ============================================================ +-- Verify that RPR windows do not use the moving aggregate (inverse +-- transition) optimization. Moving aggregates maintain state by +-- adding arriving rows and subtracting leaving rows, but an RPR +-- reduced frame is not a sliding window; the set of rows included in +-- the frame is determined by pattern matching and cannot be derived +-- incrementally from the previous frame. + +-- sum() would normally be eligible for the moving aggregate +-- optimization; under RPR it must be computed from scratch over each +-- reduced frame, and the returned values must match the pattern. +-- Note: inverse-transition selection is not exposed in the plan, so +-- there is no direct EXPLAIN assertion for it. The structural +-- guarantee is that RPR uses its own navigation mark, distinct from +-- the moving-aggregate mark, so the inverse-transition path is +-- never reached on the RPR side. This test verifies that +-- separation indirectly: if inverse transition leaked into the RPR +-- path, state would mix across match boundaries and pattern_sum +-- would diverge from the expected output, failing the regression. +SELECT id, val, + sum(val) OVER w AS pattern_sum +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + +-- A custom aggregate logs how its transition functions are called: msfunc +-- appends '+' for a forward transition, minvfunc appends '-' for an inverse +-- transition. Over an RPR reduced frame only '+' appears, confirming the +-- inverse-transition path is not used, and the logged values show which rows +-- each frame feeds the aggregate. +CREATE FUNCTION rpr_logging_sfunc(text, anyelement) RETURNS text AS +$$ SELECT COALESCE($1, '') || '*' || quote_nullable($2) $$ LANGUAGE SQL IMMUTABLE; +CREATE FUNCTION rpr_logging_msfunc(text, anyelement) RETURNS text AS +$$ SELECT COALESCE($1, '') || '+' || quote_nullable($2) $$ LANGUAGE SQL IMMUTABLE; +CREATE FUNCTION rpr_logging_minvfunc(text, anyelement) RETURNS text AS +$$ SELECT $1 || '-' || quote_nullable($2) $$ LANGUAGE SQL IMMUTABLE; +CREATE AGGREGATE rpr_logging_agg (anyelement) +( + stype = text, + sfunc = rpr_logging_sfunc, + mstype = text, + msfunc = rpr_logging_msfunc, + minvfunc = rpr_logging_minvfunc +); + +-- Match 1 is id1..id3 (v = NULL,'a','b') and match 2 is id4..id6 +-- (v = NULL,'c','d'); B extends the match while k increases. Every frame +-- shows '+' only (no '-'), and the two matches never share state across their +-- boundary. +SELECT id, v, + rpr_logging_agg(v) OVER w AS logged +FROM (VALUES + (1, 1, NULL), + (2, 3, 'a'), + (3, 5, 'b'), + (4, 2, NULL), + (5, 4, 'c'), + (6, 6, 'd') +) AS t(id, k, v) +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE A AS TRUE, B AS k > PREV(k)) +ORDER BY id; + +DROP AGGREGATE rpr_logging_agg(anyelement); +DROP FUNCTION rpr_logging_sfunc(text, anyelement); +DROP FUNCTION rpr_logging_msfunc(text, anyelement); +DROP FUNCTION rpr_logging_minvfunc(text, anyelement); + +-- ============================================================ +-- A7. Cost estimation RPR awareness +-- ============================================================ +-- cost_windowagg() must account for DEFINE expression evaluation cost. +-- Verify RPR WindowAgg cost > non-RPR WindowAgg cost. + +CREATE FUNCTION get_windowagg_cost(query text) RETURNS numeric AS $$ +DECLARE + plan json; + cost numeric; +BEGIN + EXECUTE 'EXPLAIN (FORMAT JSON) ' || query INTO plan; + cost := (plan->0->'Plan'->>'Total Cost')::numeric; + RETURN cost; +END; +$$ LANGUAGE plpgsql; + +SELECT get_windowagg_cost( + 'SELECT count(*) OVER w FROM rpr_integ + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+ C+) DEFINE B AS val > PREV(val), C AS val < PREV(val))') + > + get_windowagg_cost( + 'SELECT count(*) OVER w FROM rpr_integ + WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)') + AS rpr_cost_is_higher; + +DROP FUNCTION get_windowagg_cost(text); + +-- ============================================================ +-- A8. Subquery flattening prevention +-- ============================================================ +-- Verify that a subquery containing an RPR window is not flattened +-- into the outer query. is_simple_subquery() already blocks pullup +-- for subqueries with window functions in general; this test confirms +-- the rule continues to apply to RPR windows, so EXPLAIN must still +-- show a Subquery Scan above the RPR WindowAgg. + +EXPLAIN (COSTS OFF) +SELECT * FROM ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) sub +WHERE cnt > 0; + +-- ============================================================ +-- A9. DEFINE expression non-propagation +-- ============================================================ +-- Verify that DEFINE expressions are not propagated into the +-- targetlist of any upper WindowAgg node. Only the column references +-- consumed by DEFINE should be passed up; the full DEFINE expression +-- is meaningful only inside the RPR WindowAgg that owns it. +-- EXPLAIN VERBOSE is therefore expected to show a clean targetlist on +-- the outer WindowAgg, with no DEFINE-derived expression leaking in. +-- Note: columns referenced by DEFINE (e.g., "val") may appear as +-- resjunk entries in upper WindowAgg targetlists -- but that is harmless. +-- The claim here is limited to the full DEFINE boolean expression. +EXPLAIN (VERBOSE, COSTS OFF) +SELECT + count(*) OVER w_rpr AS rpr_cnt, + count(*) OVER w_normal AS normal_cnt +FROM rpr_integ +WINDOW + w_rpr AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)), + w_normal AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING); + +SELECT + count(*) OVER w_rpr AS rpr_cnt, + count(*) OVER w_normal AS normal_cnt +FROM rpr_integ +WINDOW + w_rpr AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)), + w_normal AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) +ORDER BY rpr_cnt DESC, normal_cnt DESC; + +-- ============================================================ +-- A10. RPR + LIMIT +-- ============================================================ +-- LIMIT must not interfere with RPR pattern matching. The Limit +-- node must sit above the WindowAgg so that pattern matching runs +-- on the full partition first; the result is then a prefix of the +-- un-LIMITed output. Pushing Limit below the WindowAgg would +-- truncate input before matching and silently drop valid matches. +EXPLAIN (COSTS OFF) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +LIMIT 5; + +-- Reference: un-LIMITed result against which the LIMIT 5 result is +-- compared. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + +-- LIMIT 5 case; the first five rows must match the reference above. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +LIMIT 5; + +-- ============================================================ +-- B1. RPR + CTE +-- ============================================================ +-- Verify that an RPR window embedded inside a CTE behaves the same as +-- a direct RPR query: +-- (1) A single-reference CTE is inlined by the planner and yields +-- per-row results identical to the direct RPR query. +-- (2) A multi-reference CTE is materialized (CTE Scan appears in +-- the plan) so pattern matching runs once, and every reference +-- observes the same match results. + +-- Baseline: direct RPR produces the per-row reference output. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + +-- Single-reference CTE: plan has no "CTE rpr_result" scope, showing +-- the CTE was inlined into the surrounding query. +EXPLAIN (COSTS OFF) +WITH rpr_result AS ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) +SELECT id, val, cnt FROM rpr_result ORDER BY id; + +-- Result must match the baseline row-for-row. +WITH rpr_result AS ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) +SELECT id, val, cnt FROM rpr_result ORDER BY id; + +-- Multi-reference CTE (self-join): plan has a "CTE rpr_result" scope +-- and CTE Scan nodes on both sides, showing the CTE was materialized +-- and pattern matching ran only once. +EXPLAIN (COSTS OFF) +WITH rpr_result AS ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) +SELECT r1.id, r1.cnt +FROM rpr_result r1 +JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt +WHERE r1.cnt > 0 +ORDER BY r1.id; + +-- Result: both references see the same match counts, so the self-join +-- preserves all matched rows from the baseline. +WITH rpr_result AS ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) +SELECT r1.id, r1.cnt +FROM rpr_result r1 +JOIN rpr_result r2 ON r1.id = r2.id AND r1.cnt = r2.cnt +WHERE r1.cnt > 0 +ORDER BY r1.id; + +-- ============================================================ +-- B2. RPR + JOIN +-- ============================================================ +-- Verify that an RPR subquery can be joined with another relation. +-- Two aspects are checked against a non-RPR baseline: +-- (1) Flattening: a non-RPR subquery is pulled up by the planner +-- (no Subquery Scan in the plan); an RPR subquery is kept +-- un-flattened (Subquery Scan above WindowAgg). +-- (2) Join correctness: the join aligns each RPR match row with +-- the dimension-table row on the same key. + +CREATE TABLE rpr_integ2 (id INT, label TEXT); +INSERT INTO rpr_integ2 VALUES + (1, 'a'), (2, 'b'), (3, 'c'), (4, 'd'), (5, 'e'), + (6, 'f'), (7, 'g'), (8, 'h'), (9, 'i'), (10, 'j'); + +-- Baseline: a non-RPR subquery is flattened by the planner. No +-- Subquery Scan node appears; the inner SELECT is merged into the +-- outer join. +EXPLAIN (COSTS OFF) +SELECT r.id, r.val, j.label +FROM (SELECT id, val FROM rpr_integ) r +JOIN rpr_integ2 j ON r.id = j.id +ORDER BY r.id; + +-- RPR subquery JOIN: the Subquery Scan is preserved above the +-- WindowAgg, confirming the RPR subquery is not flattened. +EXPLAIN (COSTS OFF) +SELECT r.id, r.cnt, j.label +FROM ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) r +JOIN rpr_integ2 j ON r.id = j.id +WHERE r.cnt > 0 +ORDER BY r.id; + +-- Result: matched RPR rows align with dimension rows on id, showing +-- the join correctly pairs per-row match counts with their labels. +SELECT r.id, r.cnt, j.label +FROM ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) r +JOIN rpr_integ2 j ON r.id = j.id +WHERE r.cnt > 0 +ORDER BY r.id; + +-- ============================================================ +-- B3. RPR + Set operations +-- ============================================================ +-- Verify that RPR results combine correctly with non-RPR results +-- under a UNION ALL. The plan must show an Append node with two +-- independent child plans: the RPR branch with Pattern/DEFINE active, +-- and the non-RPR branch with a plain WindowAgg. Each child scans +-- the base relation on its own and contributes its rows to the +-- unioned output. + +-- Plan: Append with two independent children. The RPR branch has a +-- WindowAgg carrying Pattern/Nav Mark Lookback; the non-RPR branch +-- has a plain WindowAgg with no pattern metadata. +EXPLAIN (COSTS OFF) +SELECT id, cnt, 'rpr' AS source FROM ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t WHERE cnt > 0 +UNION ALL +SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source +FROM rpr_integ +ORDER BY source, id; + +-- Result: rows from both branches are present in the unioned output. +-- The RPR branch emits only matched rows (cnt > 0), while the +-- non-RPR branch emits all rows with its own count values. +SELECT id, cnt, 'rpr' AS source FROM ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) t WHERE cnt > 0 +UNION ALL +SELECT id, count(*) OVER (ORDER BY id) AS cnt, 'normal' AS source +FROM rpr_integ +ORDER BY source, id; + +-- ============================================================ +-- B4. RPR + Prepared statements +-- ============================================================ +-- Verify that RPR queries survive the prepared-statement path by +-- exercising both plancache modes with a parameter that feeds into +-- RPR's navigation offset (PREV(val, $1)). The parameter surfaces +-- the RPR-specific plancache difference: +-- - custom plan: "Nav Mark Lookback" is resolved to the literal +-- parameter value at plan time (e.g., "Nav Mark Lookback: 1"). +-- - generic plan: "Nav Mark Lookback" is deferred to execution and +-- appears as "Nav Mark Lookback: runtime" in the plan. +-- The result must be identical under both modes. + +-- Register the prepared statement; DEFINE uses PREV(val, $1) so the +-- parameter reaches RPR's navigation machinery. +PREPARE rpr_prev(int) AS +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val, $1)) +ORDER BY id; + +-- Custom plan: Nav Mark Lookback resolved to the literal 1. +SET plan_cache_mode = force_custom_plan; +EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1); +EXECUTE rpr_prev(1); + +-- Generic plan: Nav Mark Lookback deferred to execution, shown as +-- "runtime" in the plan. Result must match the custom-plan result +-- exactly. +SET plan_cache_mode = force_generic_plan; +EXPLAIN (COSTS OFF) EXECUTE rpr_prev(1); +EXECUTE rpr_prev(1); + +-- Negative runtime nav offset under the generic plan: init clamps it to 0 for +-- trim sizing, but the per-row navigation rejects the negative offset. +EXECUTE rpr_prev(-1); + +RESET plan_cache_mode; +DEALLOCATE rpr_prev; + +-- ============================================================ +-- B5. RPR + Partitioned table +-- ============================================================ +-- Verify that RPR pattern matching works correctly when the source +-- relation is partitioned. The planner must gather rows from every +-- partition into a single ordered stream before RPR can see them, +-- because pattern matching is sequential across the entire +-- partition-by group and cannot be performed independently on each +-- table partition. + +CREATE TABLE rpr_part (id INT, val INT) PARTITION BY RANGE (id); +CREATE TABLE rpr_part_1 PARTITION OF rpr_part FOR VALUES FROM (1) TO (6); +CREATE TABLE rpr_part_2 PARTITION OF rpr_part FOR VALUES FROM (6) TO (11); +INSERT INTO rpr_part SELECT id, val FROM rpr_integ; + +-- Plan: partition scans are combined with Append (or Merge Append), +-- sorted into a single ordered stream, and fed into one WindowAgg +-- that performs RPR pattern matching across the combined stream. +EXPLAIN (COSTS OFF) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_part +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + +-- Baseline: the same query against the non-partitioned rpr_integ +-- produces the per-row reference output. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + +-- Result against the partitioned table must match the baseline +-- row-for-row. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_part +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id; + +DROP TABLE rpr_part; + +-- ============================================================ +-- B6. RPR + LATERAL +-- ============================================================ +-- RPR inside a LATERAL subquery. Qualified column references from +-- the outer query are not yet supported in DEFINE, so this tests +-- the basic case where LATERAL provides the correlation filter +-- (WHERE id <= o.id) and DEFINE uses only local columns. The plan +-- must show a Nested Loop driving the outer relation into the inner +-- subquery scan, with the RPR WindowAgg re-executed for each outer +-- row and the correlation surfacing as a scan-level Filter on +-- "id <= o.id". + +-- Plan: Nested Loop with the RPR WindowAgg in the inner leg, driven +-- by the filtered outer rows (o.id IN (5, 10)). +EXPLAIN (COSTS OFF) +SELECT o.id AS outer_id, r.id, r.cnt +FROM rpr_integ o, +LATERAL ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WHERE id <= o.id + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) r +WHERE r.cnt > 0 AND o.id IN (5, 10) +ORDER BY o.id, r.id; + +-- Result: for each of the two outer ids (5 and 10), the LATERAL +-- subquery produces RPR match counts over the restricted input. +SELECT o.id AS outer_id, r.id, r.cnt +FROM rpr_integ o, +LATERAL ( + SELECT id, count(*) OVER w AS cnt + FROM rpr_integ + WHERE id <= o.id + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +) r +WHERE r.cnt > 0 AND o.id IN (5, 10) +ORDER BY o.id, r.id; + +-- ============================================================ +-- B7. RPR + Recursive CTE +-- ============================================================ +-- Verify that RPR is rejected inside a recursive query. +-- ISO/IEC 19075-5 6.17.5 (R020) and 4.18.5 (R010) cite CREATE +-- RECURSIVE VIEW examples and state that "row pattern matching +-- is prohibited in recursive queries". The formal rule lives in +-- ISO/IEC 9075-2:2016 7.17 Syntax Rule 3)f): a potentially +-- recursive shall not contain a or . Per 3)e), every +-- under WITH RECURSIVE is "potentially +-- recursive", so the rejection covers the base (non-recursive) +-- leg too, not just the self-referencing leg. + +-- WITH RECURSIVE: RPR in the base leg is rejected even though the +-- base leg never references the recursive CTE name. +WITH RECURSIVE seq AS ( + SELECT id, val, count(*) OVER w AS cnt + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) + UNION ALL + SELECT id + 100, val, cnt FROM seq WHERE id < 3 +) +SELECT id, val, cnt FROM seq ORDER BY id; + +-- CREATE RECURSIVE VIEW: rewritten by makeRecursiveViewSelect() +-- into WITH RECURSIVE, so the same rejection applies. This is +-- the form ISO/IEC 19075-5 6.17.5 cites verbatim. +CREATE RECURSIVE VIEW rpr_recv(id, val, cnt) AS + SELECT id, val, count(*) OVER w + FROM rpr_integ + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)); + +-- ============================================================ +-- B8. RPR + Incremental sort +-- ============================================================ +-- Verify that RPR pattern matching works correctly when the input +-- to WindowAgg arrives via an incremental sort. The index on (id) +-- provides presorted input for the first ORDER BY key, so +-- "ORDER BY id, val" lets the planner use Incremental Sort to order +-- only on the second key. The plan must show Incremental Sort +-- below the RPR WindowAgg, and RPR must produce the same per-row +-- match counts as it would with a plain Sort. + +CREATE INDEX rpr_integ_id_idx ON rpr_integ (id); +SET enable_seqscan = off; + +-- Plan: RPR WindowAgg above an Incremental Sort above an Index Scan. +-- The Incremental Sort declares "Presorted Key: id" and sorts only +-- on val within each id group. +EXPLAIN (COSTS OFF) +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id, val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)); + +-- Result: RPR over the incrementally sorted stream produces match +-- counts per row. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id, val + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) +ORDER BY id, val; + +RESET enable_seqscan; +DROP INDEX rpr_integ_id_idx; + +-- ============================================================ +-- B9. RPR + Volatile function in DEFINE +-- ============================================================ +-- Volatile functions in DEFINE are rejected in the planner. Under +-- RPR's NFA engine the same row's DEFINE predicate may be evaluated +-- multiple times (backtracking, PREV/NEXT navigation), so a volatile +-- result would make pattern matching non-deterministic. STABLE and +-- IMMUTABLE callees are accepted. + +-- Baseline: STABLE (to_char) and IMMUTABLE (length) callees are accepted. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val) + AND length('x') = 1 + AND to_char(date '2026-01-01', 'YYYY') = '2026') +ORDER BY id; + +-- Volatile (random) is rejected. +SELECT id, val, count(*) OVER w AS cnt +FROM rpr_integ +WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val) AND random() >= 0.0) +ORDER BY id; + +-- ============================================================ +-- B10. RPR + Correlated subquery in WHERE +-- ============================================================ +-- Verify that an RPR window placed inside a correlated scalar +-- subquery is executed once per outer row. DEFINE still references +-- only local columns (qualified refs from the outer query are not +-- supported in DEFINE); the correlation lives in the subquery's +-- WHERE clause as "i.id <= o.id". The plan must show a SubPlan +-- attached to the outer scan, with the RPR WindowAgg driven by a +-- per-row scan filter carrying the correlation predicate. + +-- Plan: SubPlan attached to the outer Seq Scan; the inner scan +-- carries "Filter: (id <= o.id)", confirming the correlation is +-- evaluated per outer row. +EXPLAIN (COSTS OFF) +SELECT o.id, o.val, + (SELECT count(*) OVER w + FROM rpr_integ i + WHERE i.id <= o.id + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) + ORDER BY id + LIMIT 1) AS first_cnt +FROM rpr_integ o +ORDER BY o.id; + +-- Result: each outer row receives the first_cnt from its own +-- correlated RPR subquery. +SELECT o.id, o.val, + (SELECT count(*) OVER w + FROM rpr_integ i + WHERE i.id <= o.id + WINDOW w AS (ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A B+) + DEFINE B AS val > PREV(val)) + ORDER BY id + LIMIT 1) AS first_cnt +FROM rpr_integ o +ORDER BY o.id; + +-- A column referenced only by DEFINE must not keep an unrelated column that +-- merely shares its attribute number. DEFINE references a (rpr_over1); c +-- (rpr_over2) has the same attno but is unused, so it must be dropped. +CREATE TABLE rpr_over1 (a int); +CREATE TABLE rpr_over2 (c int); +INSERT INTO rpr_over1 VALUES (1),(2),(3); +INSERT INTO rpr_over2 VALUES (1),(2),(3); +EXPLAIN (VERBOSE, COSTS OFF) +SELECT cnt FROM ( + SELECT a AS oa, c AS oc, count(*) OVER w AS cnt + FROM rpr_over1 CROSS JOIN rpr_over2 + WINDOW w AS (ORDER BY a ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (X+) DEFINE X AS a > 0) +) s; +DROP TABLE rpr_over1, rpr_over2; + +-- Cleanup +DROP TABLE rpr_integ; +DROP TABLE rpr_integ2; diff --git a/src/test/regress/sql/rpr_nfa.sql b/src/test/regress/sql/rpr_nfa.sql new file mode 100644 index 00000000000..79f99922870 --- /dev/null +++ b/src/test/regress/sql/rpr_nfa.sql @@ -0,0 +1,3675 @@ +-- ============================================================ +-- RPR NFA Tests +-- Tests for Row Pattern Recognition NFA Runtime Execution +-- ============================================================ +-- +-- This test suite validates the NFA (Non-deterministic Finite +-- Automaton) runtime execution engine in nodeWindowAgg.c, +-- focusing on update_reduced_frame and related functions. +-- +-- Test Strategy: +-- Diagonal pattern style using ARRAY flags to explicitly +-- control which pattern variables match at each row. +-- +-- Test Coverage: +-- Basic NFA Flow (match->absorb->advance) +-- Absorption Optimization +-- Context Lifecycle Management +-- Advance Phase (Epsilon Transitions) +-- Match Phase (Variable Matching) +-- Frame Boundary Handling +-- State Management (Deduplication) +-- Statistics and Diagnostics +-- Quantifier Runtime Behavior +-- Pathological Pattern Protection +-- Alternation Runtime Behavior +-- Deep Nested Groups +-- SKIP Options (Runtime) +-- INITIAL Mode (Runtime) +-- Frame Boundary Variations +-- Special Partition Cases +-- DEFINE Special Cases +-- Absorption Dynamic Flags +-- Zero-Consumption Cycle Detection +-- +-- Responsibility: +-- - NFA runtime execution paths +-- - Context/State lifecycle management +-- - Runtime boundary conditions and protections +-- +-- NOT tested here (covered in other files): +-- - Pattern parsing/optimization (rpr_base.sql) +-- - EXPLAIN output (rpr_explain.sql) +-- - PREV/NEXT semantics (rpr.sql) +-- ============================================================ + +-- ============================================================ +-- Basic NFA Flow +-- ============================================================ + +-- Simple sequential pattern +WITH test_sequential AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['_']) -- No match + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_sequential +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- Quantified pattern (A+ B+ C+) +WITH test_quantified AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['B']), + (6, ARRAY['C']), + (7, ARRAY['C']), + (8, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_quantified +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B+ C+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- Optional pattern (A B? C) +WITH test_optional AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['C']), -- B skipped + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['C']), -- B matched + (6, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_optional +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B? C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- Alternation pattern (A (B|C) D) +WITH test_alternation AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), -- First branch + (3, ARRAY['D']), + (4, ARRAY['A']), + (5, ARRAY['C']), -- Second branch + (6, ARRAY['D']), + (7, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alternation +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A (B | C) D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- ============================================================ +-- Absorption Optimization +-- ============================================================ + +-- Absorbable pattern (A+) +WITH test_absorbable AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorbable +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + +-- Mixed absorbable/non-absorbable ((A+) | B) +WITH test_mixed_absorption AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_mixed_absorption +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A+) | B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- State coverage (same elemIdx, different count) +WITH test_state_coverage AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_state_coverage +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{2,} B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Reluctant pattern (A+?) - not absorbable +-- Compare with greedy A+ above: reluctant excluded from absorption. +-- Each context produces minimum match independently. +WITH test_reluctant_absorption AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_absorption +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+?) + DEFINE + A AS 'A' = ANY(flags) +); + +-- Absorption with fixed suffix: A+ B +WITH test_absorb_suffix AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_suffix +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Per-branch absorption with ALT: B+ C | B+ D +WITH test_absorb_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, ARRAY['B']), + (4, ARRAY['D']), + (5, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_alt +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (B+ C | B+ D) + DEFINE + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- Non-absorbable: A B+ (unbounded not in first position) +WITH test_no_absorb AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['B']), + (4, ARRAY['B']), + (5, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_no_absorb +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- GROUP merge enables absorption: (A B) (A B)+ optimized to (A B){2,} +WITH test_absorb_group AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_group +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A B) (A B)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Two consecutive unbounded groups: (A B)+ (C D)+ +-- The leading group (A B)+ is absorbable (unbounded multi-element); (C D)+ is +-- a distinct sibling group that does not merge with it. When the leading group +-- exits into the sibling, its body leaf-VAR count must be cleared so it does +-- not leak into the sibling's shared depth slot. +WITH test_absorb_two_groups AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['C']), + (8, ARRAY['D']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_two_groups +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ (C D)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- Fixed-length group absorption: (A B{2})+ C +-- B{2} has min == max, equivalent to unrolling to (A B B)+ C +WITH test_absorb_fixedlen AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['B']), + (4, ARRAY['A']), + (5, ARRAY['B']), + (6, ARRAY['B']), + (7, ARRAY['A']), + (8, ARRAY['B']), + (9, ARRAY['B']), + (10, ARRAY['C']), + (11, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_fixedlen +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B{2})+ C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- Consecutive vars merged to fixed-length: (A B B)+ -> (A B{2})+ +WITH test_absorb_consecutive AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['B']), + (4, ARRAY['A']), + (5, ARRAY['B']), + (6, ARRAY['B']), + (7, ARRAY['A']), + (8, ARRAY['B']), + (9, ARRAY['B']), + (10, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_consecutive +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B B)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Nested fixed-length group absorption: (A (B C){2} D)+ E +-- Inner group {2} has min == max; absorbable via recursive check +-- step_size = 1 + (1+1)*2 + 1 = 6 +WITH test_absorb_nested_fixedlen AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['B']), + (5, ARRAY['C']), + (6, ARRAY['D']), + (7, ARRAY['A']), + (8, ARRAY['B']), + (9, ARRAY['C']), + (10, ARRAY['B']), + (11, ARRAY['C']), + (12, ARRAY['D']), + (13, ARRAY['E']), + (14, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_nested_fixedlen +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A (B C){2} D)+ E) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags) +); + +-- Doubly nested fixed-length group absorption: (A ((B C{3}){2} D){2} E)+ F +-- step_size = 1 + ((1+3)*2+1)*2 + 1 = 20; 2 iterations + F = 41 rows +WITH test_absorb_doubly_nested AS ( + SELECT v AS id, ARRAY[ + CASE + WHEN v % 41 IN (1, 21) THEN 'A' + WHEN v % 41 IN (2, 6, 11, 15, 22, 26, 31, 35) THEN 'B' + WHEN v % 41 IN (3,4,5, 7,8,9, 12,13,14, 16,17,18, + 23,24,25, 27,28,29, 32,33,34, 36,37,38) THEN 'C' + WHEN v % 41 IN (10, 19, 30, 39) THEN 'D' + WHEN v % 41 IN (20, 40) THEN 'E' + WHEN v % 41 = 0 THEN 'F' + ELSE 'X' + END + ] AS flags + FROM generate_series(1, 82) AS s(v) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_doubly_nested +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A ((B C C C){2} D){2} E)+ F) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags), + F AS 'F' = ANY(flags) +); + +-- 3-level END chain: ((A (B C){2}){2})+ +-- Tests END(BC{2}) -> END(A..{2}) -> END(+) chaining +-- 2 iterations of +, each 10 rows: (A B C B C)(A B C B C) +WITH test_absorb_3level_end AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), -- 1st + iter, 1st {2}, A + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['B']), + (5, ARRAY['C']), -- 1st (BC){2} done + (6, ARRAY['A']), -- 1st + iter, 2nd {2}, A + (7, ARRAY['B']), + (8, ARRAY['C']), + (9, ARRAY['B']), + (10, ARRAY['C']), -- 2nd (BC){2} done, 1st {2} done, 1st + iter done + (11, ARRAY['A']), -- 2nd + iter, 1st {2}, A + (12, ARRAY['B']), + (13, ARRAY['C']), + (14, ARRAY['B']), + (15, ARRAY['C']), + (16, ARRAY['A']), -- 2nd + iter, 2nd {2}, A + (17, ARRAY['B']), + (18, ARRAY['C']), + (19, ARRAY['B']), + (20, ARRAY['C']), -- 2nd + iter done + (21, ARRAY['X']) -- no match, + ends + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_absorb_3level_end +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A (B C){2}){2})+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- Multiple unbounded: A+ B+ (first element unbounded enables absorption) +WITH test_multi_unbounded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['B']), + (5, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_multi_unbounded +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- Context Lifecycle +-- ============================================================ + +-- Multiple overlapping contexts (SKIP TO NEXT ROW) +WITH test_overlapping_contexts AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_overlapping_contexts +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Failed context cleanup (early failure) +WITH test_context_cleanup AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), -- Pruned at first row + (2, ARRAY['A']), + (3, ARRAY['_']), -- Mismatched after row 2 + (4, ARRAY['A']), + (5, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_context_cleanup +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Partition end (incomplete contexts) +WITH test_partition_end AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']) + -- Pattern requires B, but partition ends + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_partition_end +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Completed context encountered during processing +-- Pattern (A | B C D): Ctx1 takes long B->C->D path, while Ctx2 takes +-- short A path and completes first. Next row sees Ctx2 +-- with states=NULL and skips it. +WITH test_completed_ctx AS ( + SELECT * FROM (VALUES + (1, ARRAY['B', '_']), + (2, ARRAY['C', 'A']), + (3, ARRAY['D', '_']), + (4, ARRAY['_', '_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_completed_ctx +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A | B C D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- Reluctant context lifecycle (A+? B with SKIP TO NEXT ROW) +-- A+? exits early but if B not available, falls back to loop. +-- Contexts not absorbed (reluctant), so multiple survive. +WITH test_reluctant_context AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_context +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- Advance Phase (Epsilon Transitions) +-- ============================================================ + +-- Nested groups ((A B)+) +WITH test_nested_groups AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_groups +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A B)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Multiple alternation branches (A (B|C|D) E) +WITH test_multi_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['E']), + (4, ARRAY['A']), + (5, ARRAY['C']), + (6, ARRAY['E']), + (7, ARRAY['A']), + (8, ARRAY['D']), + (9, ARRAY['E']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_multi_alt +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A (B | C | D) E) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags) +); + +-- Optional VAR at start (A? B C) +WITH test_optional_var AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), -- A skipped + (2, ARRAY['C']), + (3, ARRAY['A']), -- A matched + (4, ARRAY['B']), + (5, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_optional_var +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A? B C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- Nested alternation ((A|B) (C|D)) +WITH test_nested_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['C']), -- A C + (3, ARRAY['A']), + (4, ARRAY['D']), -- A D + (5, ARRAY['B']), + (6, ARRAY['C']), -- B C + (7, ARRAY['B']), + (8, ARRAY['D']) -- B D + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_alt +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B) (C | D)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- Mixed greedy/reluctant sequence: A+? B+ (reluctant A, greedy B) +-- A exits as early as possible, B consumes the rest greedily +WITH test_mixed_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A','B']), + (4, ARRAY['B']), + (5, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_mixed_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+? B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Optional reluctant group: (A B)?? C +-- Reluctant group entry tries skip first, but the skip path needs C +-- at row 1 which is A -> skip fails. Enter path succeeds: A(1) B(2) C(3). +WITH test_optional_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_optional_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)?? C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- Non-leading reluctant optional VAR: (B A?? C) +-- Reluctant A?? should prefer to skip, matching B(1) C(2) with A left +-- unmatched (match_end 2). The leading/group reluctant cases above go through +-- the begin path; this exercises the non-leading skip path, +-- which must honor reluctant ordering too. +WITH test_nonleading_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['A', 'C']), + (3, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nonleading_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (B A?? C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- Reluctant outer quantifier over a nullable reluctant body: SQL/RPR +-- semantics call for the shortest (empty) match. In the count=2 boundary and single-quantifier controls localize the behaviour: only +-- the all-reluctant case (rr) should differ. +WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, true), (4, false)) +SELECT id, + count(*) OVER gg AS gg, -- (A?)+ greedy / greedy + count(*) OVER gr AS gr, -- (A??)+ greedy / reluctant + count(*) OVER rg AS rg, -- (A?)+? reluctant / greedy + count(*) OVER rr AS rr, -- (A??)+? reluctant / reluctant + count(*) OVER rr2 AS rr2, -- (A??){2,}? reluctant, min>=2 boundary + count(*) OVER ca AS ca, -- A?? single reluctant control + count(*) OVER cs AS cs -- A*? single reluctant control +FROM t +WINDOW gg AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+) DEFINE A AS isa), + gr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+) DEFINE A AS isa), + rg AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A?)+?) DEFINE A AS isa), + rr AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??)+?) DEFINE A AS isa), + rr2 AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN ((A??){2,}?) DEFINE A AS isa), + ca AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A??) DEFINE A AS isa), + cs AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING PATTERN (A*?) DEFINE A AS isa) +ORDER BY id; + +-- Doubly-nested reluctant nullable group: (((A??){2,}?){2,}?). Reluctant +-- quantifiers disable optimizer flattening, so both levels survive and the +-- inner group's END->next lands on the outer END. This exercises the +-- END->END count increment in the EMPTY_LOOP fast-forward (count < min). +WITH t(id, isa) AS (VALUES (1, true), (2, true), (3, false)) +SELECT id, count(*) OVER w AS c +FROM t +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (((A??){2,}?){2,}?) + DEFINE A AS isa +) +ORDER BY id; + +-- Non-leading reluctant optional GROUP with a follower: (B (A X)?? C) +-- Like the VAR case above but a multi-element group; it goes through the +-- begin path, which already honors reluctant ordering. +-- Reluctant (A X)?? should skip, matching B(1) C(2), with the group skipped +-- to the following C (not to FIN). +WITH test_nonleading_reluctant_group AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['A', 'C']), + (3, ARRAY['X']), + (4, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nonleading_reluctant_group +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (B (A X)?? C) + DEFINE + A AS 'A' = ANY(flags), + X AS 'X' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- Reluctant nullable group with a required follower: ((A??){2,}? B). +-- min=2 forces the reluctant fast-forward to loop back (it cannot exit to +-- FIN until B matches), exercising the loop-back route_to_elem second call +-- site in nfa_advance_end. B fails at the group's exit row, so only the +-- first match survives. +WITH test_reluctant_nullable_follower AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_nullable_follower +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A??){2,}? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Greedy/reluctant sequence: A+ B+? (greedy A, reluctant B at end) +-- A consumes greedily, B+? exits to FIN after minimum match +WITH test_greedy_then_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A','B']), + (3, ARRAY['B']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_greedy_then_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B+?) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Reluctant optional group skip-to-FIN +-- When a reluctant optional group's skip path reaches FIN, the group +-- entry path is abandoned. +-- Pattern: C (A B)?? -- after C matches, the reluctant group (A B)?? +-- prefers to skip. Skip goes to FIN (group is last element), so +-- the match completes with just C. +WITH test_begin_skip_fin AS ( + SELECT * FROM (VALUES + (1, ARRAY['C']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['C','A']), + (5, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_begin_skip_fin +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (C (A B)??) + DEFINE + C AS 'C' = ANY(flags), + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- Match Phase +-- ============================================================ + +-- Simple VAR with END next (A B C all min=max=1) +WITH test_simple_var AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_simple_var +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- VAR max exceeded (A{2,3}) +WITH test_max_exceeded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), -- Max = 3 + (4, ARRAY['A']), -- Exceeds max, state removed + (5, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_max_exceeded +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{2,3} B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Non-matching VAR (DEFINE false) +WITH test_non_matching AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['_']), -- B not matched (DEFINE false) + (3, ARRAY['A']), + (4, ARRAY['B']), -- B matched + (5, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_non_matching +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- ============================================================ +-- Frame Boundary Handling +-- ============================================================ + +-- Limited frame (ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING) +WITH test_limited_frame AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), -- Within 3 FOLLOWING + (5, ARRAY['B']), -- Beyond 3 FOLLOWING from row 1 + (6, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_limited_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Unbounded frame (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) +WITH test_unbounded_frame AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['B']) -- Far from start, but unbounded + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_unbounded_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Match exceeds frame boundary +WITH test_frame_exceeded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']) + -- Frame ends at row 3 (2 FOLLOWING), B never appears + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_frame_exceeded +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Frame boundary forced mismatch +-- Limited frame with enough rows so that a context's frame boundary +-- is exceeded while still processing. +WITH test_frame_boundary AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_frame_boundary +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Reluctant with limited frame (A+? B with 2 FOLLOWING) +-- Reluctant exits early, B must be within frame boundary +WITH test_reluctant_frame AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_frame +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- State Management +-- ============================================================ + +-- Duplicate state creation +WITH test_duplicate_states AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', 'B']), -- Both A and B match (creates duplicate states via different paths) + (2, ARRAY['C', '_']), + (3, ARRAY['D', '_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_duplicate_states +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B) C D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- Reluctant duplicate state handling +-- (A+? | B+?) creates exit and loop states; exit paths may converge +WITH test_reluctant_dedup AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['A','B']), + (3, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_dedup +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A+? | B+?)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Large pattern (stress free list) +WITH test_large_pattern AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['E']), + (6, ARRAY['F']), + (7, ARRAY['G']), + (8, ARRAY['H']), + (9, ARRAY['I']), + (10, ARRAY['J']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_large_pattern +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C D E F G H I J) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags), + F AS 'F' = ANY(flags), + G AS 'G' = ANY(flags), + H AS 'H' = ANY(flags), + I AS 'I' = ANY(flags), + J AS 'J' = ANY(flags) +); + +-- Reduced frame map reallocation (> 1024 rows) +WITH test_map_realloc AS ( + SELECT id, CASE WHEN id % 2 = 1 THEN ARRAY['A'] ELSE ARRAY['B'] END AS flags + FROM generate_series(1, 1100) AS id +) +SELECT count(*), min(match_start), max(match_end) +FROM ( + SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end + FROM test_map_realloc + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) + ) +) sub; + +-- ============================================================ +-- Statistics and Diagnostics +-- ============================================================ + +-- Matched contexts +WITH test_matched AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_matched +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Pruned contexts (failed at first row) +WITH test_pruned AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), -- Pruned + (2, ARRAY['_']), -- Pruned + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_pruned +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Mismatched contexts (failed after multiple rows) +WITH test_mismatched AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['_']), -- Mismatched after 2 rows + (4, ARRAY['A']), + (5, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_mismatched +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Reluctant not absorbed (A+? with SKIP TO NEXT ROW) +-- Compare with greedy A+ below: reluctant is not absorbable, +-- so all contexts survive independently. +WITH test_reluctant_stats AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_stats +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+?) + DEFINE + A AS 'A' = ANY(flags) +); + +-- Absorbed contexts +WITH test_absorbed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorbed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + +-- Skipped contexts (SKIP TO NEXT ROW) +WITH test_skipped AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']) -- Completes match starting at row 1 + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_skipped +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- Quantifier Runtime Behavior +-- ============================================================ + +-- Large count handling (A{100}) +WITH test_large_count AS ( + SELECT i AS id, ARRAY['A'] AS flags + FROM generate_series(1, 105) i +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_large_count +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{100}) + DEFINE + A AS 'A' = ANY(flags) +); + +-- Unlimited quantifier (A{10,}) +WITH test_unlimited AS ( + SELECT i AS id, ARRAY['A'] AS flags + FROM generate_series(1, 15) i + UNION ALL + SELECT 16, ARRAY['B'] +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_unlimited +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{10,} B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Min boundary (A{3,5}) +WITH test_min_boundary AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), -- Min=3 reached, exit path available + (4, ARRAY['B']), -- Match ends at min + (5, ARRAY['A']), + (6, ARRAY['A']), + (7, ARRAY['A']), + (8, ARRAY['A']), + (9, ARRAY['A']), -- Count=5, max reached + (10, ARRAY['B']) -- Match ends at max + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_min_boundary +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{3,5} B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Max boundary exceeded (A{3,5}) +WITH test_max_boundary AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['A']), -- Count=6 > max=5, row 1 context removed + (7, ARRAY['B']) -- Row 1 context: no match (exceeded max) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_max_boundary +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A{3,5} B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Greedy vs reluctant: A+ matches all rows, A+? matches minimum +WITH test_greedy_vs_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','_']), + (2, ARRAY['A','_']), + (3, ARRAY['A','B']), + (4, ARRAY['B','_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_greedy_vs_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Same data, reluctant A+? exits at row 3 where B is first available +WITH test_greedy_vs_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','_']), + (2, ARRAY['A','_']), + (3, ARRAY['A','B']), + (4, ARRAY['B','_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_greedy_vs_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Reluctant group: (A B)+? matches minimum 1 iteration +WITH test_reluctant_group AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_group +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+?) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- A+? B (reluctant plus): exits A at first B availability +-- (Same scenario as greedy-vs-reluctant comparison above; retained for +-- standalone quantifier coverage alongside A{1,3}? and A{2,3}? below) +WITH test_reluctant_plus AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','_']), + (2, ARRAY['A','_']), + (3, ARRAY['A','B']), + (4, ARRAY['B','_']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_reluctant_plus +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- A{1,3}? B (reluctant bounded): same data, bounded quantifier +WITH test_reluctant_bounded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','_']), + (2, ARRAY['A','_']), + (3, ARRAY['A','B']), + (4, ARRAY['B','_']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_reluctant_bounded +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{1,3}? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- A{3,5}? B (reluctant bounded mid-band): the VAR-level count +-- cycles through 3, 4, 5 within a single match attempt. Exercises +-- a reluctant bounded quantifier that absorbability analysis excludes +-- (reluctant quantifiers are never absorbable, so A stays +-- non-absorbable). +WITH test_reluctant_mid_band AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_mid_band +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{3,5}? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Nested quantifier flattening must not widen the matching language (H-1). +-- (A{k,})* with k >= 2 reaches repetition counts {0} UNION [k, INF); the gap +-- 1..k-1 is unreachable, so it must NOT collapse to A*. An isolated single A +-- must yield an EMPTY match (count 0), not a length-1 match. +WITH test_nested_quant_var AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), -- isolated A: (A{2,})* matches empty here, not 1 + (2, ARRAY['_']), + (3, ARRAY['A']), + (4, ARRAY['A']), -- run of 2: matched + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end, + count(*) OVER w AS match_count +FROM test_nested_quant_var +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A{2,})*) + DEFINE A AS 'A' = ANY(flags) +); + +-- Same for a GROUP child: ((A B){2,})* must not collapse to (A B)*. +-- An isolated single (A B) pair must yield an EMPTY match (count 0). +WITH test_nested_quant_group AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), -- isolated (A B) pair: matches empty here + (2, ARRAY['B']), + (3, ARRAY['_']), + (4, ARRAY['A']), + (5, ARRAY['B']), + (6, ARRAY['A']), + (7, ARRAY['B']), -- run of 2 pairs: matched + (8, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end, + count(*) OVER w AS match_count +FROM test_nested_quant_group +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A B){2,})*) + DEFINE A AS 'A' = ANY(flags), B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- Pathological Pattern Runtime Protection +-- ============================================================ + +-- Complex nested nullable ((A* B*)*) - Runtime protection +WITH test_complex_nested AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['B']), + (5, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_complex_nested +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A* B*)*) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Nested nullable with quantifier ((A{0,3})*) +WITH test_nested_quantifier AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_quantifier +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A{0,3})*) + DEFINE + A AS 'A' = ANY(flags) +); + +-- Reluctant nullable: A*? (prefers 0 matches) +-- A*? always takes skip path (0 iterations preferred) +WITH test_reluctant_nullable AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_nullable +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A*? B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- Alternation Runtime Behavior +-- ============================================================ + +-- Multi-branch alternation (A (B|C|D|E) F) +WITH test_multi_branch AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['F']), + (4, ARRAY['A']), + (5, ARRAY['C']), + (6, ARRAY['F']), + (7, ARRAY['A']), + (8, ARRAY['D']), + (9, ARRAY['F']), + (10, ARRAY['A']), + (11, ARRAY['E']), + (12, ARRAY['F']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_multi_branch +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A (B | C | D | E) F) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags), + F AS 'F' = ANY(flags) +); + +-- Alternation with quantifiers (A+ | B+ | C+) +WITH test_alt_quantifiers AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['B']), + (6, ARRAY['C']), + (7, ARRAY['C']), + (8, ARRAY['C']), + (9, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_quantifiers +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ | B+ | C+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- altPriority replacement (A B C | D) +-- D branch (higher altPriority) matches first at row 1, +-- then A B C branch (lower altPriority) replaces it at row 3. +WITH test_alt_replace AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', 'D']), + (2, ARRAY['B', '_']), + (3, ARRAY['C', '_']), + (4, ARRAY['_', '_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_replace +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C | D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- ALT lexical order takes priority over greedy (longer match). +-- Row 1 matches both A and B; A wins by lexical order (match 1-1). +WITH test_alt_lexical_order AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), -- A and B both match + (2, ARRAY['_','C']) -- only C matches (would continue B C) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_lexical_order +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B C)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- ALT with reluctant: (A+? | B+) - A branch is reluctant, B is greedy. +-- Row 1 matches both A and B. A+? exits immediately (match 1-1). +WITH test_alt_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['B','_']), + (3, ARRAY['B','_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A+? | B+)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Optional first branch in ALT with quantifier: (A? | B){1,2} +-- First branch A? exit path may loop back to ALT and trigger cycle +-- detection during DFS. All branches must receive correct counts. +WITH test_alt_opt_first AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_opt_first +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A? | B){1,2})) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Mixed A/B rows across iterations of (A? | B){1,2} +WITH test_alt_opt_mixed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A','B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_opt_mixed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A? | B){1,2})) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Reluctant variant: (A?? | B){1,2} +WITH test_alt_opt_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_opt_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A?? | B){1,2})) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Overlapping match: A B C D E | B C D | C D E F (SKIP PAST LAST ROW) +WITH test_overlap1 AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['E']), + (6, ARRAY['F']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_overlap1 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E | B C D | C D E F) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags), + F AS 'F' = ANY(flags) +); + +-- Same with SKIP TO NEXT ROW: three overlapping matches +WITH test_overlap1 AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['E']), + (6, ARRAY['F']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_overlap1 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C D E | B C D | C D E F) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags), + F AS 'F' = ANY(flags) +); + +-- Longer pattern fails, shorter survives: A+ B C D E | B+ C +WITH test_overlap1b AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_overlap1b +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ B C D E | B+ C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags) +); + +-- Long B sequence with different endings: A B+ C | B+ D +WITH test_overlap2 AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['B']), + (4, ARRAY['B']), + (5, ARRAY['B']), + (6, ARRAY['C']), + (7, ARRAY['B']), + (8, ARRAY['B']), + (9, ARRAY['B']), + (10, ARRAY['D']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_overlap2 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B+ C | B+ D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- Greedy with late failure ("betrayal"): A B C+ D | A B +WITH test_betrayal AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['C']), + (5, ARRAY['C']), + (6, ARRAY['E']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_betrayal +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C+ D | A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- Multiple TRUE per row: overlapping pattern variables +WITH test_multi_true AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['B','C']), + (3, ARRAY['C','D']), + (4, ARRAY['D','E']), + (5, ARRAY['E','_']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_multi_true +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C D E) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags), + E AS 'E' = ANY(flags) +); + +-- Diagonal pattern with shifted multi-TRUE overlap +WITH test_diagonal AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','_']), + (2, ARRAY['B','A']), + (3, ARRAY['C','B']), + (4, ARRAY['D','C']), + (5, ARRAY['_','D']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_diagonal +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- ((A | B) C)+ - alternation inside group with outer quantifier +WITH test_alt_group AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['C']), + (3, ARRAY['B']), + (4, ARRAY['C']), + (5, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_alt_group +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B) C)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- ============================================================ +-- Deep Nested Groups +-- ============================================================ + +-- Three-level nesting ((((A B)+)+)+) +WITH test_deep_nesting AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_deep_nesting +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((((A B)+)+)+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Multiple groups in nesting (((A B) (C D))+) +WITH test_nested_sequential AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['C']), + (8, ARRAY['D']), + (9, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_sequential +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A B) (C D))+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- Nested END->END max reached +-- Inner group (A B){2} reaches max=2 -> exits to outer END +WITH test_end_nested_max AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['A']), + (8, ARRAY['B']), + (9, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_end_nested_max +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A B){2})+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Nested END->END between min/max +-- Inner group (A B){1,3} exits between min/max -> outer END count++ +WITH test_end_nested_mid AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']), + (7, ARRAY['A']), + (8, ARRAY['B']), + (9, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_end_nested_mid +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A B){1,3})+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Nested reluctant group ((A B)+?) with following element C +-- Inner group exits after minimum 1 iteration +WITH test_nested_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A B)+? C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- (A B){2} - group with exact quantifier +WITH test_group_exact AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['X']) + ) AS t(id, flags) +) +SELECT id, flags, first_value(id) OVER w AS match_start, last_value(id) OVER w AS match_end +FROM test_group_exact +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B){2}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Nested END->END fast-forward +-- When an inner group has a nullable body and count < min, the +-- fast-forward path exits through the outer END, incrementing +-- the outer group's count. +-- Pattern: ((A?){2,3}){2,3} -- nested groups, neither collapses +-- because the optimizer cannot safely multiply non-exact quantifiers. +-- Data has no A rows, forcing all-empty iterations via fast-forward. +WITH test_nested_ff AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_nested_ff +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (((A?){2,3}){2,3}) + DEFINE + A AS 'A' = ANY(flags) +); + +-- ============================================================ +-- SKIP Options (Runtime) +-- ============================================================ + +-- SKIP PAST LAST ROW (non-overlapping matches) +WITH test_skip_past AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_skip_past +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + +-- SKIP TO NEXT ROW (overlapping matches) +WITH test_skip_next AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_skip_next +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + +-- SKIP difference verification +WITH test_skip_diff AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT 'SKIP PAST' AS mode, id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_skip_diff +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +) +UNION ALL +SELECT 'SKIP NEXT' AS mode, id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_skip_diff +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +) +ORDER BY mode, id; + +-- Reluctant SKIP comparison: A+? with SKIP PAST vs SKIP NEXT +WITH test_reluctant_skip AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT 'SKIP PAST' AS mode, id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_skip +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+?) + DEFINE + A AS 'A' = ANY(flags) +) +UNION ALL +SELECT 'SKIP NEXT' AS mode, id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_skip +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+?) + DEFINE + A AS 'A' = ANY(flags) +) +ORDER BY mode, id; + +-- ============================================================ +-- INITIAL Mode (Runtime) +-- ============================================================ + +-- Explicit INITIAL (after AFTER MATCH SKIP, per the grammar); same as the default +WITH test_initial_mode AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), -- Unmatched + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['_']), -- Unmatched + (5, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_initial_mode +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + +-- Default mode (include all rows) +WITH test_default_mode AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), -- Unmatched, but included + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['_']), -- Unmatched, but included + (5, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_default_mode +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + +-- Mode equivalence verification: explicit INITIAL equals the default mode +WITH test_mode_diff AS ( + SELECT * FROM (VALUES + (1, ARRAY['_']), + (2, ARRAY['A']), + (3, ARRAY['_']) + ) AS t(id, flags) +) +SELECT 'INITIAL' AS mode, COUNT(*) AS row_count +FROM ( + SELECT id FROM test_mode_diff + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + INITIAL + PATTERN (A) + DEFINE A AS 'A' = ANY(flags) + ) +) sub +UNION ALL +SELECT 'DEFAULT' AS mode, COUNT(*) AS row_count +FROM ( + SELECT id FROM test_mode_diff + WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE A AS 'A' = ANY(flags) + ) +) sub +ORDER BY mode; + +-- ============================================================ +-- Frame Boundary Variations +-- ============================================================ + +-- Very limited frame (1 FOLLOWING) +WITH test_one_following AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), -- Within 1 FOLLOWING + (3, ARRAY['A']), -- Beyond 1 FOLLOWING from row 1 + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_one_following +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Medium frame (10 FOLLOWING) +WITH test_ten_following AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']), + (6, ARRAY['A']), + (7, ARRAY['A']), + (8, ARRAY['A']), + (9, ARRAY['A']), + (10, ARRAY['A']), + (11, ARRAY['B']), -- Within 10 FOLLOWING from row 1 + (12, ARRAY['A']), + (13, ARRAY['B']) -- Beyond 10 FOLLOWING from row 1 + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_ten_following +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Exact boundary match +WITH test_exact_boundary AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['B']) -- Exactly at 4 FOLLOWING (frame end) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_exact_boundary +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 4 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- N FOLLOWING + SKIP TO NEXT ROW: overlapping matches bounded by frame +-- Row 1: frame [1,4], A(1-3) B(4) -> match +-- Row 2: frame [2,5], A(2-3) B(4) -> match +-- Row 3: frame [3,6], A(3) B(4) -> match +-- Row 5: frame [5,6], A(5) B(6) -> match +WITH test_n_skip_next AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_n_skip_next +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Frame exactly 1 row short of potential match +-- From row 1: A A A B needs 4 rows but frame holds 3 -> no match +-- From row 2: A A B fits in 3-row frame -> match +WITH test_frame_one_short AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_frame_one_short +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND 2 FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+ B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- Special Partition Cases +-- ============================================================ + +-- Empty partition (0 rows) +WITH test_empty_partition AS ( + SELECT * FROM (VALUES + (1, 1, ARRAY['A']), + (2, 2, ARRAY['_']) -- Different partition + ) AS t(id, part, flags) +) +SELECT id, part, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_empty_partition +WHERE part = 99 -- No rows match +WINDOW w AS ( + PARTITION BY part + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE + A AS 'A' = ANY(flags) +); + +-- Single row partition +WITH test_single_row AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_single_row +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A) + DEFINE + A AS 'A' = ANY(flags) +); + +-- All rows fail matching (all DEFINE false) +WITH test_all_fail AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_all_fail +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A+) + DEFINE + A AS false -- All rows fail +); + +-- Partition end with absorbable pattern +-- SKIP PAST LAST ROW + unbounded frame + all rows match A +-- Triggers absorb in !rowExists path at partition boundary. +WITH test_absorb_partition_end AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (5, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorb_partition_end +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+) + DEFINE + A AS 'A' = ANY(flags) +); + +-- ============================================================ +-- DEFINE Special Cases +-- ============================================================ + +-- Undefined variable in DEFINE +WITH test_undefined_var AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['X']), -- B not defined, defaults to TRUE + (3, ARRAY['C']), + (4, ARRAY['A']), + (5, ARRAY['_']), -- B defaults to TRUE, but no flags + (6, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_undefined_var +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN (A B C) + DEFINE + A AS 'A' = ANY(flags), + -- B is undefined, defaults to TRUE + C AS 'C' = ANY(flags) +); + +-- ============================================================ +-- Absorption Dynamic Flags +-- ============================================================ + +-- Partial absorbable pattern ((A+) B) +WITH test_partial_absorbable AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_partial_absorbable +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A+) B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Dynamic flag update ((A+) | B) +WITH test_dynamic_flags AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_dynamic_flags +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A+) | B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Non-absorbable context during absorption +-- Pattern (A B)+ C: A,B in absorbable group, C is not. +-- When END exits to C, the cloned context becomes non-absorbable. +WITH test_non_absorbable AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['C']), + (6, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_non_absorbable +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- Absorption skipped when no absorbable state remains +-- Pattern (A B)+ C D with SKIP PAST LAST ROW +-- After reaching C (non-absorbable), no absorbable state remains. +-- On next row (D), the early return fires. +WITH test_absorption_early_return AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['C']), + (6, ARRAY['D']), + (7, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_absorption_early_return +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A B)+ C D) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- Coverage failure: older can't cover newer's states +-- Pattern A+ | B+ with SKIP PAST LAST ROW. +-- Row 1: only A -> Ctx1 takes A branch only (B fails). +-- Row 2: A and B -> Ctx2 takes both branches. +-- Absorption: Ctx1 has A but no B -> can't cover Ctx2's B state -> fails. +WITH test_coverage_fail AS ( + SELECT * FROM (VALUES + (1, ARRAY['A', '_']), + (2, ARRAY['A', 'B']), + (3, ARRAY['A', '_']), + (4, ARRAY['A', '_']), + (5, ARRAY['_', '_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_coverage_fail +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ | B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Absorb skips completed context (older->states==NULL) +-- Pattern A+ | B+ with SKIP PAST LAST ROW. +-- Row 1: A only -> Ctx1 takes A branch. Row 2: B only -> Ctx1 A fails (completed). +-- Ctx2 takes B branch. Absorption: Ctx1 states==NULL -> skip. +WITH test_older_completed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['B']), + (4, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_older_completed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ | B+) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- Absorb skips a context with no absorbable state +-- Pattern A+ | B C with SKIP PAST LAST ROW (only A+ branch absorbable). +-- Row 1: B only -> Ctx1 takes B branch (non-absorbable), advances to C. +-- Row 2: C,A -> Ctx1 C matches (no absorbable state). Ctx2 takes A (absorbable). +-- Absorption: Ctx1 has no absorbable state -> skip. +WITH test_older_non_absorbable AS ( + SELECT * FROM (VALUES + (1, ARRAY['B', '_']), + (2, ARRAY['C', 'A']), + (3, ARRAY['_', 'A']), + (4, ARRAY['_', '_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_older_non_absorbable +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A+ | B C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- Reluctant branch in ALT not absorbable: (A+?) | B +-- A+? is reluctant so not absorbable. Compare with greedy (A+) | B above. +WITH test_reluctant_alt_absorption AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['_']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_reluctant_alt_absorption +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A+?) | B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- Zero-Consumption Cycle Detection +-- ============================================================ + +-- Cycle prevention at count > 0: (A*)* inner skip cycles at count=3 +WITH test_cycle_nonzero AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']) -- Inner A* matches 0, cycles at count=3 + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_cycle_nonzero +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A*)*) + DEFINE + A AS 'A' = ANY(flags) +); + +-- Cycle with mixed nullables: (A* B*)* multiple nullable paths +WITH test_cycle_mixed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_cycle_mixed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A* B*)*) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ============================================================ +-- Standard Clause 7: Formal Pattern Matching Rules +-- ISO/IEC 19075-5, Clause 7 +-- ============================================================ + +-- ------------------------------------------------------------ +-- 7.2.2 Alternation: first alternative is preferred +-- ------------------------------------------------------------ + +-- (A | B): A preferred over B when both could match +-- Row 1 has both A and B flags: A should be chosen (first alternative) +WITH test_alt_prefer AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['B']), + (3, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_prefer +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- (A{1,2} | B{2,3}): all A-matches before all B-matches +-- Standard example: preferment order is AA, A, BBB, BB +-- Rows 1-2 have both A and B: greedy A{1,2} should match 1-2 +WITH test_alt_quantified AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['A','B']), + (3, ARRAY['B']), + (4, ARRAY['B']), + (5, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_alt_quantified +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A{1,2} | B{2,3})) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ------------------------------------------------------------ +-- 7.2.3 Concatenation: lexicographic ordering +-- ------------------------------------------------------------ + +-- ((A | B) (C | D)): preferment order is AC, AD, BC, BD +-- Row 1 matches A and B, Row 2 matches C and D +-- Preferred match: A then C (first alternatives in both positions) +WITH test_concat_lex AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['C','D']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_concat_lex +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A | B) (C | D)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags), + D AS 'D' = ANY(flags) +); + +-- ((A | B) C): first alt (A) fails, second alt (B) succeeds +-- Tests backtracking: row 1 has only B, row 2 has C +WITH test_concat_backtrack AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['C']), + (3, ARRAY['A']), + (4, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_concat_backtrack +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A | B) C) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags), + C AS 'C' = ANY(flags) +); + +-- ------------------------------------------------------------ +-- 7.2.4 Quantification: greedy/reluctant, lexicographic > length +-- ------------------------------------------------------------ + +-- V{2,4} greedy: longer match preferred +WITH test_quant_greedy AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_quant_greedy +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4}) + DEFINE + A AS 'A' = ANY(flags) +); + +-- V{2,4}? reluctant: shorter match preferred +WITH test_quant_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_quant_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A{2,4}?) + DEFINE + A AS 'A' = ANY(flags) +); + +-- ((A|B){1,2}) greedy: lexicographic > length +-- Standard example: preferment AA, AB, A, BA, BB, B +-- Single A preferred over B-starting longer match +WITH test_quant_lex_greedy AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_quant_lex_greedy +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B){1,2})) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ((A|B){1,2}?) reluctant: lexicographic > length +-- Standard example: preferment A, AA, AB, B, BA, BB +-- Single A preferred over any B-starting match +WITH test_quant_lex_reluctant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), + (2, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_quant_lex_reluctant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (((A | B){1,2}?)) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ------------------------------------------------------------ +-- 7.2.6 Anchors (not yet implemented - syntax error expected) +-- ------------------------------------------------------------ + +-- ^ anchor: not yet supported +SELECT count(*) OVER w FROM (SELECT 1 AS v) t +WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (^ A) DEFINE A AS TRUE); + +-- $ anchor: not yet supported +SELECT count(*) OVER w FROM (SELECT 1 AS v) t +WINDOW w AS (ORDER BY v ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (A $) DEFINE A AS TRUE); + +-- ------------------------------------------------------------ +-- 7.2.8 Infinite repetitions of empty matches +-- (Perl lower-bound stopping rule) +-- ------------------------------------------------------------ +-- Standard examples from 7.2.8: +-- (A?){0,3}: allowed strings include STR00=(), STR01=(A), STR02=(empty), +-- STR03=(AA), STR04=(A,empty), STR07=(AAA), STR08=(AA,empty) +-- (A?){1,3}: same as {0,3} but STR00 excluded (min=1 not met) +-- (A?){2,3}: STR03-06 (len 2) and STR07,08,11,12 (len 3) are valid +-- STR06=(STRE,STRE) IS valid because non-final STRE at +-- position 1 fills the lower bound + +-- (A??)*B: Standard 7.2.8 introductory example +-- "matched against a sequence of rows for which the only feasible +-- matching is: B" +-- A?? is reluctant, prefers empty. * is greedy but Perl rule stops +-- after empty match with min(=0) satisfied. +-- Expected: each B row matches alone (A?? empty, * stops, B matches) +WITH test_empty_reluctant_star AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_empty_reluctant_star +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A??)* B) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- (A?){0,3}: min=0, nullable inner. +-- A never matches but A? matches empty, satisfying min=0 immediately. +-- NFA reports 3 length-0 matches (one per row); first_value / last_value +-- are NULL because the window frame for an empty match has no rows. +WITH test_728_min0 AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_728_min0 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){0,3}) + DEFINE + A AS 'A' = ANY(flags) +); + +-- (A?){1,3}: min=1, nullable inner. +-- A never matches; one empty iteration satisfies min=1. +-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame. +WITH test_728_min1 AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_728_min1 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){1,3}) + DEFINE + A AS 'A' = ANY(flags) +); + +-- (A?){2,3}: min=2, nullable inner. Per ISO/IEC 19075-5 7.2.8 STR06 = (STRE STRE) +-- is valid: two empty iterations satisfy min=2. +-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame. +WITH test_728_min2 AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, ARRAY['B']), + (3, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_728_min2 +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE + A AS 'A' = ANY(flags) +); + +-- (A?){2,3} mixed: some rows match A, some don't +-- Rows 1-2: A matches, greedy takes 2 -> min satisfied (real match) +-- Row 3: A doesn't match, two empty iterations satisfy min=2 (length-0 match) +-- Row 4: A matches 1 real iter + 1 ff empty exit -> match 4-4 +WITH test_728_min2_mixed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['B']), + (4, ARRAY['A']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_728_min2_mixed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A?){2,3}) + DEFINE + A AS 'A' = ANY(flags) +); + +-- (A? B?){2,3}: multi-element nullable body with real matches +-- Body A? B? is nullable (both optional), but A and B DO match rows. +-- Real (non-empty) iterations loop back normally; fast-forward only +-- fires as a parallel exit path (EXIT ONLY, no greedy/reluctant loop). +-- Data: alternating A, B rows (6 rows) +-- Greedy: each row gets the longest match from its starting position. +-- Row 1: 3 iters (A@1,B@2)(A@3,B@4)(A@5,B@6) -> 1-6 +-- Row 5: 1 real iter + 1 ff empty exit -> 5-6 +WITH test_728_multi_body AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['A']), + (4, ARRAY['B']), + (5, ARRAY['A']), + (6, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_728_multi_body +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){2,3}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- (A? B?){2,3}: pure empty body (nothing matches A or B). +-- NFA reports 3 length-0 matches; first/last_value NULL over empty frame. +WITH test_728_multi_empty AS ( + SELECT * FROM (VALUES + (1, ARRAY['C']), + (2, ARRAY['C']), + (3, ARRAY['C']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_728_multi_empty +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){2,3}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- (A? B?){2,3}: mixed real and empty iterations +-- Row 1: iter1 real (A@1,B@2), iter2 at row 3 empty -> ff exit, match 1-2 +-- Row 3: C doesn't match A or B -> NULL +-- Row 4: iter1 real (A@4,B@5), iter2 at end empty -> ff exit, match 4-5 +WITH test_728_multi_mixed AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['A']), + (5, ARRAY['B']) + ) AS t(id, flags) +) +SELECT id, flags, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_728_multi_mixed +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP TO NEXT ROW + PATTERN ((A? B?){2,3}) + DEFINE + A AS 'A' = ANY(flags), + B AS 'B' = ANY(flags) +); + +-- ------------------------------------------------------------ +-- 7.3 Pattern matching in theory and practice +-- ------------------------------------------------------------ + +-- Standard's worked example: A? B+ with specific data +-- Preferment order: (A)(BBB), (A)(BB), (A)(B), ()(BBB), ()(BB), ()(B) +-- Row 1: A condition (price>100) is false -> A fails +-- Backtrack: empty A?, then B+ from row 1 +-- Expected: rows 1-3 match as B (A? takes empty match) +WITH test_73_example AS ( + SELECT * FROM (VALUES + (1, 60), + (2, 70), + (3, 40) + ) AS t(id, price) +) +SELECT id, price, + first_value(id) OVER w AS match_start, + last_value(id) OVER w AS match_end +FROM test_73_example +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A? B+) + DEFINE + A AS price > 100, + B AS TRUE +); diff --git a/src/test/regress/sql/window.sql b/src/test/regress/sql/window.sql index 8e6f92d94c7..1a4971a6083 100644 --- a/src/test/regress/sql/window.sql +++ b/src/test/regress/sql/window.sql @@ -235,6 +235,14 @@ SELECT last_value(unique1) over (ORDER BY four rows between current row and 2 fo unique1, four FROM tenk1 WHERE unique1 < 10; +SELECT nth_value(unique1,2) over (ORDER BY four rows between current row and 3 following exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 10; + +SELECT last_value(unique1) over (ORDER BY four rows between 1 following and 2 following exclude ties), + unique1, four +FROM tenk1 WHERE unique1 < 12 ORDER BY four, unique1; + SELECT sum(unique1) over (rows between 2 preceding and 1 preceding), unique1, four FROM tenk1 WHERE unique1 < 10; -- 2.43.0