($INBOX_DIR/description missing)help / color / mirror / Atom feed
[PATCH 14/21] doc review: logical decode in prepare 24+ messages / 2 participants [nested] [flat]
* [PATCH 14/21] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --jI8keyz6grp/JLjh Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* [PATCH 14/18] doc review: logical decode in prepare @ 2021-01-24 02:33 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Justin Pryzby @ 2021-01-24 02:33 UTC (permalink / raw) a271a1b50e9bec07e2ef3a05e38e7285113e4ce6 --- doc/src/sgml/logicaldecoding.sgml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/sgml/logicaldecoding.sgml b/doc/src/sgml/logicaldecoding.sgml index cf705ed9cd..b854f2ccfc 100644 --- a/doc/src/sgml/logicaldecoding.sgml +++ b/doc/src/sgml/logicaldecoding.sgml @@ -1214,7 +1214,7 @@ stream_commit_cb(...); <-- commit of the streamed transaction </para> <para> - When a prepared transaction is rollbacked using the + When a prepared transaction is rolled back using the <command>ROLLBACK PREPARED</command>, then the <function>rollback_prepared_cb</function> callback is invoked and when the prepared transaction is committed using <command>COMMIT PREPARED</command>, -- 2.17.0 --lc9FT7cWel8HagAv Content-Type: text/x-diff; charset=us-ascii Content-Disposition: attachment; filename="0015-doc-review-for-multiranges.patch" ^ permalink raw reply [nested|flat] 24+ messages in thread
* Re: Row pattern recognition @ 2026-01-16 04:44 Tatsuo Ishii <[email protected]> 0 siblings, 0 replies; 24+ messages in thread From: Tatsuo Ishii @ 2026-01-16 04:44 UTC (permalink / raw) To: [email protected]; +Cc: [email protected]; [email protected]; [email protected]; [email protected]; [email protected]; pgsql-hackers Hi Henson, > Hi Ishii-san, > > Thank you for raising the frame boundary concern. You're right that > different contexts have different frame boundaries. Ok. > Your suggestion was to use row_is_in_frame() to check frame boundaries > for each row. However, I took a simpler approach: just disable context > absorption entirely when the frame is limited. > > The root cause is that context absorption assumes all contexts see > the same rows. With limited frames, each context starting at a different > row has a different visible range, so we cannot absorb one context into > another. > > As I mentioned before, I think we should use absorption conservatively > for now - only in cases where it's clearly safe (e.g., A* at the > beginning of the pattern). We can extend it later through more in-depth > research. Agreed. > The fix: > > if (winstate->rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame) > nfa_absorb_contexts(winstate, targetCtx, currentPos); > > I added a test case to verify this: [snip] > I'm attaching three related commits: > > 1. Row pattern recognition: Improve NFA state memory management > > This commit refactors NFA state management for better readability > and maintainability. > > 2. Row pattern recognition: Disable context absorption for limited frame > > This commit further restricts absorption: even with SKIP PAST LAST ROW, > absorption is disabled when the frame is limited. With limited frames, > different contexts have different frame boundaries, making absorption > unsafe. I have applied the patches on top of v38 to create v39 patches. In addition to these patches, I have made following changes: - Add check for frame option "EXCLUDE". With RPR, using EXCLUDE is not permitted by the SQL standard. A test case added. - Remove unused typedef from windowfuncs.c (which causes removal of the typedef from typedes.list). The typedef was accidentally left by me while developing RPR. - Update the Copyright year to 2026 in some newly added files. Best regards, -- Tatsuo Ishii SRA OSS K.K. English: http://www.sraoss.co.jp/index_en/ Japanese:http://www.sraoss.co.jp Attachments: [application/octet-stream] v39-0001-Row-pattern-recognition-patch-for-raw-parser.patch (26.4K, ../../[email protected]/2-v39-0001-Row-pattern-recognition-patch-for-raw-parser.patch) download | inline diff: From 63031536473c8004eecaf96d01432e7bce8b9645 Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii <[email protected]> Date: Fri, 16 Jan 2026 13:11:21 +0900 Subject: [PATCH v39 1/8] Row pattern recognition patch for raw parser. The series of patches are to implement the row pattern recognition (SQL/RPR) feature. Currently the implementation is a subset of SQL/RPR (ISO/IEC 19075-2:2016). Namely, implementation of some features of R020 (WINDOW clause). R010 (MATCH_RECOGNIZE) is out of the scope of the patches. Currently following features are implemented in the patches. - PATTERN - PATTERN regular expressions (+, *, ?) (|, () (grouping), {n}, {n,}, {n,m}, {,m} - DEFINE - INITIAL - AFTER MATCH SKIP TO PAST LAST ROW - AFTER MATCH SKIP TO NEXT ROW Currently following features are not implemented in the patches. - MEASURE - SUBSET - SEEK - AFTER MATCH SKIP TO - AFTER MATCH SKIP TO FIRST - AFTER MATCH SKIP TO LAST - PATTERN regular expression reluctant quantifiers (*? etc.), {- and -}, ^, $, () (empty pattern) - PERMUTE - FIRST, LAST, CLASSIFIER Author: Tatsuo Ishii <[email protected]> Author: Henson Choi <[email protected]> Reviewed-by: Vik Fearing <[email protected]> Reviewed-by: Jacob Champion <[email protected]> Reviewed-by: NINGWEI CHEN <[email protected]> Reviewed-by: "David G. Johnston" <[email protected]> Reviewed-by: Chao Li <[email protected]> Reviewed-by: Henson Choi <[email protected]> Reviewed-by: Tatsuo Ishii <[email protected]> Discussion: https://postgr.es/m/20230625.210509.1276733411677577841.t-ishii%40sranhm.sra.co.jp --- src/backend/parser/gram.y | 384 ++++++++++++++++++++++++++++++-- src/backend/parser/scan.l | 4 +- src/include/nodes/parsenodes.h | 66 ++++++ src/include/parser/kwlist.h | 5 + src/include/parser/parse_node.h | 1 + 5 files changed, 441 insertions(+), 19 deletions(-) diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 713ee5c10a2..ffb950ac61d 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -209,6 +209,7 @@ static void preprocess_pub_all_objtype_list(List *all_objects_list, static void preprocess_pubobj_list(List *pubobjspec_list, core_yyscan_t yyscanner); static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); +static RPRPatternNode *makeRPRQuantifier(int min, int max, bool reluctant); %} @@ -682,6 +683,15 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_constructor_null_clause_opt json_array_constructor_null_clause_opt +%type <target> row_pattern_definition +%type <node> opt_row_pattern_common_syntax + row_pattern row_pattern_alt row_pattern_seq + row_pattern_term row_pattern_primary + row_pattern_quantifier_opt +%type <list> row_pattern_definition_list +%type <ival> opt_row_pattern_skip_to +%type <boolean> opt_row_pattern_initial_or_seek + /* * Non-keyword token types. These are hard-wired into the "flex" lexer. * They must be listed first so that their numeric codes do not depend on @@ -724,7 +734,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURSOR CYCLE DATA_P DATABASE DAY_P DEALLOCATE DEC DECIMAL_P DECLARE DEFAULT DEFAULTS - DEFERRABLE DEFERRED DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC + DEFERRABLE DEFERRED DEFINE DEFINER DELETE_P DELIMITER DELIMITERS DEPENDS DEPTH DESC DETACH DICTIONARY DISABLE_P DISCARD DISTINCT DO DOCUMENT_P DOMAIN_P DOUBLE_P DROP @@ -740,7 +750,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); HANDLER HAVING HEADER_P HOLD HOUR_P IDENTITY_P IF_P IGNORE_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE - INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P + INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIAL INITIALLY INLINE_P INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION @@ -765,8 +775,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); ORDER ORDINALITY OTHERS OUT_P OUTER_P OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER - PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PATH - PERIOD PLACING PLAN PLANS POLICY + PARALLEL PARAMETER PARSER PARTIAL PARTITION PARTITIONS PASSING PASSWORD PAST PATH + PATTERN_P PERIOD PLACING PLAN PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -777,7 +787,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); RESET RESPECT_P RESTART RESTRICT RETURN RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP ROUTINE ROUTINES ROW ROWS RULE - SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT + SAVEPOINT SCALAR SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SEEK SELECT SEQUENCE SEQUENCES SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SPLIT SOURCE SQL_P STABLE STANDALONE_P @@ -860,8 +870,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); * reference point for a precedence level that we can assign to other * keywords that lack a natural precedence level. * - * We need to do this for PARTITION, RANGE, ROWS, and GROUPS to support - * opt_existing_window_name (see comment there). + * We need to do this for PARTITION, RANGE, ROWS, GROUPS, AFTER, INITIAL, + * SEEK, PATTERN_P to support opt_existing_window_name (see comment there). * * The frame_bound productions UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING * are even messier: since UNBOUNDED is an unreserved keyword (per spec!), @@ -891,7 +901,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH -%left Op OPERATOR /* multi-character ops and user-defined operators */ + AFTER INITIAL SEEK PATTERN_P +%left Op OPERATOR '|' /* multi-character ops and user-defined operators */ %left '+' '-' %left '*' '/' '%' %left '^' @@ -16625,7 +16636,8 @@ over_clause: OVER window_specification ; window_specification: '(' opt_existing_window_name opt_partition_clause - opt_sort_clause opt_frame_clause ')' + opt_sort_clause opt_frame_clause + opt_row_pattern_common_syntax ')' { WindowDef *n = makeNode(WindowDef); @@ -16637,20 +16649,21 @@ window_specification: '(' opt_existing_window_name opt_partition_clause n->frameOptions = $5->frameOptions; n->startOffset = $5->startOffset; n->endOffset = $5->endOffset; + n->rpCommonSyntax = (RPCommonSyntax *)$6; n->location = @1; $$ = n; } ; /* - * If we see PARTITION, RANGE, ROWS or GROUPS as the first token after the '(' - * of a window_specification, we want the assumption to be that there is - * no existing_window_name; but those keywords are unreserved and so could - * be ColIds. We fix this by making them have the same precedence as IDENT - * and giving the empty production here a slightly higher precedence, so - * that the shift/reduce conflict is resolved in favor of reducing the rule. - * These keywords are thus precluded from being an existing_window_name but - * are not reserved for any other purpose. + * If we see PARTITION, RANGE, ROWS, GROUPS, AFTER, INITIAL, SEEK or PATTERN_P + * as the first token after the '(' of a window_specification, we want the + * assumption to be that there is no existing_window_name; but those keywords + * are unreserved and so could be ColIds. We fix this by making them have the + * same precedence as IDENT and giving the empty production here a slightly + * higher precedence, so that the shift/reduce conflict is resolved in favor + * of reducing the rule. These keywords are thus precluded from being an + * existing_window_name but are not reserved for any other purpose. */ opt_existing_window_name: ColId { $$ = $1; } | /*EMPTY*/ %prec Op { $$ = NULL; } @@ -16819,6 +16832,315 @@ opt_window_exclusion_clause: | /*EMPTY*/ { $$ = 0; } ; +opt_row_pattern_common_syntax: +opt_row_pattern_skip_to opt_row_pattern_initial_or_seek + PATTERN_P '(' row_pattern ')' + DEFINE row_pattern_definition_list + { + RPCommonSyntax *n = makeNode(RPCommonSyntax); + n->rpSkipTo = $1; + n->initial = $2; + n->rpPattern = (RPRPatternNode *) $5; + n->rpDefs = $8; + $$ = (Node *) n; + } + | /*EMPTY*/ { $$ = NULL; } + ; + +opt_row_pattern_skip_to: + AFTER MATCH SKIP TO NEXT ROW + { + $$ = ST_NEXT_ROW; + } + | AFTER MATCH SKIP PAST LAST_P ROW + { + $$ = ST_PAST_LAST_ROW; + } + | /*EMPTY*/ + { + $$ = ST_PAST_LAST_ROW; + } + ; + +opt_row_pattern_initial_or_seek: + INITIAL { $$ = true; } + | SEEK + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("SEEK is not supported"), + errhint("Use INITIAL instead."), + parser_errposition(@1))); + } + | /*EMPTY*/ { $$ = true; } + ; + +row_pattern: + row_pattern_alt { $$ = $1; } + ; + +row_pattern_alt: + row_pattern_seq { $$ = $1; } + | row_pattern_alt '|' row_pattern_seq + { + RPRPatternNode *n; + /* If left side is already ALT, append to it */ + if (IsA($1, RPRPatternNode) && + ((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_ALT) + { + n = (RPRPatternNode *) $1; + n->children = lappend(n->children, $3); + $$ = (Node *) n; + } + else + { + n = makeNode(RPRPatternNode); + n->nodeType = RPR_PATTERN_ALT; + n->children = list_make2($1, $3); + n->min = 1; + n->max = 1; + n->location = @1; + $$ = (Node *) n; + } + } + ; + +row_pattern_seq: + row_pattern_term { $$ = $1; } + | row_pattern_seq row_pattern_term + { + RPRPatternNode *n; + /* If left side is already SEQ, append to it */ + if (IsA($1, RPRPatternNode) && + ((RPRPatternNode *) $1)->nodeType == RPR_PATTERN_SEQ) + { + n = (RPRPatternNode *) $1; + n->children = lappend(n->children, $2); + $$ = (Node *) n; + } + else + { + n = makeNode(RPRPatternNode); + n->nodeType = RPR_PATTERN_SEQ; + n->children = list_make2($1, $2); + n->min = 1; + n->max = 1; + n->location = @1; + $$ = (Node *) n; + } + } + ; + +row_pattern_term: + row_pattern_primary row_pattern_quantifier_opt + { + RPRPatternNode *n = (RPRPatternNode *) $1; + RPRPatternNode *q = (RPRPatternNode *) $2; + + n->min = q->min; + n->max = q->max; + n->reluctant = q->reluctant; + $$ = (Node *) n; + } + ; + +row_pattern_primary: + ColId + { + RPRPatternNode *n = makeNode(RPRPatternNode); + n->nodeType = RPR_PATTERN_VAR; + n->varName = $1; + n->min = 1; + n->max = 1; + n->reluctant = false; + n->children = NIL; + n->location = @1; + $$ = (Node *) n; + } + | '(' row_pattern ')' + { + RPRPatternNode *inner = (RPRPatternNode *) $2; + RPRPatternNode *n = makeNode(RPRPatternNode); + n->nodeType = RPR_PATTERN_GROUP; + n->children = list_make1(inner); + n->min = 1; + n->max = 1; + n->reluctant = false; + n->location = @1; + $$ = (Node *) n; + } + ; + +row_pattern_quantifier_opt: + /* EMPTY */ { $$ = (Node *) makeRPRQuantifier(1, 1, false); } + | '*' { $$ = (Node *) makeRPRQuantifier(0, INT_MAX, false); } + | '+' { $$ = (Node *) makeRPRQuantifier(1, INT_MAX, false); } + | Op + { + /* Handle single Op: ? or reluctant quantifiers *?, +?, ?? */ + if (strcmp($1, "?") == 0) + $$ = (Node *) makeRPRQuantifier(0, 1, false); + else if (strcmp($1, "*?") == 0) + $$ = (Node *) makeRPRQuantifier(0, INT_MAX, true); + else if (strcmp($1, "+?") == 0) + $$ = (Node *) makeRPRQuantifier(1, INT_MAX, true); + else if (strcmp($1, "??") == 0) + $$ = (Node *) makeRPRQuantifier(0, 1, true); + else + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unsupported quantifier \"%s\"", $1), + parser_errposition(@1)); + } + /* RELUCTANT quantifiers (when lexer separates tokens) */ + | '*' Op + { + if (strcmp($2, "?") != 0) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unsupported quantifier"), + parser_errposition(@2)); + $$ = (Node *) makeRPRQuantifier(0, INT_MAX, true); + } + | '+' Op + { + if (strcmp($2, "?") != 0) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unsupported quantifier"), + parser_errposition(@2)); + $$ = (Node *) makeRPRQuantifier(1, INT_MAX, true); + } + | Op Op + { + if (strcmp($1, "?") != 0 || strcmp($2, "?") != 0) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unsupported quantifier"), + parser_errposition(@1)); + $$ = (Node *) makeRPRQuantifier(0, 1, true); + } + /* {n}, {n,}, {,m}, {n,m} quantifiers */ + | '{' Iconst '}' + { + if ($2 < 0 || $2 >= INT_MAX) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1), + parser_errposition(@2)); + $$ = (Node *) makeRPRQuantifier($2, $2, false); + } + | '{' Iconst ',' '}' + { + if ($2 < 0 || $2 >= INT_MAX) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1), + parser_errposition(@2)); + $$ = (Node *) makeRPRQuantifier($2, INT_MAX, false); + } + | '{' ',' Iconst '}' + { + if ($3 < 0 || $3 >= INT_MAX) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1), + parser_errposition(@3)); + $$ = (Node *) makeRPRQuantifier(0, $3, false); + } + | '{' Iconst ',' Iconst '}' + { + if ($2 < 0 || $4 < 0 || $2 >= INT_MAX || $4 >= INT_MAX) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1), + parser_errposition(@2)); + if ($2 > $4) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("quantifier minimum bound must not exceed maximum"), + parser_errposition(@2)); + $$ = (Node *) makeRPRQuantifier($2, $4, false); + } + /* Reluctant versions: {n}?, {n,}?, {,m}?, {n,m}? */ + | '{' Iconst '}' Op + { + if (strcmp($4, "?") != 0) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unsupported quantifier"), + parser_errposition(@4)); + if ($2 < 0 || $2 >= INT_MAX) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1), + parser_errposition(@2)); + $$ = (Node *) makeRPRQuantifier($2, $2, true); + } + | '{' Iconst ',' '}' Op + { + if (strcmp($5, "?") != 0) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unsupported quantifier"), + parser_errposition(@5)); + if ($2 < 0 || $2 >= INT_MAX) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1), + parser_errposition(@2)); + $$ = (Node *) makeRPRQuantifier($2, INT_MAX, true); + } + | '{' ',' Iconst '}' Op + { + if (strcmp($5, "?") != 0) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unsupported quantifier"), + parser_errposition(@5)); + if ($3 < 0 || $3 >= INT_MAX) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1), + parser_errposition(@3)); + $$ = (Node *) makeRPRQuantifier(0, $3, true); + } + | '{' Iconst ',' Iconst '}' Op + { + if (strcmp($6, "?") != 0) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unsupported quantifier"), + parser_errposition(@6)); + if ($2 < 0 || $4 < 0 || $2 >= INT_MAX || $4 >= INT_MAX) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("quantifier bound must be between 0 and %d", INT_MAX - 1), + parser_errposition(@2)); + if ($2 > $4) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("quantifier minimum bound must not exceed maximum"), + parser_errposition(@2)); + $$ = (Node *) makeRPRQuantifier($2, $4, true); + } + ; + +row_pattern_definition_list: + row_pattern_definition { $$ = list_make1($1); } + | row_pattern_definition_list ',' row_pattern_definition { $$ = lappend($1, $3); } + ; + +row_pattern_definition: + ColId AS a_expr + { + $$ = makeNode(ResTarget); + $$->name = $1; + $$->indirection = NIL; + $$->val = (Node *) $3; + $$->location = @1; + } + ; /* * Supporting nonterminals for expressions. @@ -16863,12 +17185,15 @@ MathOp: '+' { $$ = "+"; } | LESS_EQUALS { $$ = "<="; } | GREATER_EQUALS { $$ = ">="; } | NOT_EQUALS { $$ = "<>"; } + | '|' { $$ = "|"; } ; qual_Op: Op { $$ = list_make1(makeString($1)); } | OPERATOR '(' any_operator ')' { $$ = $3; } + | '|' + { $$ = list_make1(makeString("|")); } ; qual_all_Op: @@ -17958,6 +18283,7 @@ unreserved_keyword: | DECLARE | DEFAULTS | DEFERRED + | DEFINE | DEFINER | DELETE_P | DELIMITER @@ -18023,6 +18349,7 @@ unreserved_keyword: | INDEXES | INHERIT | INHERITS + | INITIAL | INLINE_P | INPUT_P | INSENSITIVE @@ -18098,7 +18425,9 @@ unreserved_keyword: | PARTITIONS | PASSING | PASSWORD + | PAST | PATH + | PATTERN_P | PERIOD | PLAN | PLANS @@ -18152,6 +18481,7 @@ unreserved_keyword: | SEARCH | SECOND_P | SECURITY + | SEEK | SEQUENCE | SEQUENCES | SERIALIZABLE @@ -18539,6 +18869,7 @@ bare_label_keyword: | DEFAULTS | DEFERRABLE | DEFERRED + | DEFINE | DEFINER | DELETE_P | DELIMITER @@ -18617,6 +18948,7 @@ bare_label_keyword: | INDEXES | INHERIT | INHERITS + | INITIAL | INITIALLY | INLINE_P | INNER_P @@ -18730,7 +19062,9 @@ bare_label_keyword: | PARTITIONS | PASSING | PASSWORD + | PAST | PATH + | PATTERN_P | PERIOD | PLACING | PLAN @@ -18789,6 +19123,7 @@ bare_label_keyword: | SCROLL | SEARCH | SECURITY + | SEEK | SELECT | SEQUENCE | SEQUENCES @@ -19980,6 +20315,21 @@ makeRecursiveViewSelect(char *relname, List *aliases, Node *query) return (Node *) s; } +/* + * makeRPRQuantifier + * Create an RPRPatternNode with specified quantifier bounds. + */ +static RPRPatternNode * +makeRPRQuantifier(int min, int max, bool reluctant) +{ + RPRPatternNode *n = makeNode(RPRPatternNode); + + n->min = min; + n->max = max; + n->reluctant = reluctant; + return n; +} + /* parser_init() * Initialize to parse one query string */ diff --git a/src/backend/parser/scan.l b/src/backend/parser/scan.l index 731d584106d..f6ca2406146 100644 --- a/src/backend/parser/scan.l +++ b/src/backend/parser/scan.l @@ -363,7 +363,7 @@ not_equals "!=" * If you change either set, adjust the character lists appearing in the * rule for "operator"! */ -self [,()\[\].;\:\+\-\*\/\%\^\<\>\=] +self [,()\[\].;\:\+\-\*\/\%\^\<\>\=\|] op_chars [\~\!\@\#\^\&\|\`\?\+\-\*\/\%\<\>\=] operator {op_chars}+ @@ -955,7 +955,7 @@ other . * that the "self" rule would have. */ if (nchars == 1 && - strchr(",()[].;:+-*/%^<>=", yytext[0])) + strchr(",()[].;:+-*/%^<>=|", yytext[0])) return yytext[0]; /* * Likewise, if what we have left is two chars, and diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 646d6ced763..2bb5c321157 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -578,6 +578,56 @@ typedef struct SortBy ParseLoc location; /* operator location, or -1 if none/unknown */ } SortBy; +/* + * AFTER MATCH row pattern skip to types in row pattern common syntax + */ +typedef enum RPSkipTo +{ + ST_NONE, /* AFTER MATCH omitted */ + ST_NEXT_ROW, /* SKIP TO NEXT ROW */ + ST_PAST_LAST_ROW, /* SKIP TO PAST LAST ROW */ +} RPSkipTo; + +/* + * RPRPatternNodeType - Row Pattern Recognition pattern node types + */ +typedef enum RPRPatternNodeType +{ + RPR_PATTERN_VAR, /* variable reference */ + RPR_PATTERN_SEQ, /* sequence (concatenation) */ + RPR_PATTERN_ALT, /* alternation (|) */ + RPR_PATTERN_GROUP /* group (parentheses) */ +} RPRPatternNodeType; + +/* + * RPRPatternNode - Row Pattern Recognition pattern AST node + */ +typedef struct RPRPatternNode +{ + NodeTag type; /* T_RPRPatternNode */ + RPRPatternNodeType nodeType; /* VAR, SEQ, ALT, GROUP */ + int min; /* minimum repetitions (0 for *, ?) */ + int max; /* maximum repetitions (INT_MAX for *, +) */ + bool reluctant; /* true for *?, +?, ?? */ + ParseLoc location; /* token location, or -1 */ + char *varName; /* VAR: variable name */ + List *children; /* SEQ, ALT, GROUP: child nodes */ +} RPRPatternNode; + +/* + * RowPatternCommonSyntax - raw representation of row pattern common syntax + */ +typedef struct RPCommonSyntax +{ + NodeTag type; + RPSkipTo rpSkipTo; /* Row Pattern AFTER MATCH SKIP type */ + bool initial; /* true if <row pattern initial or seek> is + * initial */ + RPRPatternNode *rpPattern; /* PATTERN clause AST */ + List *rpDefs; /* row pattern definitions clause (list of + * ResTarget) */ +} RPCommonSyntax; + /* * WindowDef - raw representation of WINDOW and OVER clauses * @@ -593,6 +643,7 @@ typedef struct WindowDef char *refname; /* referenced window name, if any */ List *partitionClause; /* PARTITION BY expression list */ List *orderClause; /* ORDER BY (list of SortBy) */ + RPCommonSyntax *rpCommonSyntax; /* row pattern common syntax */ int frameOptions; /* frame_clause options, see below */ Node *startOffset; /* expression for starting bound, if any */ Node *endOffset; /* expression for ending bound, if any */ @@ -1588,6 +1639,11 @@ typedef struct GroupingSet * the orderClause might or might not be copied (see copiedOrder); the framing * options are never copied, per spec. * + * "defineClause" is Row Pattern Recognition DEFINE clause (list of + * TargetEntry). TargetEntry.resname represents row pattern definition + * variable name. "rpPattern" represents PATTERN clause as an AST tree + * (RPRPatternNode). + * * The information relevant for the query jumbling is the partition clause * type and its bounds. */ @@ -1617,6 +1673,16 @@ typedef struct WindowClause Index winref; /* ID referenced by window functions */ /* did we copy orderClause from refname? */ bool copiedOrder pg_node_attr(query_jumble_ignore); + /* Row Pattern AFTER MATCH SKIP clause */ + RPSkipTo rpSkipTo; /* Row Pattern Skip To type */ + bool initial; /* true if <row pattern initial or seek> is + * initial */ + /* Row Pattern DEFINE clause (list of TargetEntry) */ + List *defineClause; + /* Row Pattern DEFINE variable initial names (list of String) */ + List *defineInitial; + /* Row Pattern PATTERN clause AST */ + RPRPatternNode *rpPattern; } WindowClause; /* diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index f7753c5c8a8..1624f4412dc 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -129,6 +129,7 @@ PG_KEYWORD("default", DEFAULT, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("defaults", DEFAULTS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("deferrable", DEFERRABLE, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("deferred", DEFERRED, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("define", DEFINE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("definer", DEFINER, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("delete", DELETE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("delimiter", DELIMITER, UNRESERVED_KEYWORD, BARE_LABEL) @@ -217,6 +218,7 @@ PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("inherits", INHERITS, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("initial", INITIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("initially", INITIALLY, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("inline", INLINE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("inner", INNER_P, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) @@ -342,7 +344,9 @@ PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partitions", PARTITIONS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("past", PAST, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("pattern", PATTERN_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("period", PERIOD, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL) @@ -405,6 +409,7 @@ PG_KEYWORD("scroll", SCROLL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("search", SEARCH, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("second", SECOND_P, UNRESERVED_KEYWORD, AS_LABEL) PG_KEYWORD("security", SECURITY, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("seek", SEEK, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("select", SELECT, RESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("sequence", SEQUENCE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("sequences", SEQUENCES, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h index a9bffb8a78f..36fed104b32 100644 --- a/src/include/parser/parse_node.h +++ b/src/include/parser/parse_node.h @@ -51,6 +51,7 @@ typedef enum ParseExprKind EXPR_KIND_WINDOW_FRAME_RANGE, /* window frame clause with RANGE */ EXPR_KIND_WINDOW_FRAME_ROWS, /* window frame clause with ROWS */ EXPR_KIND_WINDOW_FRAME_GROUPS, /* window frame clause with GROUPS */ + EXPR_KIND_RPR_DEFINE, /* DEFINE */ EXPR_KIND_SELECT_TARGET, /* SELECT target list item */ EXPR_KIND_INSERT_TARGET, /* INSERT target list item */ EXPR_KIND_UPDATE_SOURCE, /* UPDATE assignment source item */ -- 2.43.0 [application/octet-stream] v39-0002-Row-pattern-recognition-patch-parse-analysis.patch (24.3K, ../../[email protected]/3-v39-0002-Row-pattern-recognition-patch-parse-analysis.patch) download | inline diff: From ec0a6cc4d3d47cb955cd1b73c2892366cce91838 Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii <[email protected]> Date: Fri, 16 Jan 2026 13:11:21 +0900 Subject: [PATCH v39 2/8] Row pattern recognition patch (parse/analysis). --- src/backend/nodes/copyfuncs.c | 39 ++++ src/backend/nodes/equalfuncs.c | 33 +++ src/backend/nodes/outfuncs.c | 51 +++++ src/backend/nodes/readfuncs.c | 79 +++++++ src/backend/parser/Makefile | 1 + src/backend/parser/README | 1 + src/backend/parser/meson.build | 1 + src/backend/parser/parse_agg.c | 7 + src/backend/parser/parse_clause.c | 10 +- src/backend/parser/parse_expr.c | 6 + src/backend/parser/parse_func.c | 3 + src/backend/parser/parse_rpr.c | 346 ++++++++++++++++++++++++++++++ src/include/parser/parse_clause.h | 3 + src/include/parser/parse_rpr.h | 22 ++ 14 files changed, 598 insertions(+), 4 deletions(-) create mode 100644 src/backend/parser/parse_rpr.c create mode 100644 src/include/parser/parse_rpr.h diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index ff22a04abe5..5d69805fc24 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -16,6 +16,7 @@ #include "postgres.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "utils/datum.h" @@ -166,6 +167,44 @@ _copyBitmapset(const Bitmapset *from) return bms_copy(from); } +static RPRPattern * +_copyRPRPattern(const RPRPattern *from) +{ + RPRPattern *newnode = makeNode(RPRPattern); + + COPY_SCALAR_FIELD(numVars); + COPY_SCALAR_FIELD(maxDepth); + COPY_SCALAR_FIELD(numElements); + + /* Deep copy the varNames array */ + if (from->numVars > 0) + { + newnode->varNames = palloc0(from->numVars * sizeof(char *)); + for (int i = 0; i < from->numVars; i++) + newnode->varNames[i] = pstrdup(from->varNames[i]); + } + else + { + newnode->varNames = NULL; + } + + /* Deep copy the elements array */ + if (from->numElements > 0) + { + newnode->elements = palloc(from->numElements * sizeof(RPRPatternElement)); + memcpy(newnode->elements, from->elements, + from->numElements * sizeof(RPRPatternElement)); + } + else + { + newnode->elements = NULL; + } + + COPY_SCALAR_FIELD(isAbsorbable); + + return newnode; +} + /* * copyObjectImpl -- implementation of copyObject(); see nodes/nodes.h diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 3d1a1adf86e..9410790e3d3 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -20,6 +20,7 @@ #include "postgres.h" #include "miscadmin.h" +#include "nodes/plannodes.h" #include "utils/datum.h" @@ -149,6 +150,38 @@ _equalBitmapset(const Bitmapset *a, const Bitmapset *b) return bms_equal(a, b); } +static bool +_equalRPRPattern(const RPRPattern *a, const RPRPattern *b) +{ + COMPARE_SCALAR_FIELD(numVars); + COMPARE_SCALAR_FIELD(maxDepth); + COMPARE_SCALAR_FIELD(numElements); + + /* Compare varNames array */ + if (a->numVars > 0) + { + if (a->varNames == NULL || b->varNames == NULL) + return false; + for (int i = 0; i < a->numVars; i++) + { + if (strcmp(a->varNames[i], b->varNames[i]) != 0) + return false; + } + } + + /* Compare elements array */ + if (a->numElements > 0) + { + if (a->elements == NULL || b->elements == NULL) + return false; + if (memcmp(a->elements, b->elements, + a->numElements * sizeof(RPRPatternElement)) != 0) + return false; + } + + return true; +} + /* * Lists are handled specially */ diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index c8eef2c75d2..1a70b016770 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -23,6 +23,7 @@ #include "nodes/bitmapset.h" #include "nodes/nodes.h" #include "nodes/pg_list.h" +#include "nodes/plannodes.h" #include "utils/datum.h" /* State flag that determines how nodeToStringInternal() should treat location fields */ @@ -718,6 +719,56 @@ _outA_Const(StringInfo str, const A_Const *node) WRITE_LOCATION_FIELD(location); } +static void +_outRPRPattern(StringInfo str, const RPRPattern *node) +{ + WRITE_NODE_TYPE("RPRPATTERN"); + + WRITE_INT_FIELD(numVars); + WRITE_INT_FIELD(maxDepth); + WRITE_INT_FIELD(numElements); + + /* Write varNames array as list of strings */ + appendStringInfoString(str, " :varNames"); + if (node->numVars > 0 && node->varNames != NULL) + { + appendStringInfoString(str, " ("); + for (int i = 0; i < node->numVars; i++) + { + if (i > 0) + appendStringInfoChar(str, ' '); + outToken(str, node->varNames[i]); + } + appendStringInfoChar(str, ')'); + } + else + appendStringInfoString(str, " <>"); + + /* Write elements array */ + appendStringInfoString(str, " :elements"); + if (node->numElements > 0 && node->elements != NULL) + { + appendStringInfoChar(str, ' '); + for (int i = 0; i < node->numElements; i++) + { + const RPRPatternElement *elem = &node->elements[i]; + + appendStringInfo(str, "(%d %u %d %d %d %d %d)", + (int) elem->varId, + (unsigned) elem->flags, + (int) elem->depth, + (int) elem->min, + (int) elem->max, + (int) elem->next, + (int) elem->jump); + } + } + else + appendStringInfoString(str, " <>"); + + WRITE_BOOL_FIELD(isAbsorbable); +} + /* * outNode - diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index 981ab9c34ef..0ba9afe45e6 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/bitmapset.h" +#include "nodes/plannodes.h" #include "nodes/readfuncs.h" @@ -558,6 +559,84 @@ _readExtensibleNode(void) READ_DONE(); } +static RPRPattern * +_readRPRPattern(void) +{ + READ_LOCALS(RPRPattern); + + READ_INT_FIELD(numVars); + READ_INT_FIELD(maxDepth); + READ_INT_FIELD(numElements); + + /* Read varNames array */ + token = pg_strtok(&length); /* skip :varNames */ + token = pg_strtok(&length); /* get '(' or '<>' */ + if (local_node->numVars > 0 && token[0] == '(') + { + local_node->varNames = palloc(local_node->numVars * sizeof(char *)); + for (int i = 0; i < local_node->numVars; i++) + { + token = pg_strtok(&length); + local_node->varNames[i] = debackslash(token, length); + } + token = pg_strtok(&length); /* skip ')' */ + } + else + { + local_node->varNames = NULL; + } + + /* Read elements array */ + token = pg_strtok(&length); /* skip :elements */ + token = pg_strtok(&length); /* get '(' or '<>' */ + if (local_node->numElements > 0 && token[0] == '(') + { + local_node->elements = palloc0(local_node->numElements * sizeof(RPRPatternElement)); + for (int i = 0; i < local_node->numElements; i++) + { + RPRPatternElement *elem = &local_node->elements[i]; + int varId, flags, depth, min, max, next, jump; + + /* Parse "(varId flags depth min max next jump)" */ + token = pg_strtok(&length); + varId = atoi(token); + token = pg_strtok(&length); + flags = atoi(token); + token = pg_strtok(&length); + depth = atoi(token); + token = pg_strtok(&length); + min = atoi(token); + token = pg_strtok(&length); + max = atoi(token); + token = pg_strtok(&length); + next = atoi(token); + token = pg_strtok(&length); + jump = atoi(token); + token = pg_strtok(&length); /* skip ')' */ + + elem->varId = (RPRVarId) varId; + elem->flags = (RPRElemFlags) flags; + elem->depth = (RPRDepth) depth; + elem->min = (RPRQuantity) min; + elem->max = (RPRQuantity) max; + elem->next = (RPRElemIdx) next; + elem->jump = (RPRElemIdx) jump; + + /* Read next element's '(' or end */ + if (i < local_node->numElements - 1) + token = pg_strtok(&length); /* get '(' */ + } + } + else + { + local_node->elements = NULL; + } + + READ_BOOL_FIELD(isAbsorbable); + + READ_DONE(); +} + /* * parseNodeString diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 8c0fe28d63f..f4c7d605fe3 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -29,6 +29,7 @@ OBJS = \ parse_oper.o \ parse_param.o \ parse_relation.o \ + parse_rpr.o \ parse_target.o \ parse_type.o \ parse_utilcmd.o \ diff --git a/src/backend/parser/README b/src/backend/parser/README index e26eb437a9f..2baffa9517e 100644 --- a/src/backend/parser/README +++ b/src/backend/parser/README @@ -26,6 +26,7 @@ parse_node.c create nodes for various structures parse_oper.c handle operators in expressions parse_param.c handle Params (for the cases used in the core backend) parse_relation.c support routines for tables and column handling +parse_rpr.c handle Row Pattern Recognition parse_target.c handle the result list of the query parse_type.c support routines for data type handling parse_utilcmd.c parse analysis for utility commands (done at execution time) diff --git a/src/backend/parser/meson.build b/src/backend/parser/meson.build index 924ee87a453..a07102b37a0 100644 --- a/src/backend/parser/meson.build +++ b/src/backend/parser/meson.build @@ -16,6 +16,7 @@ backend_sources += files( 'parse_oper.c', 'parse_param.c', 'parse_relation.c', + 'parse_rpr.c', 'parse_target.c', 'parse_type.c', 'parse_utilcmd.c', diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index 25ee0f87d93..5ed785ea0d5 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -584,6 +584,10 @@ check_agglevels_and_constraints(ParseState *pstate, Node *expr) errkind = true; break; + case EXPR_KIND_RPR_DEFINE: + errkind = true; + break; + /* * There is intentionally no default: case here, so that the * compiler will warn if we add a new ParseExprKind without @@ -1023,6 +1027,9 @@ transformWindowFuncCall(ParseState *pstate, WindowFunc *wfunc, case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_RPR_DEFINE: + errkind = true; + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index e35fd25c9bb..f98ac7cfcb5 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -37,6 +37,7 @@ #include "parser/parse_func.h" #include "parser/parse_oper.h" #include "parser/parse_relation.h" +#include "parser/parse_rpr.h" #include "parser/parse_target.h" #include "parser/parse_type.h" #include "parser/parser.h" @@ -84,8 +85,6 @@ static void checkExprIsVarFree(ParseState *pstate, Node *n, const char *constructName); static TargetEntry *findTargetlistEntrySQL92(ParseState *pstate, Node *node, List **tlist, ParseExprKind exprKind); -static TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node, - List **tlist, ParseExprKind exprKind); static int get_matching_location(int sortgroupref, List *sortgrouprefs, List *exprs); static List *resolve_unique_index_expr(ParseState *pstate, InferClause *infer, @@ -97,7 +96,6 @@ static Node *transformFrameOffset(ParseState *pstate, int frameOptions, Oid rangeopfamily, Oid rangeopcintype, Oid *inRangeFunc, Node *clause); - /* * transformFromClause - * Process the FROM clause and add items to the query's range table, @@ -2168,7 +2166,7 @@ findTargetlistEntrySQL92(ParseState *pstate, Node *node, List **tlist, * tlist the target list (passed by reference so we can append to it) * exprKind identifies clause type being processed */ -static TargetEntry * +TargetEntry * findTargetlistEntrySQL99(ParseState *pstate, Node *node, List **tlist, ParseExprKind exprKind) { @@ -3019,6 +3017,10 @@ transformWindowDefinitions(ParseState *pstate, rangeopfamily, rangeopcintype, &wc->endInRangeFunc, windef->endOffset); + + /* Process Row Pattern Recognition related clauses */ + transformRPR(pstate, wc, windef, targetlist); + wc->winref = winref; result = lappend(result, wc); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 56826db4c26..7ebcd15fa39 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -577,6 +577,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref) case EXPR_KIND_COPY_WHERE: case EXPR_KIND_GENERATED_COLUMN: case EXPR_KIND_CYCLE_MARK: + case EXPR_KIND_RPR_DEFINE: /* okay */ break; @@ -1862,6 +1863,9 @@ transformSubLink(ParseState *pstate, SubLink *sublink) case EXPR_KIND_GENERATED_COLUMN: err = _("cannot use subquery in column generation expression"); break; + case EXPR_KIND_RPR_DEFINE: + err = _("cannot use subquery in DEFINE expression"); + break; /* * There is intentionally no default: case here, so that the @@ -3221,6 +3225,8 @@ ParseExprKindName(ParseExprKind exprKind) return "GENERATED AS"; case EXPR_KIND_CYCLE_MARK: return "CYCLE"; + case EXPR_KIND_RPR_DEFINE: + return "DEFINE"; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 24f6745923b..357b236a818 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -2783,6 +2783,9 @@ check_srf_call_placement(ParseState *pstate, Node *last_srf, int location) case EXPR_KIND_CYCLE_MARK: errkind = true; break; + case EXPR_KIND_RPR_DEFINE: + errkind = true; + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c new file mode 100644 index 00000000000..0c6d2c2d321 --- /dev/null +++ b/src/backend/parser/parse_rpr.c @@ -0,0 +1,346 @@ +/*------------------------------------------------------------------------- + * + * parse_rpr.c + * handle Row Pattern Recognition in parser + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_rpr.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_rpr.h" +#include "parser/parse_target.h" + +/* DEFINE variable name initials */ +static const char *defineVariableInitials = "abcdefghijklmnopqrstuvwxyz"; + +/* Forward declarations */ +static void collectRPRPatternVarNames(RPRPatternNode *node, List **varNames); +static List *transformDefineClause(ParseState *pstate, WindowClause *wc, + WindowDef *windef, List **targetlist); +static void transformPatternClause(ParseState *pstate, WindowClause *wc, + WindowDef *windef); +static RPRPatternNode *copyRPRPatternNode(RPRPatternNode *node); + +/* + * transformRPR + * Process Row Pattern Recognition related clauses + */ +void +transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef, + List **targetlist) +{ + /* + * Window definition exists? + */ + if (windef == NULL) + return; + + /* + * Row Pattern Common Syntax clause exists? + */ + if (windef->rpCommonSyntax == NULL) + return; + + /* Check Frame options */ + /* Frame must start at current row */ + if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("FRAME must start at current row when row pattern recognition is used"))); + /* EXCLUDE options are not permitted */ + if ((wc->frameOptions & FRAMEOPTION_EXCLUSION) != 0) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("EXCLUDE options are not permitted when row pattern recognition is used"))); + + /* Transform AFTER MATCH SKIP TO clause */ + wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo; + + /* Transform SEEK or INITIAL clause */ + wc->initial = windef->rpCommonSyntax->initial; + + /* Transform DEFINE clause into list of TargetEntry's */ + wc->defineClause = transformDefineClause(pstate, wc, windef, targetlist); + + /* Check PATTERN clause and copy to patternClause */ + transformPatternClause(pstate, wc, windef); +} + +/* + * collectRPRPatternVarNames + * Collect all variable names from RPRPatternNode tree. + */ +static void +collectRPRPatternVarNames(RPRPatternNode *node, List **varNames) +{ + ListCell *lc; + + if (node == NULL) + return; + + switch (node->nodeType) + { + case RPR_PATTERN_VAR: + /* Add variable name if not already in list */ + { + bool found = false; + + foreach(lc, *varNames) + { + if (strcmp(strVal(lfirst(lc)), node->varName) == 0) + { + found = true; + break; + } + } + if (!found) + *varNames = lappend(*varNames, makeString(pstrdup(node->varName))); + } + break; + + case RPR_PATTERN_SEQ: + case RPR_PATTERN_ALT: + case RPR_PATTERN_GROUP: + /* Recurse into children */ + foreach(lc, node->children) + { + collectRPRPatternVarNames((RPRPatternNode *) lfirst(lc), varNames); + } + break; + } +} + +/* + * transformDefineClause + * Process DEFINE clause and transform ResTarget into list of + * TargetEntry. + * + * XXX we only support column reference in row pattern definition search + * condition, e.g. "price". <row pattern definition variable name>.<column + * reference> is not supported, e.g. "A.price". + */ +static List * +transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef, + List **targetlist) +{ + ListCell *lc, + *l; + ResTarget *restarget, + *r; + List *restargets; + List *defineClause; + char *name; + int initialLen; + int numinitials; + List *patternVarNames = NIL; + + /* + * If Row Definition Common Syntax exists, DEFINE clause must exist. (the + * raw parser should have already checked it.) + */ + Assert(windef->rpCommonSyntax->rpDefs != NULL); + + /* + * Collect all pattern variable names from the pattern tree. + */ + collectRPRPatternVarNames(windef->rpCommonSyntax->rpPattern, &patternVarNames); + + /* + * Check and add "A AS A IS TRUE" if pattern variable is missing in DEFINE + * per the SQL standard. + */ + restargets = NIL; + foreach(lc, patternVarNames) + { + bool found = false; + + name = strVal(lfirst(lc)); + + foreach(l, windef->rpCommonSyntax->rpDefs) + { + restarget = (ResTarget *) lfirst(l); + + if (!strcmp(restarget->name, name)) + { + found = true; + break; + } + } + + if (!found) + { + /* + * "name" is missing. So create "name AS name IS TRUE" ResTarget + * node and add it to the temporary list. + */ + A_Const *n; + + restarget = makeNode(ResTarget); + n = makeNode(A_Const); + n->val.boolval.type = T_Boolean; + n->val.boolval.boolval = true; + n->location = -1; + restarget->name = pstrdup(name); + restarget->indirection = NIL; + restarget->val = (Node *) n; + restarget->location = -1; + restargets = lappend((List *) restargets, restarget); + } + } + + if (list_length(restargets) >= 1) + { + /* add missing DEFINEs */ + windef->rpCommonSyntax->rpDefs = + list_concat(windef->rpCommonSyntax->rpDefs, restargets); + list_free(restargets); + } + + /* + * Check for duplicate row pattern definition variables. The standard + * requires that no two row pattern definition variable names shall be + * equivalent. + */ + restargets = NIL; + numinitials = 0; + initialLen = strlen(defineVariableInitials); + foreach(lc, windef->rpCommonSyntax->rpDefs) + { + char initial[2]; + + restarget = (ResTarget *) lfirst(lc); + name = restarget->name; + + /* + * Add DEFINE expression (Restarget->val) to the targetlist as a + * TargetEntry if it does not exist yet. Planner will add the column + * ref var node to the outer plan's target list later on. This makes + * DEFINE expression could access the outer tuple while evaluating + * PATTERN. + * + * XXX: adding whole expressions of DEFINE to the plan.targetlist is + * not so good, because it's not necessary to evalute the expression + * in the target list while running the plan. We should extract the + * var nodes only then add them to the plan.targetlist. + */ + findTargetlistEntrySQL99(pstate, (Node *) restarget->val, + targetlist, EXPR_KIND_RPR_DEFINE); + + /* + * Make sure that the row pattern definition search condition is a + * boolean expression. + */ + transformWhereClause(pstate, restarget->val, + EXPR_KIND_RPR_DEFINE, "DEFINE"); + + foreach(l, restargets) + { + char *n; + + r = (ResTarget *) lfirst(l); + n = r->name; + + if (!strcmp(n, name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("row pattern definition variable name \"%s\" appears more than once in DEFINE clause", + name), + parser_errposition(pstate, exprLocation((Node *) r)))); + } + + /* + * Create list of row pattern DEFINE variable name's initial. We + * assign [a-z] to them (up to 26 variable names are allowed). + */ + if (numinitials >= initialLen) + { + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("number of row pattern definition variable names exceeds %d", + initialLen), + parser_errposition(pstate, + exprLocation((Node *) restarget)))); + } + initial[0] = defineVariableInitials[numinitials++]; + initial[1] = '\0'; + wc->defineInitial = lappend(wc->defineInitial, + makeString(pstrdup(initial))); + + restargets = lappend(restargets, restarget); + } + list_free(restargets); + + /* turns a list of ResTarget's into a list of TargetEntry's */ + defineClause = transformTargetList(pstate, windef->rpCommonSyntax->rpDefs, + EXPR_KIND_RPR_DEFINE); + + /* mark column origins */ + markTargetListOrigins(pstate, defineClause); + + /* mark all nodes in the DEFINE clause tree with collation information */ + assign_expr_collations(pstate, (Node *) defineClause); + + return defineClause; +} + +/* + * transformPatternClause + * Process PATTERN clause and return PATTERN clause in the raw parse tree + * + * windef->rpCommonSyntax must exist. + * + * The original (unoptimized) pattern is stored for deparsing (pg_get_viewdef). + * Optimization happens later in the planner phase (buildRPRPattern). + */ +static void +transformPatternClause(ParseState *pstate, WindowClause *wc, + WindowDef *windef) +{ + Assert(windef->rpCommonSyntax != NULL); + + /* Store original AST for deparsing (no optimization here) */ + wc->rpPattern = copyRPRPatternNode(windef->rpCommonSyntax->rpPattern); +} + +/* + * copyRPRPatternNode + * Deep copy an RPRPatternNode tree. + */ +static RPRPatternNode * +copyRPRPatternNode(RPRPatternNode *node) +{ + RPRPatternNode *copy; + ListCell *lc; + + if (node == NULL) + return NULL; + + copy = makeNode(RPRPatternNode); + copy->nodeType = node->nodeType; + copy->varName = node->varName ? pstrdup(node->varName) : NULL; + copy->min = node->min; + copy->max = node->max; + copy->reluctant = node->reluctant; + copy->children = NIL; + + foreach(lc, node->children) + { + copy->children = lappend(copy->children, + copyRPRPatternNode(lfirst(lc))); + } + + return copy; +} diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index fe234611007..8aaac881f2b 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -52,6 +52,9 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +extern TargetEntry *findTargetlistEntrySQL99(ParseState *pstate, Node *node, + List **tlist, ParseExprKind exprKind); + /* functions in parse_jsontable.c */ extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); diff --git a/src/include/parser/parse_rpr.h b/src/include/parser/parse_rpr.h new file mode 100644 index 00000000000..7fab6f292aa --- /dev/null +++ b/src/include/parser/parse_rpr.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * parse_rpr.h + * handle Row Pattern Recognition in parser + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/parser/parse_rpr.h + * + *------------------------------------------------------------------------- + */ +#ifndef PARSE_RPR_H +#define PARSE_RPR_H + +#include "parser/parse_node.h" + +extern void transformRPR(ParseState *pstate, WindowClause *wc, + WindowDef *windef, List **targetlist); + +#endif /* PARSE_RPR_H */ -- 2.43.0 [application/octet-stream] v39-0003-Row-pattern-recognition-patch-rewriter.patch (5.7K, ../../[email protected]/4-v39-0003-Row-pattern-recognition-patch-rewriter.patch) download | inline diff: From 42be99f692dc00e083059c84afbd4d8965f7ded2 Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii <[email protected]> Date: Fri, 16 Jan 2026 13:11:21 +0900 Subject: [PATCH v39 3/8] Row pattern recognition patch (rewriter). --- src/backend/utils/adt/ruleutils.c | 172 ++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 033b625f3fc..b8a3ee2915b 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -438,6 +438,12 @@ static void get_rule_groupingset(GroupingSet *gset, List *targetlist, bool omit_parens, deparse_context *context); static void get_rule_orderby(List *orderList, List *targetList, bool force_colno, deparse_context *context); +static void append_pattern_quantifier(StringInfo buf, RPRPatternNode *node); +static void get_rule_pattern_node(RPRPatternNode *node, deparse_context *context); +static void get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno, + deparse_context *context); +static void get_rule_define(List *defineClause, bool force_colno, + deparse_context *context); static void get_rule_windowclause(Query *query, deparse_context *context); static void get_rule_windowspec(WindowClause *wc, List *targetList, deparse_context *context); @@ -6744,6 +6750,127 @@ get_rule_orderby(List *orderList, List *targetList, } } +/* + * Helper function to append quantifier string for pattern node + */ +static void +append_pattern_quantifier(StringInfo buf, RPRPatternNode *node) +{ + bool has_quantifier = true; + + if (node->min == 1 && node->max == 1) + { + /* {1,1} = no quantifier */ + has_quantifier = false; + } + else if (node->min == 0 && node->max == INT_MAX) + appendStringInfoChar(buf, '*'); + else if (node->min == 1 && node->max == INT_MAX) + appendStringInfoChar(buf, '+'); + else if (node->min == 0 && node->max == 1) + appendStringInfoChar(buf, '?'); + else if (node->max == INT_MAX) + appendStringInfo(buf, "{%d,}", node->min); + else if (node->min == node->max) + appendStringInfo(buf, "{%d}", node->min); + else + appendStringInfo(buf, "{%d,%d}", node->min, node->max); + + if (node->reluctant && has_quantifier) + appendStringInfoChar(buf, '?'); +} + +/* + * Recursive helper to display RPRPatternNode tree + */ +static void +get_rule_pattern_node(RPRPatternNode *node, deparse_context *context) +{ + StringInfo buf = context->buf; + ListCell *lc; + const char *sep; + + if (node == NULL) + return; + + switch (node->nodeType) + { + case RPR_PATTERN_VAR: + appendStringInfoString(buf, node->varName); + append_pattern_quantifier(buf, node); + break; + + case RPR_PATTERN_SEQ: + sep = ""; + foreach(lc, node->children) + { + appendStringInfoString(buf, sep); + get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context); + sep = " "; + } + break; + + case RPR_PATTERN_ALT: + sep = ""; + foreach(lc, node->children) + { + appendStringInfoString(buf, sep); + get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context); + sep = " | "; + } + break; + + case RPR_PATTERN_GROUP: + appendStringInfoChar(buf, '('); + sep = ""; + foreach(lc, node->children) + { + appendStringInfoString(buf, sep); + get_rule_pattern_node((RPRPatternNode *) lfirst(lc), context); + sep = " "; + } + appendStringInfoChar(buf, ')'); + append_pattern_quantifier(buf, node); + break; + } +} + +/* + * Display a PATTERN clause. + */ +static void +get_rule_pattern(RPRPatternNode *rpPattern, bool force_colno, + deparse_context *context) +{ + StringInfo buf = context->buf; + + appendStringInfoChar(buf, '('); + get_rule_pattern_node(rpPattern, context); + appendStringInfoChar(buf, ')'); +} + +/* + * Display a DEFINE clause. + */ +static void +get_rule_define(List *defineClause, bool force_colno, deparse_context *context) +{ + StringInfo buf = context->buf; + const char *sep; + ListCell *lc_def; + + sep = " "; + + foreach(lc_def, defineClause) + { + TargetEntry *te = (TargetEntry *) lfirst(lc_def); + + appendStringInfo(buf, "%s%s AS ", sep, te->resname); + get_rule_expr((Node *) te->expr, context, false); + sep = ",\n "; + } +} + /* * Display a WINDOW clause. * @@ -6824,6 +6951,7 @@ get_rule_windowspec(WindowClause *wc, List *targetList, get_rule_orderby(wc->orderClause, targetList, false, context); needspace = true; } + /* framing clause is never inherited, so print unless it's default */ if (wc->frameOptions & FRAMEOPTION_NONDEFAULT) { @@ -6832,7 +6960,51 @@ get_rule_windowspec(WindowClause *wc, List *targetList, get_window_frame_options(wc->frameOptions, wc->startOffset, wc->endOffset, context); + needspace = true; + } + + /* RPR */ + if (wc->rpSkipTo == ST_NEXT_ROW) + { + if (needspace) + appendStringInfoChar(buf, ' '); + appendStringInfoString(buf, + "\n AFTER MATCH SKIP TO NEXT ROW "); + needspace = true; + } + else if (wc->rpSkipTo == ST_PAST_LAST_ROW) + { + if (needspace) + appendStringInfoChar(buf, ' '); + appendStringInfoString(buf, + "\n AFTER MATCH SKIP PAST LAST ROW "); + needspace = true; + } + if (wc->initial) + { + if (needspace) + appendStringInfoChar(buf, ' '); + appendStringInfoString(buf, "\n INITIAL"); + needspace = true; } + if (wc->rpPattern) + { + if (needspace) + appendStringInfoChar(buf, ' '); + appendStringInfoString(buf, "\n PATTERN "); + get_rule_pattern(wc->rpPattern, false, context); + needspace = true; + } + + if (wc->defineClause) + { + if (needspace) + appendStringInfoChar(buf, ' '); + appendStringInfoString(buf, "\n DEFINE\n"); + get_rule_define(wc->defineClause, false, context); + appendStringInfoChar(buf, ' '); + } + appendStringInfoChar(buf, ')'); } -- 2.43.0 [application/octet-stream] v39-0004-Row-pattern-recognition-patch-planner.patch (40.6K, ../../[email protected]/5-v39-0004-Row-pattern-recognition-patch-planner.patch) download | inline diff: From e32366d964062fdea56e8980c0423f6f0d2c6389 Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii <[email protected]> Date: Fri, 16 Jan 2026 13:11:21 +0900 Subject: [PATCH v39 4/8] Row pattern recognition patch (planner). --- src/backend/optimizer/plan/Makefile | 1 + src/backend/optimizer/plan/createplan.c | 64 +- src/backend/optimizer/plan/meson.build | 1 + src/backend/optimizer/plan/planner.c | 3 + src/backend/optimizer/plan/rpr.c | 1024 +++++++++++++++++++++ src/backend/optimizer/plan/setrefs.c | 27 +- src/backend/optimizer/prep/prepjointree.c | 9 + src/include/nodes/plannodes.h | 76 ++ src/include/optimizer/rpr.h | 46 + 9 files changed, 1233 insertions(+), 18 deletions(-) create mode 100644 src/backend/optimizer/plan/rpr.c create mode 100644 src/include/optimizer/rpr.h diff --git a/src/backend/optimizer/plan/Makefile b/src/backend/optimizer/plan/Makefile index 80ef162e484..7e0167789d8 100644 --- a/src/backend/optimizer/plan/Makefile +++ b/src/backend/optimizer/plan/Makefile @@ -19,6 +19,7 @@ OBJS = \ planagg.o \ planmain.o \ planner.o \ + rpr.o \ setrefs.o \ subselect.o diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index eb4806b084a..409623a1d1d 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -35,6 +35,7 @@ #include "optimizer/prep.h" #include "optimizer/restrictinfo.h" #include "optimizer/subselect.h" +#include "optimizer/rpr.h" #include "optimizer/tlist.h" #include "parser/parse_clause.h" #include "parser/parsetree.h" @@ -287,7 +288,10 @@ static Memoize *make_memoize(Plan *lefttree, Oid *hashoperators, static WindowAgg *make_windowagg(List *tlist, WindowClause *wc, int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations, int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations, - List *runCondition, List *qual, bool topWindow, + List *runCondition, RPSkipTo rpSkipTo, + RPRPattern *compiledPattern, + List *defineClause, List *defineInitial, + List *qual, bool topWindow, Plan *lefttree); static Group *make_group(List *tlist, List *qual, int numGroupCols, AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations, @@ -2528,21 +2532,37 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path) ordNumCols++; } - /* And finally we can make the WindowAgg node */ - plan = make_windowagg(tlist, - wc, - partNumCols, - partColIdx, - partOperators, - partCollations, - ordNumCols, - ordColIdx, - ordOperators, - ordCollations, - best_path->runCondition, - best_path->qual, - best_path->topwindow, - subplan); + /* Build defineVariableList from defineClause for pattern compilation */ + { + List *defineVariableList = NIL; + + foreach(lc, wc->defineClause) + { + TargetEntry *te = (TargetEntry *) lfirst(lc); + defineVariableList = lappend(defineVariableList, + makeString(pstrdup(te->resname))); + } + + /* And finally we can make the WindowAgg node */ + plan = make_windowagg(tlist, + wc, + partNumCols, + partColIdx, + partOperators, + partCollations, + ordNumCols, + ordColIdx, + ordOperators, + ordCollations, + best_path->runCondition, + wc->rpSkipTo, + buildRPRPattern(wc->rpPattern, defineVariableList), + wc->defineClause, + wc->defineInitial, + best_path->qual, + best_path->topwindow, + subplan); + } copy_generic_path_info(&plan->plan, (Path *) best_path); @@ -6608,7 +6628,10 @@ static WindowAgg * make_windowagg(List *tlist, WindowClause *wc, int partNumCols, AttrNumber *partColIdx, Oid *partOperators, Oid *partCollations, int ordNumCols, AttrNumber *ordColIdx, Oid *ordOperators, Oid *ordCollations, - List *runCondition, List *qual, bool topWindow, Plan *lefttree) + List *runCondition, RPSkipTo rpSkipTo, + RPRPattern *compiledPattern, + List *defineClause, List *defineInitial, + List *qual, bool topWindow, Plan *lefttree) { WindowAgg *node = makeNode(WindowAgg); Plan *plan = &node->plan; @@ -6635,6 +6658,13 @@ make_windowagg(List *tlist, WindowClause *wc, node->inRangeAsc = wc->inRangeAsc; node->inRangeNullsFirst = wc->inRangeNullsFirst; node->topWindow = topWindow; + node->rpSkipTo = rpSkipTo; + + /* Store compiled pattern for NFA execution */ + node->rpPattern = compiledPattern; + + node->defineClause = defineClause; + node->defineInitial = defineInitial; plan->targetlist = tlist; plan->lefttree = lefttree; diff --git a/src/backend/optimizer/plan/meson.build b/src/backend/optimizer/plan/meson.build index c565b2adbcc..5b2381cd774 100644 --- a/src/backend/optimizer/plan/meson.build +++ b/src/backend/optimizer/plan/meson.build @@ -7,6 +7,7 @@ backend_sources += files( 'planagg.c', 'planmain.c', 'planner.c', + 'rpr.c', 'setrefs.c', 'subselect.c', ) diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 7a6b8b749f2..2a93feee25b 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -964,6 +964,9 @@ subquery_planner(PlannerGlobal *glob, Query *parse, char *plan_name, EXPRKIND_LIMIT); wc->endOffset = preprocess_expression(root, wc->endOffset, EXPRKIND_LIMIT); + wc->defineClause = (List *) preprocess_expression(root, + (Node *) wc->defineClause, + EXPRKIND_TARGET); } parse->limitOffset = preprocess_expression(root, parse->limitOffset, diff --git a/src/backend/optimizer/plan/rpr.c b/src/backend/optimizer/plan/rpr.c new file mode 100644 index 00000000000..0022495c1cd --- /dev/null +++ b/src/backend/optimizer/plan/rpr.c @@ -0,0 +1,1024 @@ +/*------------------------------------------------------------------------- + * + * rpr.c + * Row Pattern Recognition pattern compilation for planner + * + * This file contains functions for optimizing RPR pattern AST and + * compiling it to bytecode for execution by WindowAgg. + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/optimizer/plan/rpr.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "nodes/makefuncs.h" +#include "optimizer/rpr.h" + +/* Forward declarations */ +static bool rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b); +static RPRPatternNode *optimizeRPRPattern(RPRPatternNode *pattern); +static void scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars, + int *numElements, RPRDepth depth, RPRDepth *maxDepth); +static void fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, + int *idx, RPRDepth depth); +static RPRVarId getVarIdFromPattern(RPRPattern *pat, const char *varName); +static void computeAbsorbability(RPRPattern *pattern); + +/* + * rprPatternChildrenEqual + * Compare children of two GROUP nodes for equality. + * + * Returns true if the children lists are structurally identical. + * Used for GROUP merge optimization where we ignore outer quantifiers. + */ +static bool +rprPatternChildrenEqual(List *a, List *b) +{ + ListCell *lca, + *lcb; + + if (list_length(a) != list_length(b)) + return false; + + forboth(lca, a, lcb, b) + { + if (!rprPatternEqual((RPRPatternNode *) lfirst(lca), + (RPRPatternNode *) lfirst(lcb))) + return false; + } + + return true; +} + +/* + * rprPatternEqual + * Compare two RPRPatternNode trees for equality. + * + * Returns true if the trees are structurally identical. + */ +static bool +rprPatternEqual(RPRPatternNode *a, RPRPatternNode *b) +{ + ListCell *lca, + *lcb; + + if (a == NULL && b == NULL) + return true; + if (a == NULL || b == NULL) + return false; + + /* Must have same node type and quantifiers */ + if (a->nodeType != b->nodeType) + return false; + if (a->min != b->min || a->max != b->max) + return false; + if (a->reluctant != b->reluctant) + return false; + + switch (a->nodeType) + { + case RPR_PATTERN_VAR: + return strcmp(a->varName, b->varName) == 0; + + case RPR_PATTERN_SEQ: + case RPR_PATTERN_ALT: + case RPR_PATTERN_GROUP: + /* Must have same number of children */ + if (list_length(a->children) != list_length(b->children)) + return false; + + /* Compare each child */ + forboth(lca, a->children, lcb, b->children) + { + if (!rprPatternEqual((RPRPatternNode *) lfirst(lca), + (RPRPatternNode *) lfirst(lcb))) + return false; + } + return true; + } + + return false; /* keep compiler quiet */ +} + +/* + * optimizeRPRPattern + * Optimize RPRPatternNode tree in a single pass. + * + * Optimizations applied (bottom-up, in order per node): + * 1. Flatten nested SEQ: (A (B C)) -> (A B C) + * 2. Flatten nested ALT: (A | (B | C)) -> (A | B | C) + * 3. Unwrap GROUP{1,1}: ((A)) -> A, (A B){1,1} -> A B + * 4. Quantifier multiplication: (A{2}){3} -> A{6} + * 5. Remove duplicate alternatives: (A | B | A) -> (A | B) + * 6. Merge consecutive vars: A A A -> A{3,3} + * 7. Remove single-item SEQ/ALT wrappers + */ +static RPRPatternNode * +optimizeRPRPattern(RPRPatternNode *pattern) +{ + ListCell *lc; + List *newChildren; + + if (pattern == NULL) + return NULL; + + switch (pattern->nodeType) + { + case RPR_PATTERN_VAR: + /* Leaf node - nothing to optimize */ + return pattern; + + case RPR_PATTERN_SEQ: + { + RPRPatternNode *prev = NULL; + + /* Recursively optimize children, flatten SEQ/GROUP{1,1} */ + newChildren = NIL; + foreach(lc, pattern->children) + { + RPRPatternNode *child = (RPRPatternNode *) lfirst(lc); + RPRPatternNode *opt = optimizeRPRPattern(child); + + /* Flatten GROUP{1,1} or nested SEQ */ + if ((opt->nodeType == RPR_PATTERN_GROUP && + opt->min == 1 && opt->max == 1 && !opt->reluctant) || + opt->nodeType == RPR_PATTERN_SEQ) + { + newChildren = list_concat(newChildren, + list_copy(opt->children)); + } + else + { + newChildren = lappend(newChildren, opt); + } + } + + /* + * Merge consecutive identical VAR nodes with any quantifier. + * A{m1,M1} A{m2,M2} -> A{m1+m2, M1+M2} + * where INF + x = INF + */ + { + List *mergedChildren = NIL; + + foreach(lc, newChildren) + { + RPRPatternNode *child = (RPRPatternNode *) lfirst(lc); + + if (child->nodeType == RPR_PATTERN_VAR && !child->reluctant) + { + if (prev != NULL && + prev->nodeType == RPR_PATTERN_VAR && + strcmp(prev->varName, child->varName) == 0 && + !prev->reluctant) + { + /* + * Merge: accumulate min/max into prev. + * INF + anything = INF + */ + prev->min += child->min; + if (prev->max == RPR_QUANTITY_INF || + child->max == RPR_QUANTITY_INF) + prev->max = RPR_QUANTITY_INF; + else + prev->max += child->max; + } + else + { + /* Flush previous and start new */ + if (prev != NULL) + mergedChildren = lappend(mergedChildren, prev); + prev = child; + } + } + else + { + /* Non-mergeable - flush previous */ + if (prev != NULL) + mergedChildren = lappend(mergedChildren, prev); + mergedChildren = lappend(mergedChildren, child); + prev = NULL; + } + } + + /* Flush remaining */ + if (prev != NULL) + mergedChildren = lappend(mergedChildren, prev); + + newChildren = mergedChildren; + } + + /* + * Merge sequence prefix/suffix into GROUP with matching children. + * A B (A B)+ -> (A B){2,} + * (A B)+ A B -> (A B){2,} + * A B (A B)+ A B -> (A B){3,} + */ + { + List *groupMergedChildren = NIL; + int numChildren = list_length(newChildren); + int i; + bool merged = false; + int skipUntil = -1; /* skip suffix elements already absorbed */ + + for (i = 0; i < numChildren; i++) + { + RPRPatternNode *child = (RPRPatternNode *) list_nth(newChildren, i); + + /* Skip elements that were absorbed as suffix */ + if (i < skipUntil) + continue; + + /* + * If this is a GROUP, see if preceding/following elements + * match its children. + * GROUP's content may be wrapped in a SEQ - unwrap for comparison. + */ + if (child->nodeType == RPR_PATTERN_GROUP && !child->reluctant) + { + List *groupContent = child->children; + int groupChildCount; + int prefixLen = list_length(groupMergedChildren); + + /* + * If GROUP has single SEQ child, compare with SEQ's children. + * e.g., (A B)+ is GROUP[SEQ[A,B]], we want to compare [A,B]. + */ + if (list_length(groupContent) == 1) + { + RPRPatternNode *inner = (RPRPatternNode *) linitial(groupContent); + + if (inner->nodeType == RPR_PATTERN_SEQ) + groupContent = inner->children; + } + + groupChildCount = list_length(groupContent); + + /* + * PREFIX MERGE: Check if preceding elements match. + * Keep absorbing as long as we have matching prefixes. + */ + while (prefixLen >= groupChildCount && groupChildCount > 0) + { + List *prefixElements = NIL; + int j; + + /* Extract last groupChildCount elements from prefix */ + for (j = prefixLen - groupChildCount; j < prefixLen; j++) + { + prefixElements = lappend(prefixElements, + list_nth(groupMergedChildren, j)); + } + + /* Compare with GROUP's (possibly unwrapped) children */ + if (rprPatternChildrenEqual(prefixElements, groupContent)) + { + /* + * Match! Merge by incrementing GROUP's min. + * Remove the prefix elements from output. + */ + child->min += 1; + + /* Rebuild groupMergedChildren without matched prefix */ + { + List *trimmed = NIL; + + for (j = 0; j < prefixLen - groupChildCount; j++) + { + trimmed = lappend(trimmed, + list_nth(groupMergedChildren, j)); + } + groupMergedChildren = trimmed; + prefixLen = list_length(groupMergedChildren); + } + merged = true; + } + else + { + list_free(prefixElements); + break; + } + + list_free(prefixElements); + } + + /* + * SUFFIX MERGE: Check if following elements match. + * Keep absorbing as long as we have matching suffixes. + */ + while (i + groupChildCount < numChildren && groupChildCount > 0) + { + List *suffixElements = NIL; + int j; + int suffixStart = i + 1; + + /* Adjust for already absorbed elements */ + if (skipUntil > suffixStart) + break; + + /* Extract next groupChildCount elements as suffix */ + for (j = 0; j < groupChildCount; j++) + { + int idx = suffixStart + j; + + if (idx >= numChildren) + break; + suffixElements = lappend(suffixElements, + list_nth(newChildren, idx)); + } + + /* Compare with GROUP's children */ + if (list_length(suffixElements) == groupChildCount && + rprPatternChildrenEqual(suffixElements, groupContent)) + { + /* + * Match! Absorb suffix by incrementing min and skipping. + */ + child->min += 1; + skipUntil = suffixStart + groupChildCount; + merged = true; + /* Update i to continue suffix check after absorbed elements */ + i = skipUntil - 1; + } + else + { + list_free(suffixElements); + break; + } + + list_free(suffixElements); + } + } + + groupMergedChildren = lappend(groupMergedChildren, child); + } + + if (merged) + newChildren = groupMergedChildren; + } + + pattern->children = newChildren; + + /* Unwrap single-item SEQ */ + if (list_length(pattern->children) == 1) + return (RPRPatternNode *) linitial(pattern->children); + + return pattern; + } + + case RPR_PATTERN_ALT: + { + ListCell *lc2; + + /* Recursively optimize children, flatten nested ALT */ + newChildren = NIL; + foreach(lc, pattern->children) + { + RPRPatternNode *child = (RPRPatternNode *) lfirst(lc); + RPRPatternNode *opt = optimizeRPRPattern(child); + + if (opt->nodeType == RPR_PATTERN_ALT) + { + newChildren = list_concat(newChildren, + list_copy(opt->children)); + } + else + { + newChildren = lappend(newChildren, opt); + } + } + + /* Remove duplicate alternatives */ + { + List *uniqueChildren = NIL; + + foreach(lc, newChildren) + { + RPRPatternNode *child = (RPRPatternNode *) lfirst(lc); + bool isDuplicate = false; + + foreach(lc2, uniqueChildren) + { + if (rprPatternEqual((RPRPatternNode *) lfirst(lc2), child)) + { + isDuplicate = true; + break; + } + } + + if (!isDuplicate) + uniqueChildren = lappend(uniqueChildren, child); + } + + pattern->children = uniqueChildren; + } + + /* Unwrap single-item ALT */ + if (list_length(pattern->children) == 1) + return (RPRPatternNode *) linitial(pattern->children); + + return pattern; + } + + case RPR_PATTERN_GROUP: + /* Recursively optimize children */ + newChildren = NIL; + foreach(lc, pattern->children) + { + RPRPatternNode *child = (RPRPatternNode *) lfirst(lc); + + newChildren = lappend(newChildren, optimizeRPRPattern(child)); + } + pattern->children = newChildren; + + /* Quantifier multiplication: (A{m}){n} -> A{m*n} */ + if (list_length(pattern->children) == 1 && !pattern->reluctant) + { + RPRPatternNode *child = (RPRPatternNode *) linitial(pattern->children); + + if (child->nodeType == RPR_PATTERN_VAR && !child->reluctant) + { + if (pattern->max != RPR_QUANTITY_INF && child->max != RPR_QUANTITY_INF) + { + int64 new_min_64 = (int64) pattern->min * child->min; + int64 new_max_64 = (int64) pattern->max * child->max; + + if (new_min_64 < RPR_QUANTITY_INF && new_max_64 < RPR_QUANTITY_INF) + { + child->min = (int) new_min_64; + child->max = (int) new_max_64; + return child; + } + } + } + } + + /* Unwrap GROUP{1,1} */ + if (pattern->min == 1 && pattern->max == 1 && !pattern->reluctant) + { + if (list_length(pattern->children) == 1) + return (RPRPatternNode *) linitial(pattern->children); + + /* Multiple children: convert to SEQ */ + pattern->nodeType = RPR_PATTERN_SEQ; + } + + return pattern; + } + + return pattern; /* keep compiler quiet */ +} + +/* + * scanRPRPattern + * Single-pass scan: collect variable names and count elements. + * + * Collects unique variable names in pattern encounter order (max RPR_VARID_MAX). + * Also counts elements and tracks maximum nesting depth. + */ +static void +scanRPRPattern(RPRPatternNode *node, char **varNames, int *numVars, + int *numElements, RPRDepth depth, RPRDepth *maxDepth) +{ + ListCell *lc; + int i; + + if (node == NULL) + return; + + /* Track maximum depth */ + if (depth > *maxDepth) + *maxDepth = depth; + + switch (node->nodeType) + { + case RPR_PATTERN_VAR: + /* Count element */ + (*numElements)++; + + /* Collect variable name if not already present */ + for (i = 0; i < *numVars; i++) + { + if (strcmp(varNames[i], node->varName) == 0) + return; /* Already have this variable */ + } + + /* New variable */ + if (*numVars >= RPR_VARID_MAX) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("too many pattern variables"), + errdetail("Maximum is %d.", RPR_VARID_MAX))); + + varNames[*numVars] = node->varName; + (*numVars)++; + break; + + case RPR_PATTERN_SEQ: + /* Sequence: just recurse into children */ + foreach(lc, node->children) + { + scanRPRPattern((RPRPatternNode *) lfirst(lc), varNames, numVars, + numElements, depth, maxDepth); + } + break; + + case RPR_PATTERN_GROUP: + /* Recurse into children at increased depth */ + foreach(lc, node->children) + { + scanRPRPattern((RPRPatternNode *) lfirst(lc), varNames, numVars, + numElements, depth + 1, maxDepth); + } + + /* Add END element if group has non-trivial quantifier */ + if (node->min != 1 || node->max != 1) + (*numElements)++; + break; + + case RPR_PATTERN_ALT: + /* Count ALT start element */ + (*numElements)++; + + /* Recurse into children at increased depth */ + foreach(lc, node->children) + { + scanRPRPattern((RPRPatternNode *) lfirst(lc), varNames, numVars, + numElements, depth + 1, maxDepth); + } + break; + } +} + +/* + * getVarIdFromPattern + * Get variable ID for a variable name from RPRPattern. + * + * Returns the index of the variable in the varNames array. + */ +static RPRVarId +getVarIdFromPattern(RPRPattern *pat, const char *varName) +{ + for (int i = 0; i < pat->numVars; i++) + { + if (strcmp(pat->varNames[i], varName) == 0) + return (RPRVarId) i; + } + + /* Should not happen - variable should already be collected */ + elog(ERROR, "pattern variable \"%s\" not found", varName); + pg_unreachable(); +} + +/* + * fillRPRPattern + * Fill the pattern elements array (second pass). + * + * This traverses the AST and fills in the pre-allocated elements array. + * The idx pointer tracks the current position in the array. + */ +static void +fillRPRPattern(RPRPatternNode *node, RPRPattern *pat, int *idx, RPRDepth depth) +{ + ListCell *lc; + RPRPatternElement *elem; + int groupStartIdx; + int altStartIdx; + List *altBranchStarts; + List *altEndPositions; + + if (node == NULL) + return; + + switch (node->nodeType) + { + case RPR_PATTERN_SEQ: + /* Sequence: fill each child in order */ + foreach(lc, node->children) + { + fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth); + } + break; + + case RPR_PATTERN_VAR: + /* Variable: create element with varId */ + elem = &pat->elements[*idx]; + memset(elem, 0, sizeof(RPRPatternElement)); + elem->varId = getVarIdFromPattern(pat, node->varName); + elem->depth = depth; + elem->min = node->min; + elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max; + elem->next = RPR_ELEMIDX_INVALID; + elem->jump = RPR_ELEMIDX_INVALID; + if (node->reluctant) + elem->flags |= RPR_ELEM_RELUCTANT; + (*idx)++; + break; + + case RPR_PATTERN_GROUP: + groupStartIdx = *idx; + + /* Fill group content at increased depth */ + foreach(lc, node->children) + { + fillRPRPattern((RPRPatternNode *) lfirst(lc), pat, idx, depth + 1); + } + + /* Add group end marker if group has non-trivial quantifier */ + if (node->min != 1 || node->max != 1) + { + elem = &pat->elements[*idx]; + memset(elem, 0, sizeof(RPRPatternElement)); + elem->varId = RPR_VARID_END; + elem->depth = depth; /* parent depth for iteration count */ + elem->min = node->min; + elem->max = (node->max == INT_MAX) ? RPR_QUANTITY_INF : node->max; + elem->next = RPR_ELEMIDX_INVALID; + elem->jump = groupStartIdx; /* jump back to group start */ + if (node->reluctant) + elem->flags |= RPR_ELEM_RELUCTANT; + (*idx)++; + } + break; + + case RPR_PATTERN_ALT: + /* Add alternation start marker */ + altStartIdx = *idx; + elem = &pat->elements[*idx]; + memset(elem, 0, sizeof(RPRPatternElement)); + elem->varId = RPR_VARID_ALT; + elem->depth = depth; + elem->min = 1; + elem->max = 1; + elem->next = RPR_ELEMIDX_INVALID; + elem->jump = RPR_ELEMIDX_INVALID; + (*idx)++; + + altBranchStarts = NIL; + altEndPositions = NIL; + + /* Fill each alternative */ + foreach(lc, node->children) + { + RPRPatternNode *alt = (RPRPatternNode *) lfirst(lc); + int branchStart = *idx; + + altBranchStarts = lappend_int(altBranchStarts, branchStart); + + fillRPRPattern(alt, pat, idx, depth + 1); + + /* Record end position if any elements were added */ + if (*idx > branchStart) + altEndPositions = lappend_int(altEndPositions, *idx - 1); + } + + /* Set ALT_START.next to first alternative */ + if (list_length(altBranchStarts) > 0) + pat->elements[altStartIdx].next = linitial_int(altBranchStarts); + + /* Set jump on first element of each alternative to next alternative */ + foreach(lc, altBranchStarts) + { + int firstElemIdx = lfirst_int(lc); + + if (lnext(altBranchStarts, lc) != NULL) + { + int nextAltStart = lfirst_int(lnext(altBranchStarts, lc)); + + pat->elements[firstElemIdx].jump = nextAltStart; + } + /* Last alternative's jump stays as -1 (default) */ + } + + /* Set next on last element of each alternative to after the alternation */ + { + int afterAltIdx = *idx; + + foreach(lc, altEndPositions) + { + int endPos = lfirst_int(lc); + + if (pat->elements[endPos].next == RPR_ELEMIDX_INVALID) + pat->elements[endPos].next = afterAltIdx; + } + } + + list_free(altBranchStarts); + list_free(altEndPositions); + break; + } +} + +/* + * buildRPRPattern + * Build flat pattern element array from AST. + * + * Optimizes the pattern tree, then uses 2-pass: count elements, allocate and fill. + * Returns NULL if pattern is NULL. + * + * This function is called from createplan.c during plan creation. + */ +RPRPattern * +buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList) +{ + RPRPattern *result; + RPRPatternNode *optimized; + char *varNamesStack[RPR_VARID_MAX + 1]; /* stack array for collection */ + int numVars = 0; + int numElements = 0; + RPRDepth maxDepth = 0; + int idx; + int finIdx; + int i; + RPRPatternElement *finElem; + ListCell *lc; + + if (pattern == NULL) + return NULL; + + /* Optimize the pattern tree (planner phase) */ + optimized = optimizeRPRPattern(pattern); + + /* + * First, populate varNames from defineVariableList in order. + * This ensures varId == defineIdx for all defined variables, + * eliminating the need for varIdToDefineIdx mapping. + */ + foreach(lc, defineVariableList) + { + char *varName = strVal(lfirst(lc)); + + if (numVars >= RPR_VARID_MAX) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("too many pattern variables"), + errdetail("Maximum is %d.", RPR_VARID_MAX))); + + varNamesStack[numVars] = varName; + numVars++; + } + + /* + * Pass 1: Single scan to collect variable names and count elements. + * Variables already in varNamesStack (from DEFINE) are skipped. + * Also counts elements and tracks max depth in one traversal. + */ + scanRPRPattern(optimized, varNamesStack, &numVars, + &numElements, 0, &maxDepth); + numElements++; /* +1 for FIN marker */ + + /* Check element count limit */ + if (numElements > RPR_ELEMIDX_MAX) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), + errmsg("pattern too complex"), + errdetail("Pattern has %d elements, maximum is %d.", + numElements, RPR_ELEMIDX_MAX))); + + /* + * Allocate result structure with makeNode for proper NodeTag. + */ + result = makeNode(RPRPattern); + result->numVars = numVars; + result->maxDepth = maxDepth + 1; /* +1: depth is 0-based, need counts[0..maxDepth] */ + result->numElements = numElements; + + /* Build varNames as char** array */ + if (numVars > 0) + { + result->varNames = palloc(numVars * sizeof(char *)); + for (i = 0; i < numVars; i++) + result->varNames[i] = pstrdup(varNamesStack[i]); + } + else + { + result->varNames = NULL; + } + + /* Allocate elements array separately (zero-init for reserved field) */ + result->elements = palloc0(numElements * sizeof(RPRPatternElement)); + + /* + * Pass 2: Fill elements directly into pre-allocated array. + */ + idx = 0; + fillRPRPattern(optimized, result, &idx, 0); + + /* Set up next pointers for elements that don't have one yet */ + finIdx = numElements - 1; /* FIN marker is at the last position */ + for (i = 0; i < finIdx; i++) + { + if (result->elements[i].next == RPR_ELEMIDX_INVALID) + { + /* Point to next element, or to FIN if this is the last */ + result->elements[i].next = (i < finIdx - 1) ? i + 1 : finIdx; + } + } + + /* Add FIN marker at the end */ + finElem = &result->elements[finIdx]; + memset(finElem, 0, sizeof(RPRPatternElement)); + finElem->varId = RPR_VARID_FIN; + finElem->depth = 0; + finElem->min = 1; + finElem->max = 1; + finElem->next = RPR_ELEMIDX_INVALID; + finElem->jump = RPR_ELEMIDX_INVALID; + + /* Compute context absorption eligibility */ + computeAbsorbability(result); + + return result; +} + +/* + * computeAbsorbability + * Determine if pattern supports context absorption optimization. + * + * Context absorption is safe when later matches are guaranteed to be + * suffixes of earlier matches (both row positions AND content). + * + * Sets RPR_ELEM_ABSORBABLE flag on the first element of each absorbable + * branch. A branch is absorbable if: + * - It starts with an unbounded element (greedy-first) + * - It has exactly one unbounded element + * - It's not inside an unbounded GROUP (GREEDY(ALT) case) + * + * pattern->isAbsorbable is set true if ANY branch is absorbable. + */ +static void +computeAbsorbability(RPRPattern *pattern) +{ + int i; + int maxAltDepth = -1; + RPRDepth minUnboundedGroupDepth = UINT8_MAX; + bool hasTopLevelAlt = false; + + pattern->isAbsorbable = false; + + if (pattern->numElements < 2) /* At minimum: one element + FIN */ + return; + + /* + * Scan elements to find ALT structure and unbounded GROUP depth. + */ + for (i = 0; i < pattern->numElements - 1; i++) + { + RPRPatternElement *elem = &pattern->elements[i]; + + if (elem->varId == RPR_VARID_ALT) + { + if (elem->depth > maxAltDepth) + maxAltDepth = elem->depth; + if (i == 0) + hasTopLevelAlt = true; + } + else if (elem->varId == RPR_VARID_END) + { + if (elem->max == INT32_MAX && elem->depth < minUnboundedGroupDepth) + minUnboundedGroupDepth = elem->depth; + } + } + + /* + * Check for GREEDY(ALT) pattern: ALT inside unbounded GROUP. + * In this case, no branch can be absorbable. + */ + if (maxAltDepth >= 0 && maxAltDepth > minUnboundedGroupDepth) + return; + + /* + * For top-level ALT patterns, check each branch separately. + * Set ABSORBABLE flag on branches that qualify. + */ + if (hasTopLevelAlt) + { + int branchStart = 1; /* after ALT marker */ + int patternEnd = pattern->numElements - 1; + + while (branchStart < patternEnd) + { + RPRPatternElement *branchFirst = &pattern->elements[branchStart]; + int branchEnd; + int branchUnbounded = 0; + int j; + bool branchAbsorbable = true; + + /* Find branch end */ + if (branchFirst->jump != RPR_ELEMIDX_INVALID) + branchEnd = branchFirst->jump; + else + branchEnd = patternEnd; + + /* First element of branch must be unbounded */ + if (branchFirst->max != INT32_MAX) + branchAbsorbable = false; + + /* Count unbounded in this branch - must be exactly 1 */ + if (branchAbsorbable) + { + for (j = branchStart; j < branchEnd; j++) + { + RPRPatternElement *elem = &pattern->elements[j]; + + if (elem->varId == RPR_VARID_END) + { + if (elem->max == INT32_MAX) + branchUnbounded++; + } + else if (elem->varId != RPR_VARID_ALT) + { + if (elem->max == INT32_MAX) + branchUnbounded++; + } + } + + if (branchUnbounded != 1) + branchAbsorbable = false; + } + + /* Set flag on branch's first element if absorbable */ + if (branchAbsorbable) + { + branchFirst->flags |= RPR_ELEM_ABSORBABLE; + pattern->isAbsorbable = true; + } + + branchStart = branchEnd; + } + return; + } + + /* + * Non-ALT pattern: check for absorbability. + * + * Case 1: First element is unbounded (A+ B, etc.) + * Case 2: Top-level unbounded GROUP (A B){2,} - GROUP END at depth 0 + * + * In both cases, must have exactly one unbounded element/group. + */ + { + RPRPatternElement *first = &pattern->elements[0]; + int numUnbounded = 0; + bool hasTopLevelUnboundedGroup = false; + RPRElemIdx topLevelGroupEnd = RPR_ELEMIDX_INVALID; + + /* Count unbounded elements and find top-level unbounded GROUP */ + for (i = 0; i < pattern->numElements - 1; i++) + { + RPRPatternElement *elem = &pattern->elements[i]; + + if (elem->varId == RPR_VARID_END) + { + if (elem->max == INT32_MAX) + { + numUnbounded++; + /* Check if this is a top-level GROUP (depth 0) */ + if (elem->depth == 0) + { + hasTopLevelUnboundedGroup = true; + topLevelGroupEnd = i; + } + } + } + else if (elem->varId != RPR_VARID_ALT) + { + if (elem->max == INT32_MAX) + numUnbounded++; + } + } + + /* Must have at least one unbounded element for absorption */ + if (numUnbounded < 1) + return; + + /* + * Case 1: First element is unbounded (e.g., A+ B) + */ + if (first->max == INT32_MAX) + { + first->flags |= RPR_ELEM_ABSORBABLE; + pattern->isAbsorbable = true; + return; + } + + /* + * Case 2: Top-level unbounded GROUP that spans entire pattern. + * e.g., (A B){2,} where GROUP END is at the last position before FIN. + * The entire pattern content is inside this unbounded group. + */ + if (hasTopLevelUnboundedGroup && + topLevelGroupEnd == pattern->numElements - 2) + { + first->flags |= RPR_ELEM_ABSORBABLE; + pattern->isAbsorbable = true; + } + } +} diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 16d200cfb46..2efeec22102 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -211,7 +211,6 @@ static List *set_windowagg_runcondition_references(PlannerInfo *root, List *runcondition, Plan *plan); - /***************************************************************************** * * SUBPLAN REFERENCES @@ -2576,6 +2575,32 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset) NRM_EQUAL, NUM_EXEC_QUAL(plan)); + /* + * Modifies an expression tree in each DEFINE clause so that all Var + * nodes's varno refers to OUTER_VAR. + */ + if (IsA(plan, WindowAgg)) + { + WindowAgg *wplan = (WindowAgg *) plan; + + if (wplan->defineClause != NIL) + { + foreach(l, wplan->defineClause) + { + TargetEntry *tle = (TargetEntry *) lfirst(l); + + tle->expr = (Expr *) + fix_upper_expr(root, + (Node *) tle->expr, + subplan_itlist, + OUTER_VAR, + rtoffset, + NRM_EQUAL, + NUM_EXEC_QUAL(plan)); + } + } + } + pfree(subplan_itlist); } diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index c80bfc88d82..63d872b029d 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -2510,6 +2510,15 @@ perform_pullup_replace_vars(PlannerInfo *root, parse->returningList = (List *) pullup_replace_vars((Node *) parse->returningList, rvcontext); + foreach(lc, parse->windowClause) + { + WindowClause *wc = lfirst_node(WindowClause, lc); + + if (wc->defineClause != NIL) + wc->defineClause = (List *) + pullup_replace_vars((Node *) wc->defineClause, rvcontext); + } + if (parse->onConflict) { parse->onConflict->onConflictSet = (List *) diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 4bc6fb5670e..8d9097a8f4e 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -20,6 +20,7 @@ #include "lib/stringinfo.h" #include "nodes/bitmapset.h" #include "nodes/lockoptions.h" +#include "nodes/parsenodes.h" #include "nodes/primnodes.h" @@ -1223,6 +1224,69 @@ typedef struct Agg List *chain; } Agg; +/* ---------------- + * Row Pattern Recognition compiled pattern types + * ---------------- + */ + +/* Type definitions for RPR pattern elements */ +typedef uint8 RPRVarId; /* pattern variable ID */ +typedef uint8 RPRElemFlags; /* element flags */ +typedef uint8 RPRDepth; /* group nesting depth */ +typedef int32 RPRQuantity; /* quantifier min/max */ +typedef int16 RPRElemIdx; /* element array index */ + +/* + * RPRPatternElement - flat element for NFA pattern matching (16 bytes) + * + * Layout optimized for alignment (no padding holes): + * varId(1) + depth(1) + flags(1) + reserved(1) + min(4) + max(4) + next(2) + jump(2) + */ +typedef struct RPRPatternElement +{ + RPRVarId varId; /* variable ID, or special value for control */ + RPRDepth depth; /* group nesting depth */ + RPRElemFlags flags; /* flags (reluctant, etc.) */ + uint8 reserved; /* reserved padding byte */ + RPRQuantity min; /* quantifier minimum */ + RPRQuantity max; /* quantifier maximum */ + RPRElemIdx next; /* next element index */ + RPRElemIdx jump; /* jump target (for ALT/GROUP) */ +} RPRPatternElement; + +/* + * RPRPattern - compiled pattern for NFA execution + * + * Requires custom copy/out/read functions due to elements array. + */ +typedef struct RPRPattern +{ + pg_node_attr(custom_copy_equal, custom_read_write) + + NodeTag type; /* T_RPRPattern */ + int numVars; /* number of pattern variables */ + char **varNames; /* array of variable names (DEFINE order first) */ + RPRDepth maxDepth; /* maximum group nesting depth */ + int numElements; /* number of elements */ + RPRPatternElement *elements; /* array of pattern elements */ + + /* + * Context absorption optimization. + * + * Absorption is only safe when later matches are guaranteed to be + * suffixes of earlier matches. This requires simple pattern structure: + * + * Case 1: No ALT, single unbounded element (A+, (A B)+) + * Case 2: Top-level ALT with each branch being single unbounded (A+ | B+) + * + * Complex patterns like A B (A B)+ could theoretically be transformed to + * (A B){2,} for absorption, but this changes lexical order and is not + * implemented. Similarly, (A|B)+ cannot be absorbed because different + * start positions produce different match contents (not suffix relation). + */ + bool isAbsorbable; /* true if pattern supports context absorption */ +} RPRPattern; + /* ---------------- * window aggregate node * ---------------- @@ -1293,6 +1357,18 @@ typedef struct WindowAgg /* nulls sort first for in_range tests? */ bool inRangeNullsFirst; + /* Row Pattern Recognition AFTER MATCH SKIP clause */ + RPSkipTo rpSkipTo; /* Row Pattern Skip To type */ + + /* Compiled Row Pattern for NFA execution */ + struct RPRPattern *rpPattern; + + /* Row Pattern DEFINE clause (list of TargetEntry) */ + List *defineClause; + + /* Row Pattern DEFINE variable initial names (list of String) */ + List *defineInitial; + /* * false for all apart from the WindowAgg that's closest to the root of * the plan diff --git a/src/include/optimizer/rpr.h b/src/include/optimizer/rpr.h new file mode 100644 index 00000000000..3771ed203ec --- /dev/null +++ b/src/include/optimizer/rpr.h @@ -0,0 +1,46 @@ +/*------------------------------------------------------------------------- + * + * rpr.h + * Row Pattern Recognition pattern compilation for planner + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/optimizer/rpr.h + * + *------------------------------------------------------------------------- + */ +#ifndef OPTIMIZER_RPR_H +#define OPTIMIZER_RPR_H + +#include "nodes/parsenodes.h" +#include "nodes/plannodes.h" + +/* Limits and special values */ +#define RPR_VARID_MAX 252 /* max pattern variables: 252 */ +#define RPR_DEPTH_MAX UINT8_MAX /* max nesting depth: 255 */ +#define RPR_QUANTITY_INF INT32_MAX /* unbounded quantifier */ +#define RPR_ELEMIDX_MAX INT16_MAX /* max pattern elements */ +#define RPR_ELEMIDX_INVALID ((RPRElemIdx) -1) /* invalid index */ + +/* Special varId values for control elements (253-255) */ +#define RPR_VARID_ALT ((RPRVarId) 253) /* alternation start */ +#define RPR_VARID_END ((RPRVarId) 254) /* group end */ +#define RPR_VARID_FIN ((RPRVarId) 255) /* pattern finish */ + +/* Element flags */ +#define RPR_ELEM_RELUCTANT 0x01 /* reluctant (non-greedy) quantifier */ +#define RPR_ELEM_ABSORBABLE 0x02 /* branch supports context absorption */ + +/* Accessor macros for RPRPatternElement */ +#define RPRElemIsReluctant(e) ((e)->flags & RPR_ELEM_RELUCTANT) +#define RPRElemIsAbsorbable(e) ((e)->flags & RPR_ELEM_ABSORBABLE) +#define RPRElemIsVar(e) ((e)->varId <= RPR_VARID_MAX) +#define RPRElemIsAlt(e) ((e)->varId == RPR_VARID_ALT) +#define RPRElemIsEnd(e) ((e)->varId == RPR_VARID_END) +#define RPRElemIsFin(e) ((e)->varId == RPR_VARID_FIN) +#define RPRElemCanSkip(e) ((e)->min == 0) + +extern RPRPattern *buildRPRPattern(RPRPatternNode *pattern, List *defineVariableList); + +#endif /* OPTIMIZER_RPR_H */ -- 2.43.0 [application/octet-stream] v39-0005-Row-pattern-recognition-patch-executor-and-comma.patch (66.3K, ../../[email protected]/6-v39-0005-Row-pattern-recognition-patch-executor-and-comma.patch) download | inline diff: From 72f6195a3eadbf364ad14acf137fd5f1ab5d49b7 Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii <[email protected]> Date: Fri, 16 Jan 2026 13:11:21 +0900 Subject: [PATCH v39 5/8] Row pattern recognition patch (executor and commands). --- src/backend/commands/explain.c | 143 +++ src/backend/executor/nodeWindowAgg.c | 1782 +++++++++++++++++++++++++- src/backend/utils/adt/windowfuncs.c | 25 +- src/include/catalog/pg_proc.dat | 6 + src/include/nodes/execnodes.h | 63 + 5 files changed, 2008 insertions(+), 11 deletions(-) diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index b7bb111688c..969c9195864 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -29,6 +29,7 @@ #include "nodes/extensible.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" +#include "optimizer/rpr.h" #include "parser/analyze.h" #include "parser/parsetree.h" #include "rewrite/rewriteHandler.h" @@ -117,6 +118,8 @@ static void show_window_def(WindowAggState *planstate, static void show_window_keys(StringInfo buf, PlanState *planstate, int nkeys, AttrNumber *keycols, List *ancestors, ExplainState *es); +static void append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem); +static char *deparse_rpr_pattern(RPRPattern *pattern); static void show_storage_info(char *maxStorageType, int64 maxSpaceUsed, ExplainState *es); static void show_tablesample(TableSampleClause *tsc, PlanState *planstate, @@ -2889,6 +2892,134 @@ show_sortorder_options(StringInfo buf, Node *sortexpr, } } +/* + * Append quantifier suffix for a pattern element. + */ +static void +append_rpr_quantifier(StringInfo buf, RPRPatternElement *elem) +{ + if (elem->min == 1 && elem->max == 1) + return; /* no quantifier */ + else if (elem->min == 0 && elem->max == RPR_QUANTITY_INF) + appendStringInfoChar(buf, '*'); + else if (elem->min == 1 && elem->max == RPR_QUANTITY_INF) + appendStringInfoChar(buf, '+'); + else if (elem->min == 0 && elem->max == 1) + appendStringInfoChar(buf, '?'); + else if (elem->max == RPR_QUANTITY_INF) + appendStringInfo(buf, "{%d,}", elem->min); + else if (elem->min == elem->max) + appendStringInfo(buf, "{%d}", elem->min); + else + appendStringInfo(buf, "{%d,%d}", elem->min, elem->max); + + if (RPRElemIsReluctant(elem)) + appendStringInfoChar(buf, '?'); +} + +/* + * Deparse a compiled RPRPattern (bytecode) back to pattern string. + * Simple approach: output parens for each depth level as-is. + */ +static char * +deparse_rpr_pattern(RPRPattern *pattern) +{ + StringInfoData buf; + int i; + RPRDepth prevDepth = 0; + bool needSpace = false; + RPRElemIdx altSepAt = RPR_ELEMIDX_INVALID; + + if (pattern == NULL || pattern->numElements == 0) + return NULL; + + initStringInfo(&buf); + + for (i = 0; i < pattern->numElements; i++) + { + RPRPatternElement *elem = &pattern->elements[i]; + + if (RPRElemIsFin(elem)) + break; + + /* Alternation separator */ + if (altSepAt == i) + { + appendStringInfoString(&buf, " | "); + needSpace = false; + altSepAt = RPR_ELEMIDX_INVALID; + } + + if (RPRElemIsAlt(elem)) + { + /* Open parens up to ALT's content depth */ + while (prevDepth <= elem->depth) + { + if (needSpace) + appendStringInfoChar(&buf, ' '); + appendStringInfoChar(&buf, '('); + prevDepth++; + needSpace = false; + } + continue; + } + + if (RPRElemIsEnd(elem)) + { + /* Close down to END's depth, output quantifier */ + while (prevDepth > elem->depth + 1) + { + appendStringInfoChar(&buf, ')'); + prevDepth--; + } + appendStringInfoChar(&buf, ')'); + append_rpr_quantifier(&buf, elem); + prevDepth = elem->depth; + needSpace = true; + continue; + } + + if (RPRElemIsVar(elem)) + { + /* Open parens for depth increase */ + while (prevDepth < elem->depth) + { + if (needSpace) + appendStringInfoChar(&buf, ' '); + appendStringInfoChar(&buf, '('); + prevDepth++; + needSpace = false; + } + + /* Close parens for depth decrease */ + while (prevDepth > elem->depth) + { + appendStringInfoChar(&buf, ')'); + prevDepth--; + } + + if (needSpace) + appendStringInfoChar(&buf, ' '); + + appendStringInfoString(&buf, pattern->varNames[elem->varId]); + append_rpr_quantifier(&buf, elem); + needSpace = true; + + if (elem->jump != RPR_ELEMIDX_INVALID) + altSepAt = elem->jump; + } + } + + /* Close remaining */ + while (prevDepth > 0) + { + appendStringInfoChar(&buf, ')'); + prevDepth--; + } + + return buf.data; +} + /* * Show the window definition for a WindowAgg node. */ @@ -2947,6 +3078,18 @@ show_window_def(WindowAggState *planstate, List *ancestors, ExplainState *es) appendStringInfoChar(&wbuf, ')'); ExplainPropertyText("Window", wbuf.data, es); pfree(wbuf.data); + + /* Show Row Pattern Recognition pattern if present */ + if (wagg->rpPattern != NULL) + { + char *patternStr = deparse_rpr_pattern(wagg->rpPattern); + + if (patternStr != NULL) + { + ExplainPropertyText("Pattern", patternStr, es); + pfree(patternStr); + } + } } /* diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c index d9b64b0f465..5dea161c928 100644 --- a/src/backend/executor/nodeWindowAgg.c +++ b/src/backend/executor/nodeWindowAgg.c @@ -36,18 +36,23 @@ #include "access/htup_details.h" #include "catalog/objectaccess.h" #include "catalog/pg_aggregate.h" +#include "catalog/pg_collation_d.h" #include "catalog/pg_proc.h" #include "executor/executor.h" #include "executor/nodeWindowAgg.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" +#include "nodes/plannodes.h" #include "optimizer/clauses.h" #include "optimizer/optimizer.h" +#include "optimizer/rpr.h" #include "parser/parse_agg.h" #include "parser/parse_coerce.h" +#include "regex/regex.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/datum.h" +#include "utils/fmgroids.h" #include "utils/expandeddatum.h" #include "utils/lsyscache.h" #include "utils/memutils.h" @@ -170,6 +175,15 @@ typedef struct WindowStatePerAggData bool restart; /* need to restart this agg in this cycle? */ } WindowStatePerAggData; +/* + * Structure used by check_rpr_navigation() and rpr_navigation_walker(). + */ +typedef struct NavigationInfo +{ + bool is_prev; /* true if PREV */ + int num_vars; /* number of var nodes */ +} NavigationInfo; + static void initialize_windowaggregate(WindowAggState *winstate, WindowStatePerFunc perfuncstate, WindowStatePerAgg peraggstate); @@ -206,6 +220,9 @@ static Datum GetAggInitVal(Datum textInitVal, Oid transtype); static bool are_peers(WindowAggState *winstate, TupleTableSlot *slot1, TupleTableSlot *slot2); +static int WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot, + int relpos, int seektype, bool set_mark, + bool *isnull, bool *isout); static bool window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot); @@ -224,6 +241,42 @@ static uint8 get_notnull_info(WindowObject winobj, int64 pos, int argno); static void put_notnull_info(WindowObject winobj, int64 pos, int argno, bool isnull); +static void attno_map(Node *node); +static bool attno_map_walker(Node *node, void *context); +static int row_is_in_reduced_frame(WindowObject winobj, int64 pos); +static bool rpr_is_defined(WindowAggState *winstate); + +static void create_reduced_frame_map(WindowAggState *winstate); +static int get_reduced_frame_map(WindowAggState *winstate, int64 pos); +static void register_reduced_frame_map(WindowAggState *winstate, int64 pos, + int val); +static void clear_reduced_frame_map(WindowAggState *winstate); +static void update_reduced_frame(WindowObject winobj, int64 pos); + +static void check_rpr_navigation(Node *node, bool is_prev); +static bool rpr_navigation_walker(Node *node, void *context); + +/* NFA-based pattern matching functions */ +static RPRNFAState *nfa_state_alloc(WindowAggState *winstate); +static void nfa_state_free(WindowAggState *winstate, RPRNFAState *state); +static void nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list); +static RPRNFAState *nfa_state_clone(WindowAggState *winstate, int16 elemIdx, + int16 altPriority, int16 *counts); +static bool nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched); +static RPRNFAContext *nfa_context_alloc(WindowAggState *winstate); +static void nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx); +static void nfa_context_free(WindowAggState *winstate, RPRNFAContext *ctx); +static RPRNFAContext *nfa_start_context(WindowAggState *winstate, int64 startPos); +static void nfa_step(WindowAggState *winstate, RPRNFAContext *ctx, + bool *varMatched, int64 pos); +static void nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos); +static void nfa_process_context(WindowAggState *winstate, RPRNFAContext *ctx, + int64 currentPos, bool hasLimitedFrame, int64 frameOffset); +static void nfa_step_single(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, bool *varMatched, int64 currentPos); +static RPRNFAContext *nfa_find_context_for_pos(WindowAggState *winstate, int64 pos); +static void nfa_remove_contexts_up_to(WindowAggState *winstate, int64 endPos); +static void nfa_absorb_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx, int64 currentPos); /* * Not null info bit array consists of 2-bit items @@ -817,6 +870,7 @@ eval_windowaggregates(WindowAggState *winstate) * transition function, or * - we have an EXCLUSION clause, or * - if the new frame doesn't overlap the old one + * - if RPR is enabled * * Note that we don't strictly need to restart in the last case, but if * we're going to remove all rows from the aggregation anyway, a restart @@ -831,7 +885,8 @@ eval_windowaggregates(WindowAggState *winstate) (winstate->aggregatedbase != winstate->frameheadpos && !OidIsValid(peraggstate->invtransfn_oid)) || (winstate->frameOptions & FRAMEOPTION_EXCLUSION) || - winstate->aggregatedupto <= winstate->frameheadpos) + winstate->aggregatedupto <= winstate->frameheadpos || + rpr_is_defined(winstate)) { peraggstate->restart = true; numaggs_restart++; @@ -905,7 +960,22 @@ eval_windowaggregates(WindowAggState *winstate) * head, so that tuplestore can discard unnecessary rows. */ if (agg_winobj->markptr >= 0) - WinSetMarkPosition(agg_winobj, winstate->frameheadpos); + { + int64 markpos = winstate->frameheadpos; + + if (rpr_is_defined(winstate)) + { + /* + * If RPR is used, it is possible PREV wants to look at the + * previous row. So the mark pos should be frameheadpos - 1 + * unless it is below 0. + */ + markpos -= 1; + if (markpos < 0) + markpos = 0; + } + WinSetMarkPosition(agg_winobj, markpos); + } /* * Now restart the aggregates that require it. @@ -960,6 +1030,14 @@ eval_windowaggregates(WindowAggState *winstate) { winstate->aggregatedupto = winstate->frameheadpos; ExecClearTuple(agg_row_slot); + + /* + * If RPR is defined, we do not use aggregatedupto_nonrestarted. To + * avoid assertion failure below, we reset aggregatedupto_nonrestarted + * to frameheadpos. + */ + if (rpr_is_defined(winstate)) + aggregatedupto_nonrestarted = winstate->frameheadpos; } /* @@ -973,6 +1051,12 @@ eval_windowaggregates(WindowAggState *winstate) { int ret; +#ifdef RPR_DEBUG + printf("===== loop in frame starts: aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n", + winstate->aggregatedupto, + winstate->aggregatedbase); +#endif + /* Fetch next row if we didn't already */ if (TupIsNull(agg_row_slot)) { @@ -989,9 +1073,53 @@ eval_windowaggregates(WindowAggState *winstate) agg_row_slot, false); if (ret < 0) break; + if (ret == 0) goto next_tuple; + if (rpr_is_defined(winstate)) + { +#ifdef RPR_DEBUG + printf("reduced_frame_map: %d aggregatedupto: " INT64_FORMAT " aggregatedbase: " INT64_FORMAT "\n", + get_reduced_frame_map(winstate, + winstate->aggregatedupto), + winstate->aggregatedupto, + winstate->aggregatedbase); +#endif + + /* + * If the row status at currentpos is already decided and current + * row status is not decided yet, it means we passed the last + * reduced frame. Time to break the loop. + */ + if (get_reduced_frame_map(winstate, winstate->currentpos) + != RF_NOT_DETERMINED && + get_reduced_frame_map(winstate, winstate->aggregatedupto) + == RF_NOT_DETERMINED) + break; + + /* + * Otherwise we need to calculate the reduced frame. + */ + ret = row_is_in_reduced_frame(winstate->agg_winobj, + winstate->aggregatedupto); + if (ret == -1) /* unmatched row */ + break; + + /* + * Check if current row needs to be skipped due to no match. + */ + if (get_reduced_frame_map(winstate, + winstate->aggregatedupto) == RF_SKIPPED && + winstate->aggregatedupto == winstate->aggregatedbase) + { +#ifdef RPR_DEBUG + printf("skip current row for aggregation\n"); +#endif + break; + } + } + /* Set tuple context for evaluation of aggregate arguments */ winstate->tmpcontext->ecxt_outertuple = agg_row_slot; @@ -1020,6 +1148,7 @@ next_tuple: ExecClearTuple(agg_row_slot); } + /* The frame's end is not supposed to move backwards, ever */ Assert(aggregatedupto_nonrestarted <= winstate->aggregatedupto); @@ -1243,6 +1372,7 @@ begin_partition(WindowAggState *winstate) winstate->framehead_valid = false; winstate->frametail_valid = false; winstate->grouptail_valid = false; + create_reduced_frame_map(winstate); winstate->spooled_rows = 0; winstate->currentpos = 0; winstate->frameheadpos = 0; @@ -1464,6 +1594,13 @@ release_partition(WindowAggState *winstate) tuplestore_clear(winstate->buffer); winstate->partition_spooled = false; winstate->next_partition = true; + + /* Reset NFA state for new partition */ + winstate->nfaContext = NULL; + winstate->nfaContextTail = NULL; + winstate->nfaContextFree = NULL; + winstate->nfaStateFree = NULL; + winstate->nfaLastProcessedRow = -1; } /* @@ -2237,6 +2374,11 @@ ExecWindowAgg(PlanState *pstate) CHECK_FOR_INTERRUPTS(); +#ifdef RPR_DEBUG + printf("ExecWindowAgg called. pos: " INT64_FORMAT "\n", + winstate->currentpos); +#endif + if (winstate->status == WINDOWAGG_DONE) return NULL; @@ -2345,6 +2487,17 @@ ExecWindowAgg(PlanState *pstate) /* don't evaluate the window functions when we're in pass-through mode */ if (winstate->status == WINDOWAGG_RUN) { + /* + * If RPR is defined and skip mode is next row, we need to clear + * existing reduced frame info so that we newly calculate the info + * starting from current row. + */ + if (rpr_is_defined(winstate)) + { + if (winstate->rpSkipTo == ST_NEXT_ROW) + clear_reduced_frame_map(winstate); + } + /* * Evaluate true window functions */ @@ -2511,6 +2664,9 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) TupleDesc scanDesc; ListCell *l; + TargetEntry *te; + Expr *expr; + /* check for unsupported flags */ Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK))); @@ -2609,6 +2765,16 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) winstate->temp_slot_2 = ExecInitExtraTupleSlot(estate, scanDesc, &TTSOpsMinimalTuple); + winstate->prev_slot = ExecInitExtraTupleSlot(estate, scanDesc, + &TTSOpsMinimalTuple); + + winstate->next_slot = ExecInitExtraTupleSlot(estate, scanDesc, + &TTSOpsMinimalTuple); + + winstate->null_slot = ExecInitExtraTupleSlot(estate, scanDesc, + &TTSOpsMinimalTuple); + winstate->null_slot = ExecStoreAllNullTuple(winstate->null_slot); + /* * create frame head and tail slots only if needed (must create slots in * exactly the same cases that update_frameheadpos and update_frametailpos @@ -2795,6 +2961,66 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) winstate->inRangeAsc = node->inRangeAsc; winstate->inRangeNullsFirst = node->inRangeNullsFirst; + /* Set up SKIP TO type */ + winstate->rpSkipTo = node->rpSkipTo; + /* Set up row pattern recognition PATTERN clause (compiled NFA) */ + winstate->rpPattern = node->rpPattern; + + /* Calculate NFA state size for allocation */ + if (node->rpPattern != NULL) + { + winstate->nfaStateSize = offsetof(RPRNFAState, counts) + + sizeof(int16) * node->rpPattern->maxDepth; + } + + /* Set up row pattern recognition DEFINE clause */ + winstate->defineInitial = node->defineInitial; + winstate->defineVariableList = NIL; + winstate->defineClauseList = NIL; + if (node->defineClause != NIL) + { + /* + * Tweak arg var of PREV/NEXT so that it refers to scan/inner slot. + */ + foreach(l, node->defineClause) + { + char *name; + ExprState *exps; + + te = lfirst(l); + name = te->resname; + expr = te->expr; + +#ifdef RPR_DEBUG + printf("defineVariable name: %s\n", name); +#endif + winstate->defineVariableList = + lappend(winstate->defineVariableList, + makeString(pstrdup(name))); + attno_map((Node *) expr); + exps = ExecInitExpr(expr, (PlanState *) winstate); + winstate->defineClauseList = + lappend(winstate->defineClauseList, exps); + } + } + + /* Initialize NFA free lists for row pattern matching */ + winstate->nfaContext = NULL; + winstate->nfaContextTail = NULL; + winstate->nfaContextFree = NULL; + winstate->nfaStateFree = NULL; + winstate->nfaLastProcessedRow = -1; + + /* + * Allocate varMatched array for NFA evaluation. + * With the new varNames ordering (DEFINE order first), varId == defineIdx + * for all defined variables, so no mapping is needed. + */ + if (list_length(winstate->defineVariableList) > 0) + winstate->nfaVarMatched = palloc0(sizeof(bool) * + list_length(winstate->defineVariableList)); + else + winstate->nfaVarMatched = NULL; winstate->all_first = true; winstate->partition_spooled = false; winstate->more_partitions = false; @@ -2803,6 +3029,111 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags) return winstate; } +/* + * Rewrite varno of Var nodes that are the argument of PREV/NET so that they + * see scan tuple (PREV) or inner tuple (NEXT). Also we check the arguments + * of PREV/NEXT include at least 1 column reference. This is required by the + * SQL standard. + */ +static void +attno_map(Node *node) +{ + (void) expression_tree_walker(node, attno_map_walker, NULL); +} + +static bool +attno_map_walker(Node *node, void *context) +{ + FuncExpr *func; + int nargs; + bool is_prev; + + if (node == NULL) + return false; + + if (IsA(node, FuncExpr)) + { + func = (FuncExpr *) node; + + if (func->funcid == F_PREV || func->funcid == F_NEXT) + { + /* + * The SQL standard allows to have two more arguments form of + * PREV/NEXT. But currently we allow only 1 argument form. + */ + nargs = list_length(func->args); + if (list_length(func->args) != 1) + elog(ERROR, "PREV/NEXT must have 1 argument but function %d has %d args", + func->funcid, nargs); + + /* + * Check expr of PREV/NEXT aruguments and replace varno. + */ + is_prev = (func->funcid == F_PREV) ? true : false; + check_rpr_navigation(node, is_prev); + } + } + return expression_tree_walker(node, attno_map_walker, NULL); +} + +/* + * Rewrite varno of Var of RPR navigation operations (PREV/NEXT). + * If is_prev is true, we take care PREV, otherwise NEXT. + */ +static void +check_rpr_navigation(Node *node, bool is_prev) +{ + NavigationInfo context; + + context.is_prev = is_prev; + context.num_vars = 0; + (void) expression_tree_walker(node, rpr_navigation_walker, &context); + if (context.num_vars < 1) + ereport(ERROR, + errmsg("row pattern navigation operation's argument must include at least one column reference")); +} + +static bool +rpr_navigation_walker(Node *node, void *context) +{ + NavigationInfo *nav = (NavigationInfo *) context; + + if (node == NULL) + return false; + + switch (nodeTag(node)) + { + case T_Var: + { + Var *var = (Var *) node; + + nav->num_vars++; + + if (nav->is_prev) + { + /* + * Rewrite varno from OUTER_VAR to regular var no so that + * the var references scan tuple. + */ + var->varno = var->varnosyn; + } + else + var->varno = INNER_VAR; + } + break; + case T_Const: + case T_FuncExpr: + case T_OpExpr: + break; + + default: + ereport(ERROR, + errmsg("row pattern navigation operation's argument includes unsupported expression")); + } + return expression_tree_walker(node, rpr_navigation_walker, context); +} + + /* ----------------- * ExecEndWindowAgg * ----------------- @@ -2860,6 +3191,8 @@ ExecReScanWindowAgg(WindowAggState *node) ExecClearTuple(node->agg_row_slot); ExecClearTuple(node->temp_slot_1); ExecClearTuple(node->temp_slot_2); + ExecClearTuple(node->prev_slot); + ExecClearTuple(node->next_slot); if (node->framehead_slot) ExecClearTuple(node->framehead_slot); if (node->frametail_slot) @@ -3220,7 +3553,8 @@ window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot) return false; if (pos < winobj->markpos) - elog(ERROR, "cannot fetch row before WindowObject's mark position"); + elog(ERROR, "cannot fetch row: " INT64_FORMAT " before WindowObject's mark position: " INT64_FORMAT, + pos, winobj->markpos); oldcontext = MemoryContextSwitchTo(winstate->ss.ps.ps_ExprContext->ecxt_per_query_memory); @@ -3922,8 +4256,6 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, WindowAggState *winstate; ExprContext *econtext; TupleTableSlot *slot; - int64 abs_pos; - int64 mark_pos; Assert(WindowObjectIsValid(winobj)); winstate = winobj->winstate; @@ -3934,6 +4266,48 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, return ignorenulls_getfuncarginframe(winobj, argno, relpos, seektype, set_mark, isnull, isout); + if (WinGetSlotInFrame(winobj, slot, + relpos, seektype, set_mark, + isnull, isout) == 0) + { + econtext->ecxt_outertuple = slot; + return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno), + econtext, isnull); + } + + if (isout) + *isout = true; + *isnull = true; + return (Datum) 0; +} + +/* + * WinGetSlotInFrame + * slot: TupleTableSlot to store the result + * relpos: signed rowcount offset from the seek position + * seektype: WINDOW_SEEK_HEAD or WINDOW_SEEK_TAIL + * set_mark: If the row is found/in frame and set_mark is true, the mark is + * moved to the row as a side-effect. + * isnull: output argument, receives isnull status of result + * isout: output argument, set to indicate whether target row position + * is out of frame (can pass NULL if caller doesn't care about this) + * + * Returns 0 if we successfullt got the slot. false if out of frame. + * (also isout is set) + */ +static int +WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot, + int relpos, int seektype, bool set_mark, + bool *isnull, bool *isout) +{ + WindowAggState *winstate; + int64 abs_pos; + int64 mark_pos; + int num_reduced_frame; + + Assert(WindowObjectIsValid(winobj)); + winstate = winobj->winstate; + switch (seektype) { case WINDOW_SEEK_CURRENT: @@ -4000,11 +4374,25 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, winstate->frameOptions); break; } + num_reduced_frame = row_is_in_reduced_frame(winobj, + winstate->frameheadpos); + if (num_reduced_frame < 0) + goto out_of_frame; + else if (num_reduced_frame > 0) + if (relpos >= num_reduced_frame) + goto out_of_frame; break; case WINDOW_SEEK_TAIL: /* rejecting relpos > 0 is easy and simplifies code below */ if (relpos > 0) goto out_of_frame; + + /* + * RPR cares about frame head pos. Need to call + * update_frameheadpos + */ + update_frameheadpos(winstate); + update_frametailpos(winstate); abs_pos = winstate->frametailpos - 1 + relpos; @@ -4071,6 +4459,14 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, mark_pos = 0; /* keep compiler quiet */ break; } + + num_reduced_frame = row_is_in_reduced_frame(winobj, + winstate->frameheadpos + relpos); + if (num_reduced_frame < 0) + goto out_of_frame; + else if (num_reduced_frame > 0) + abs_pos = winstate->frameheadpos + relpos + + num_reduced_frame - 1; break; default: elog(ERROR, "unrecognized window seek type: %d", seektype); @@ -4089,15 +4485,13 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno, *isout = false; if (set_mark) WinSetMarkPosition(winobj, mark_pos); - econtext->ecxt_outertuple = slot; - return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno), - econtext, isnull); + return 0; out_of_frame: if (isout) *isout = true; *isnull = true; - return (Datum) 0; + return -1; } /* @@ -4128,3 +4522,1373 @@ WinGetFuncArgCurrent(WindowObject winobj, int argno, bool *isnull) return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno), econtext, isnull); } + +/* + * rpr_is_defined + * return true if Row pattern recognition is defined. + */ +static bool +rpr_is_defined(WindowAggState *winstate) +{ + return winstate->rpPattern != NULL; +} + +/* + * ----------------- + * row_is_in_reduced_frame + * Determine whether a row is in the current row's reduced window frame + * according to row pattern matching + * + * The row must has been already determined that it is in a full window frame + * and fetched it into slot. + * + * Returns: + * = 0, RPR is not defined. + * >0, if the row is the first in the reduced frame. Return the number of rows + * in the reduced frame. + * -1, if the row is unmatched row + * -2, if the row is in the reduced frame but needed to be skipped because of + * AFTER MATCH SKIP PAST LAST ROW + * ----------------- + */ +static int +row_is_in_reduced_frame(WindowObject winobj, int64 pos) +{ + WindowAggState *winstate = winobj->winstate; + int state; + int rtn; + + if (!rpr_is_defined(winstate)) + { + /* + * RPR is not defined. Assume that we are always in the the reduced + * window frame. + */ + rtn = 0; +#ifdef RPR_DEBUG + printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n", + rtn, pos); +#endif + return rtn; + } + + state = get_reduced_frame_map(winstate, pos); + + if (state == RF_NOT_DETERMINED) + { + update_frameheadpos(winstate); + update_reduced_frame(winobj, pos); + } + + state = get_reduced_frame_map(winstate, pos); + + switch (state) + { + int64 i; + int num_reduced_rows; + + case RF_FRAME_HEAD: + num_reduced_rows = 1; + for (i = pos + 1; + get_reduced_frame_map(winstate, i) == RF_SKIPPED; i++) + num_reduced_rows++; + rtn = num_reduced_rows; + break; + + case RF_SKIPPED: + rtn = -2; + break; + + case RF_UNMATCHED: + rtn = -1; + break; + + default: + elog(ERROR, "Unrecognized state: %d at: " INT64_FORMAT, + state, pos); + break; + } + +#ifdef RPR_DEBUG + printf("row_is_in_reduced_frame returns %d: pos: " INT64_FORMAT "\n", + rtn, pos); +#endif + return rtn; +} + +#define REDUCED_FRAME_MAP_INIT_SIZE 1024L + +/* + * create_reduced_frame_map + * Create reduced frame map + */ +static void +create_reduced_frame_map(WindowAggState *winstate) +{ + winstate->reduced_frame_map = + MemoryContextAlloc(winstate->partcontext, + REDUCED_FRAME_MAP_INIT_SIZE); + winstate->alloc_sz = REDUCED_FRAME_MAP_INIT_SIZE; + clear_reduced_frame_map(winstate); +} + +/* + * clear_reduced_frame_map + * Clear reduced frame map + */ +static void +clear_reduced_frame_map(WindowAggState *winstate) +{ + Assert(winstate->reduced_frame_map != NULL); + MemSet(winstate->reduced_frame_map, RF_NOT_DETERMINED, + winstate->alloc_sz); +} + +/* + * get_reduced_frame_map + * Get reduced frame map specified by pos + */ +static int +get_reduced_frame_map(WindowAggState *winstate, int64 pos) +{ + Assert(winstate->reduced_frame_map != NULL); + Assert(pos >= 0); + + /* + * If pos is not in the reduced frame map, it means that any info + * regarding the pos has not been registered yet. So we return + * RF_NOT_DETERMINED. + */ + if (pos >= winstate->alloc_sz) + return RF_NOT_DETERMINED; + + return winstate->reduced_frame_map[pos]; +} + +/* + * register_reduced_frame_map + * Add/replace reduced frame map member at pos. + * If there's no enough space, expand the map. + */ +static void +register_reduced_frame_map(WindowAggState *winstate, int64 pos, int val) +{ + int64 realloc_sz; + + Assert(winstate->reduced_frame_map != NULL); + + if (pos < 0) + elog(ERROR, "wrong pos: " INT64_FORMAT, pos); + + if (pos > winstate->alloc_sz - 1) + { + realloc_sz = winstate->alloc_sz * 2; + + winstate->reduced_frame_map = + repalloc(winstate->reduced_frame_map, realloc_sz); + + MemSet(winstate->reduced_frame_map + winstate->alloc_sz, + RF_NOT_DETERMINED, realloc_sz - winstate->alloc_sz); + + winstate->alloc_sz = realloc_sz; + } + + winstate->reduced_frame_map[pos] = val; +} + +/* + * update_reduced_frame + * Update reduced frame info using multi-context NFA pattern matching. + * + * Maintains multiple NFA contexts simultaneously, one for each potential + * match start position. This allows sharing row evaluations across contexts, + * avoiding redundant DEFINE clause evaluations when rewinding for SKIP TO + * NEXT ROW mode. + * + * Key optimizations: + * - Row evaluations (expensive DEFINE clauses) happen only once per row + * - All active contexts share the same evaluation results + * - Contexts persist across calls, enabling O(n) DEFINE evaluations + */ +static void +update_reduced_frame(WindowObject winobj, int64 pos) +{ + WindowAggState *winstate = winobj->winstate; + RPRNFAContext *targetCtx; + int64 currentPos; + int64 startPos; + int frameOptions = winstate->frameOptions; + bool hasLimitedFrame; + int64 frameOffset = 0; + + /* + * Check if we have a limited frame (ROWS ... N FOLLOWING). + * Each context needs its own frame end based on matchStartRow + offset. + */ + hasLimitedFrame = (frameOptions & FRAMEOPTION_ROWS) && + !(frameOptions & FRAMEOPTION_END_UNBOUNDED_FOLLOWING); + if (hasLimitedFrame && winstate->endOffsetValue != 0) + frameOffset = DatumGetInt64(winstate->endOffsetValue); + + /* + * Case 1: pos is before any existing context's start position. + * This means the position was already processed and determined unmatched. + * Head is the oldest context (lowest matchStartRow) since contexts are + * added at tail with increasing positions. + */ + if (winstate->nfaContext != NULL && + pos < winstate->nfaContext->matchStartRow) + { + register_reduced_frame_map(winstate, pos, RF_UNMATCHED); + return; + } + + /* + * Case 2: Find existing context for this pos, or create new one. + */ + targetCtx = nfa_find_context_for_pos(winstate, pos); + if (targetCtx == NULL) + { + /* + * No context exists. If pos is already processed, it means this row + * was already determined to be unmatched or skipped - no need to + * reprocess. + */ + if (pos <= winstate->nfaLastProcessedRow) + { + register_reduced_frame_map(winstate, pos, RF_UNMATCHED); + return; + } + /* Not yet processed - create new context and start fresh */ + targetCtx = nfa_start_context(winstate, pos); + } + else if (targetCtx->states == NULL) + { + /* Context already completed - skip to result registration */ + goto register_result; + } + + /* + * Determine where to start processing. + * Usually nfaLastProcessedRow+1 >= pos since contexts are created at + * currentPos+1 during processing. However, pos can exceed this when + * rows are skipped (e.g., unmatched rows don't update nfaLastProcessedRow). + */ + startPos = Max(pos, winstate->nfaLastProcessedRow + 1); + + /* + * Process rows until target context completes or we hit boundaries. + * Each row evaluation is shared across all active contexts. + */ + for (currentPos = startPos; targetCtx->states != NULL; currentPos++) + { + bool rowExists; + RPRNFAContext *ctx; + + /* Evaluate variables for this row - done only once, shared by all contexts */ + rowExists = nfa_evaluate_row(winobj, currentPos, winstate->nfaVarMatched); + + /* No more rows in partition? Finalize all contexts */ + if (!rowExists) + { + nfa_finalize_all_contexts(winstate, currentPos - 1); + /* Absorb completed contexts at partition boundary (SKIP PAST LAST ROW only) */ + if (winstate->rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame) + nfa_absorb_contexts(winstate, NULL, currentPos - 1); + break; + } + + /* Update last processed row */ + winstate->nfaLastProcessedRow = currentPos; + + /* Process each active context with this row's evaluation results */ + for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next) + nfa_process_context(winstate, ctx, currentPos, hasLimitedFrame, frameOffset); + + /* + * Create a new context for the next potential start position. + * This enables overlapping match detection for SKIP TO NEXT ROW. + */ + nfa_start_context(winstate, currentPos + 1); + + /* + * Absorb redundant contexts (SKIP PAST LAST ROW only, unbounded frame). + * At the same elementIndex, if newer context's count <= older context's count, + * the newer context can be absorbed (for unbounded quantifiers). + * With limited frame, different contexts have different frame boundaries, + * so absorption is not safe. + * Note: Never absorb targetCtx - it's the context we're trying to complete. + */ + if (winstate->rpSkipTo == ST_PAST_LAST_ROW && !hasLimitedFrame) + nfa_absorb_contexts(winstate, targetCtx, currentPos); + + /* Check if target context is now complete */ + if (targetCtx->states == NULL) + break; + } + +register_result: + Assert(pos == targetCtx->matchStartRow); + + /* + * Register reduced frame map based on match result. + */ + if (targetCtx->matchEndRow < targetCtx->matchStartRow) + { + register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_UNMATCHED); + } + else + { + register_reduced_frame_map(winstate, targetCtx->matchStartRow, RF_FRAME_HEAD); + for (int64 i = targetCtx->matchStartRow + 1; i <= targetCtx->matchEndRow; i++) + { + register_reduced_frame_map(winstate, i, RF_SKIPPED); + } + } + + /* + * Cleanup contexts based on SKIP mode. + */ + if (targetCtx->matchEndRow >= targetCtx->matchStartRow && + winstate->rpSkipTo == ST_PAST_LAST_ROW) + { + /* Remove all contexts with start <= matchEnd */ + nfa_remove_contexts_up_to(winstate, targetCtx->matchEndRow); + } + else + { + /* SKIP TO NEXT ROW or no match: just remove the target context */ + nfa_context_free(winstate, targetCtx); + } +} + +/* + * NFA-based pattern matching implementation + * + * These functions implement direct NFA execution using the compiled + * RPRPattern structure, avoiding regex compilation overhead. + */ + +/* + * nfa_state_alloc + * + * Allocate an NFA state, reusing from freeList if available. + * freeList is stored in WindowAggState for reuse across match attempts. + * Uses flexible array member for counts[]. + */ +static RPRNFAState * +nfa_state_alloc(WindowAggState *winstate) +{ + RPRNFAState *state; + + /* Try to reuse from free list first */ + if (winstate->nfaStateFree != NULL) + { + state = winstate->nfaStateFree; + winstate->nfaStateFree = state->next; + } + else + { + /* Allocate in partition context for proper lifetime */ + state = MemoryContextAlloc(winstate->partcontext, winstate->nfaStateSize); + } + + /* Initialize entire state to zero */ + memset(state, 0, winstate->nfaStateSize); + + return state; +} + +/* + * nfa_state_free + * + * Return a state to the free list for later reuse. + */ +static void +nfa_state_free(WindowAggState *winstate, RPRNFAState *state) +{ + state->next = winstate->nfaStateFree; + winstate->nfaStateFree = state; +} + +/* + * nfa_state_free_list + * + * Free all states in a list using pfree. + */ +static void +nfa_state_free_list(WindowAggState *winstate, RPRNFAState *list) +{ + RPRNFAState *state; + + while(list != NULL) + { + state = list; + list = list->next; + nfa_state_free(winstate, state); + } +} + +/* + * nfa_states_equal + * + * Check if two states are equivalent (same elemIdx and counts). + */ +static bool +nfa_states_equal(WindowAggState *winstate, RPRNFAState *s1, RPRNFAState *s2) +{ + int maxDepth = winstate->rpPattern->maxDepth; + + if (s1->elemIdx != s2->elemIdx) + return false; + + if (maxDepth > 0 && + memcmp(s1->counts, s2->counts, sizeof(int16) * maxDepth) != 0) + return false; + + return true; +} + +/* + * nfa_add_state_unique + * + * Add a state to ctx->states at the END, only if no duplicate exists. + * Returns true if state was added, false if duplicate found (state is freed). + * Earlier states have lower altPriority (lexical order), so existing wins. + */ +static bool +nfa_add_state_unique(WindowAggState *winstate, RPRNFAContext *ctx, RPRNFAState *state) +{ + RPRNFAState *s; + RPRNFAState *tail = NULL; + + /* Check for duplicate and find tail */ + for (s = ctx->states; s != NULL; s = s->next) + { + if (nfa_states_equal(winstate, s, state)) + { + /* Duplicate found - existing has better lexical order, discard new */ + nfa_state_free(winstate, state); + return false; + } + tail = s; + } + + /* No duplicate, add at end */ + state->next = NULL; + if (tail == NULL) + ctx->states = state; + else + tail->next = state; + + return true; +} + +/* + * nfa_state_clone + * + * Clone a state with given elemIdx, altPriority and counts. + * Caller is responsible for linking the returned state. + */ +static RPRNFAState * +nfa_state_clone(WindowAggState *winstate, int16 elemIdx, int16 altPriority, + int16 *counts) +{ + RPRPattern *pattern = winstate->rpPattern; + int maxDepth = pattern->maxDepth; + RPRNFAState *state = nfa_state_alloc(winstate); + + state->elemIdx = elemIdx; + state->altPriority = altPriority; + if (counts != NULL && maxDepth > 0) + memcpy(state->counts, counts, sizeof(int16) * maxDepth); + + return state; +} + +/* + * nfa_add_matched_state + * + * Record a matched state following SQL standard semantics. + * Lexical order (lower altPriority) wins first. Among same lexical order, + * longer match wins (greedy). + */ +static void +nfa_add_matched_state(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, int64 matchEndRow) +{ + bool shouldUpdate = false; + + if (ctx->matchedState == NULL) + shouldUpdate = true; + else if (state->altPriority < ctx->matchedState->altPriority) + shouldUpdate = true; /* Better lexical order wins */ + else if (state->altPriority == ctx->matchedState->altPriority && + matchEndRow > ctx->matchEndRow) + shouldUpdate = true; /* Same lexical order, longer wins */ + + if (shouldUpdate) + { + /* Free old matchedState if exists */ + if (ctx->matchedState != NULL) + nfa_state_free(winstate, ctx->matchedState); + + /* Take ownership of the new state */ + ctx->matchedState = state; + state->next = NULL; + ctx->matchEndRow = matchEndRow; + } + else + { + /* This state didn't win, free it */ + nfa_state_free(winstate, state); + } +} + +/* + * nfa_evaluate_row + * + * Evaluate all DEFINE variables for current row. + * Returns true if the row exists, false if out of partition. + * If row exists, fills varMatched array. + * varMatched[i] = true if variable i matched at current row. + */ +static bool +nfa_evaluate_row(WindowObject winobj, int64 pos, bool *varMatched) +{ + WindowAggState *winstate = winobj->winstate; + ExprContext *econtext = winstate->ss.ps.ps_ExprContext; + int numDefineVars = list_length(winstate->defineVariableList); + ListCell *lc; + int varIdx = 0; + TupleTableSlot *slot; + + /* + * Set up slots for current, previous, and next rows. + * We don't call get_slots() here to avoid recursion through + * row_is_in_frame -> update_reduced_frame -> nfa_match_pattern. + */ + + /* Current row -> ecxt_outertuple */ + slot = winstate->temp_slot_1; + if (!window_gettupleslot(winobj, pos, slot)) + return false; /* No row exists */ + econtext->ecxt_outertuple = slot; + + /* Previous row -> ecxt_scantuple (for PREV) */ + if (pos > 0) + { + slot = winstate->prev_slot; + if (!window_gettupleslot(winobj, pos - 1, slot)) + econtext->ecxt_scantuple = winstate->null_slot; + else + econtext->ecxt_scantuple = slot; + } + else + econtext->ecxt_scantuple = winstate->null_slot; + + /* Next row -> ecxt_innertuple (for NEXT) */ + slot = winstate->next_slot; + if (!window_gettupleslot(winobj, pos + 1, slot)) + econtext->ecxt_innertuple = winstate->null_slot; + else + econtext->ecxt_innertuple = slot; + + foreach(lc, winstate->defineClauseList) + { + ExprState *exprState = (ExprState *) lfirst(lc); + Datum result; + bool isnull; + + /* Evaluate DEFINE expression */ + result = ExecEvalExpr(exprState, econtext, &isnull); + + varMatched[varIdx] = (!isnull && DatumGetBool(result)); + + varIdx++; + if (varIdx >= numDefineVars) + break; + } + + return true; /* Row exists */ +} + +/* + * nfa_context_alloc + * + * Allocate an NFA context from free list or palloc. + */ +static RPRNFAContext * +nfa_context_alloc(WindowAggState *winstate) +{ + RPRNFAContext *ctx; + + if (winstate->nfaContextFree != NULL) + { + ctx = winstate->nfaContextFree; + winstate->nfaContextFree = ctx->next; + } + else + { + /* Allocate in partition context for proper lifetime */ + ctx = MemoryContextAlloc(winstate->partcontext, sizeof(RPRNFAContext)); + } + + ctx->next = NULL; + ctx->prev = NULL; + ctx->states = NULL; + ctx->matchStartRow = -1; + ctx->matchEndRow = -1; + ctx->matchedState = NULL; + + return ctx; +} + +/* + * nfa_unlink_context + * + * Remove a context from the doubly-linked active context list. + * Updates head (nfaContext) and tail (nfaContextTail) as needed. + */ +static void +nfa_unlink_context(WindowAggState *winstate, RPRNFAContext *ctx) +{ + if (ctx->prev != NULL) + ctx->prev->next = ctx->next; + else + winstate->nfaContext = ctx->next; /* was head */ + + if (ctx->next != NULL) + ctx->next->prev = ctx->prev; + else + winstate->nfaContextTail = ctx->prev; /* was tail */ + + ctx->next = NULL; + ctx->prev = NULL; +} + +/* + * nfa_context_free + * + * Unlink context from active list and return it to free list. + * Also frees any states in the context. + */ +static void +nfa_context_free(WindowAggState *winstate, RPRNFAContext *ctx) +{ + /* Unlink from active list first */ + nfa_unlink_context(winstate, ctx); + + if (ctx->states != NULL) + nfa_state_free_list(winstate, ctx->states); + if (ctx->matchedState != NULL) + nfa_state_free(winstate, ctx->matchedState); + + ctx->states = NULL; + ctx->matchedState = NULL; + ctx->next = winstate->nfaContextFree; + winstate->nfaContextFree = ctx; +} + +/* + * nfa_start_context + * + * Start a new match context at given position. + * Adds context to winstate->nfaContext list and returns the new context. + */ +static RPRNFAContext * +nfa_start_context(WindowAggState *winstate, int64 startPos) +{ + RPRNFAContext *ctx; + + ctx = nfa_context_alloc(winstate); + ctx->matchStartRow = startPos; + ctx->states = nfa_state_alloc(winstate); /* initial state at elem 0 */ + + /* Add to tail of active context list (doubly-linked, oldest-first) */ + ctx->prev = winstate->nfaContextTail; + ctx->next = NULL; + if (winstate->nfaContextTail != NULL) + winstate->nfaContextTail->next = ctx; + else + winstate->nfaContext = ctx; /* first context becomes head */ + winstate->nfaContextTail = ctx; + + return ctx; +} + +/* + * nfa_find_context_for_pos + * + * Find a context with the given start position. + * Returns NULL if not found. + */ +static RPRNFAContext * +nfa_find_context_for_pos(WindowAggState *winstate, int64 pos) +{ + RPRNFAContext *ctx; + + /* + * List is sorted by matchStartRow ascending (oldest/smallest at head). + * Stop early if we pass the target position. + */ + for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next) + { + if (ctx->matchStartRow == pos) + return ctx; + if (ctx->matchStartRow > pos) + break; /* won't find it, list is sorted */ + } + return NULL; +} + +/* + * nfa_remove_contexts_up_to + * + * Remove all contexts with matchStartRow <= endPos. + * Used by SKIP PAST LAST ROW to discard contexts within matched frame. + */ +static void +nfa_remove_contexts_up_to(WindowAggState *winstate, int64 endPos) +{ + RPRNFAContext *ctx; + RPRNFAContext *next; + + /* Contexts are sorted by matchStartRow ascending, so we can stop early */ + for (ctx = winstate->nfaContext; ctx != NULL; ctx = next) + { + next = ctx->next; + if (ctx->matchStartRow > endPos) + break; + nfa_context_free(winstate, ctx); + } +} + +/* + * nfa_absorb_contexts + * + * Absorb newer contexts into older ones when states are fully covered. + * For pattern like A+, if older context (started earlier) has count >= newer + * context's count at the same element, the newer context produces subset matches. + * + * Absorption condition: + * - For unbounded quantifiers (max=INT32_MAX): older.counts >= newer.counts + * - For bounded quantifiers: older.counts == newer.counts + * + * Note: List is oldest-first (head=oldest, tail=newest), so we start from + * tail (newest) and check if older contexts (via prev) can absorb it. + */ +static void +nfa_absorb_contexts(WindowAggState *winstate, RPRNFAContext *excludeCtx, int64 currentPos) +{ + RPRPattern *pattern = winstate->rpPattern; + RPRNFAContext *ctx; + int maxDepth; + + /* Need at least 2 contexts for absorption */ + if (winstate->nfaContext == NULL || winstate->nfaContext->next == NULL) + return; + + if (pattern == NULL) + return; + + /* + * Only absorb for patterns marked as absorbable during planning. + * See computeAbsorbability() in rpr.c for criteria. + */ + if (!pattern->isAbsorbable) + return; + + maxDepth = pattern->maxDepth; + + /* + * Context absorption: A later context (started at higher row) can be + * absorbed by an earlier context if ALL states in the later context + * are "covered" by states in the earlier context. + * + * A later state is covered by an earlier state if: + * 1. They are at the same element + * 2. For unbounded elements (max == INT32_MAX): earlier.counts[d] >= later.counts[d] + * for all depths d + * 3. For bounded elements: counts must be exactly equal at all depths + * + * List is oldest-first (head = lowest matchStartRow, tail = highest). + * We iterate from tail (newest) via prev and check if older contexts can absorb it. + */ + ctx = winstate->nfaContextTail; + + while (ctx != NULL) + { + RPRNFAContext *nextCtx = ctx->prev; /* traverse toward older (head) */ + RPRNFAContext *older; + bool absorbed = false; + + /* Never absorb the excluded context (it's the target being completed) */ + if (ctx == excludeCtx) + { + ctx = nextCtx; + continue; + } + + /* Skip contexts that haven't started processing yet (just created for future row) */ + if (ctx->matchStartRow > currentPos) + { + ctx = nextCtx; + continue; + } + + /* + * Handle completed contexts (states == NULL) separately. + * A completed context can be absorbed by an older completed context + * if the older one has the same or later matchEndRow. + */ + if (ctx->states == NULL) + { + /* Only completed contexts with valid matchEndRow can be absorbed */ + if (ctx->matchEndRow < 0) + { + ctx = nextCtx; + continue; + } + + /* Check if any older completed context can absorb this one */ + for (older = ctx->prev; older != NULL && !absorbed; older = older->prev) + { + /* Must have started earlier */ + if (older->matchStartRow >= ctx->matchStartRow) + continue; + + /* Older must also be completed with valid matchEndRow */ + if (older->states != NULL || older->matchEndRow < 0) + continue; + + /* + * Older context absorbs newer if it has the same or later + * matchEndRow, meaning all matches from newer are subsets. + */ + if (older->matchEndRow >= ctx->matchEndRow) + { + /* Absorb: remove ctx (newer) */ + nfa_context_free(winstate, ctx); + absorbed = true; + } + } + + ctx = nextCtx; + continue; + } + + /* + * Check if all states in ctx are on absorbable branches. + * If any state is on a non-absorbable branch, skip this context. + */ + { + RPRNFAState *s; + bool allAbsorbable = true; + + for (s = ctx->states; s != NULL && allAbsorbable; s = s->next) + { + RPRPatternElement *branchFirst; + + /* altPriority is the branch's first element index */ + if (s->altPriority < 0 || s->altPriority >= pattern->numElements) + continue; + + branchFirst = &pattern->elements[s->altPriority]; + if (!(branchFirst->flags & RPR_ELEM_ABSORBABLE)) + allAbsorbable = false; + } + + if (!allAbsorbable) + { + ctx = nextCtx; + continue; + } + } + + /* + * Check if any older context can absorb this one. + * Older contexts have lower matchStartRow (toward head via prev). + */ + for (older = ctx->prev; older != NULL && !absorbed; older = older->prev) + { + RPRNFAState *laterState; + RPRNFAState *earlierState; + bool canAbsorb; + int laterCount = 0; + int earlierCount = 0; + + /* Skip if not started earlier */ + if (older->matchStartRow >= ctx->matchStartRow) + continue; + + /* Skip contexts that haven't started processing yet */ + if (older->matchStartRow > currentPos) + continue; + + /* + * For in-progress ctx, older must also be in-progress. + * (Completed older contexts are handled above for completed ctx.) + */ + if (older->states == NULL) + continue; + + /* Count states in both contexts */ + for (laterState = ctx->states; laterState != NULL; laterState = laterState->next) + laterCount++; + for (earlierState = older->states; earlierState != NULL; earlierState = earlierState->next) + earlierCount++; + + /* Must have same number of states (same structure) */ + if (laterCount != earlierCount) + continue; + + /* + * Check if ALL states in ctx (later) are covered by states in older. + * Both must have the same set of element indices. + */ + canAbsorb = true; + for (laterState = ctx->states; laterState != NULL && canAbsorb; laterState = laterState->next) + { + bool found = false; + + for (earlierState = older->states; earlierState != NULL && !found; earlierState = earlierState->next) + { + RPRPatternElement *elem; + bool countsMatch; + int d; + + /* Must be at same element */ + if (earlierState->elemIdx != laterState->elemIdx) + continue; + + /* Must be on same branch (same altPriority) */ + if (earlierState->altPriority != laterState->altPriority) + continue; + + /* Handle invalid element index (terminal state) */ + if (laterState->elemIdx < 0 || laterState->elemIdx >= pattern->numElements) + { + found = true; + break; + } + + elem = &pattern->elements[laterState->elemIdx]; + countsMatch = true; + + if (elem->max == INT32_MAX) + { + /* Unbounded: earlier.count >= later.count at all depths */ + for (d = 0; d <= maxDepth && countsMatch; d++) + { + if (earlierState->counts[d] < laterState->counts[d]) + countsMatch = false; + } + } + else + { + /* Bounded: counts must be exactly equal at all depths */ + for (d = 0; d <= maxDepth && countsMatch; d++) + { + if (earlierState->counts[d] != laterState->counts[d]) + countsMatch = false; + } + } + + if (countsMatch) + found = true; + } + + if (!found) + canAbsorb = false; + } + + if (canAbsorb) + { + /* Absorb: remove ctx (newer) */ + nfa_context_free(winstate, ctx); + absorbed = true; + } + } + + ctx = nextCtx; + } +} + +/* + * nfa_step + * + * Process all states in context through NFA for one row. + * Calls nfa_step_single for each state. + */ +static void +nfa_step(WindowAggState *winstate, RPRNFAContext *ctx, bool *varMatched, int64 pos) +{ + RPRNFAState *states = ctx->states; + RPRNFAState *state; + RPRNFAState *nextState; + + ctx->states = NULL; + + for (state = states; state != NULL; state = nextState) + { + nextState = state->next; + state->next = NULL; + nfa_step_single(winstate, ctx, state, varMatched, pos); + } +} + +/* + * nfa_finalize_all_contexts + * + * Finalize all active contexts when partition ends. + * Calls nfa_step with NULL varMatched to complete without new row data. + */ +static void +nfa_finalize_all_contexts(WindowAggState *winstate, int64 lastPos) +{ + RPRNFAContext *ctx; + + for (ctx = winstate->nfaContext; ctx != NULL; ctx = ctx->next) + { + if (ctx->states != NULL) + nfa_step(winstate, ctx, NULL, lastPos); + } +} + +/* + * nfa_process_context + * + * Process one context for the current row. + * Handles frame boundary check and NFA step. + */ +static void +nfa_process_context(WindowAggState *winstate, RPRNFAContext *ctx, + int64 currentPos, bool hasLimitedFrame, int64 frameOffset) +{ + /* Skip already-completed contexts */ + if (ctx->states == NULL) + return; + + /* Check frame boundary */ + if (hasLimitedFrame) + { + int64 ctxFrameEnd = ctx->matchStartRow + frameOffset + 1; + if (currentPos >= ctxFrameEnd) + { + nfa_step(winstate, ctx, NULL, ctxFrameEnd - 1); + return; + } + } + + /* Process states for this context */ + nfa_step(winstate, ctx, winstate->nfaVarMatched, currentPos); +} + +/* + * nfa_step_single + * + * Process one state through NFA for one row. + * New states are added to ctx->states. + * Match (FIN) is recorded in ctx->matchedState. + * When FIN is reached by matching (not skipping), matchEndRow is updated. + */ +static void +nfa_step_single(WindowAggState *winstate, RPRNFAContext *ctx, + RPRNFAState *state, bool *varMatched, int64 currentPos) +{ + RPRPattern *pattern = winstate->rpPattern; + RPRPatternElement *elements = pattern->elements; + RPRNFAState *pending = state; /* states to process in current row */ + RPRNFAState *pending_tail = state; /* tail for FIFO append */ + + while (pending != NULL) + { + RPRPatternElement *elem; + bool matched; + int16 count; + int depth; + + /* Pop from head */ + state = pending; + pending = pending->next; + if (pending == NULL) + pending_tail = NULL; + state->next = NULL; + + Assert(state->elemIdx >= 0 && state->elemIdx < pattern->numElements); + elem = &elements[state->elemIdx]; + depth = elem->depth; + + if (RPRElemIsVar(elem)) + { + /* + * Variable: check if it matches current row. + * If varMatched is NULL (boundary), all variables are forced to false. + * Otherwise, varId < numDefines uses DEFINE expr, + * varId >= numDefines defaults to TRUE. + */ + if (varMatched == NULL) + matched = false; + else + { + int numDefines = list_length(winstate->defineVariableList); + + if (elem->varId >= numDefines) + matched = true; /* Not defined in DEFINE, defaults to TRUE */ + else + matched = varMatched[elem->varId]; + } + + count = state->counts[depth]; + + if (matched) + { + count++; + + if (elem->max != RPR_QUANTITY_INF && count > elem->max) + { + nfa_state_free(winstate, state); + continue; + } + + state->counts[depth] = count; + + /* Can repeat more? Clone for staying */ + if (elem->max == RPR_QUANTITY_INF || count < elem->max) + { + RPRNFAState *clone = nfa_state_clone(winstate, state->elemIdx, + state->altPriority, + state->counts); + nfa_add_state_unique(winstate, ctx, clone); + } + + /* Satisfied? Advance to next element */ + if (count >= elem->min) + { + RPRPatternElement *nextElem; + + state->counts[depth] = 0; + state->elemIdx = elem->next; + nextElem = &elements[state->elemIdx]; + + if (RPRElemIsFin(nextElem)) + { + /* Match ends at current row since we matched */ + nfa_add_matched_state(winstate, ctx, state, currentPos); + } + else if (RPRElemIsEnd(nextElem)) + { + /* + * END is epsilon transition - process immediately on same row. + * This ensures match end position is recorded at the row where + * the last VAR matched, not the next row. + */ + state->next = NULL; + if (pending_tail) + pending_tail->next = state; + else + pending = state; + pending_tail = state; + } + else + { + /* + * VAR, ALT - wait for next row. ALT dispatches to VARs that + * need input, so it must wait for the next row after VAR + * consumed the current row. + */ + nfa_add_state_unique(winstate, ctx, state); + } + } + else + { + nfa_state_free(winstate, state); + } + } + else + { + /* Not matched: can we skip? */ + if (count >= elem->min) + { + RPRPatternElement *nextElem; + + state->counts[depth] = 0; + state->elemIdx = elem->next; + nextElem = &elements[state->elemIdx]; + + if (RPRElemIsFin(nextElem)) + { + /* Match ends at previous row since current didn't match */ + nfa_add_matched_state(winstate, ctx, state, currentPos - 1); + } + else if (RPRElemIsVar(nextElem)) + { + /* + * Current row was NOT consumed (skip case), so next VAR + * must be tried on the SAME row via pending list + */ + state->next = NULL; + if (pending_tail) + pending_tail->next = state; + else + pending = state; + pending_tail = state; + } + else + { + state->next = NULL; + if (pending_tail) + pending_tail->next = state; + else + pending = state; + pending_tail = state; + } + } + else + { + nfa_state_free(winstate, state); + } + } + } + else if (RPRElemIsFin(elem)) + { + /* Already at FIN - match ends at current row */ + nfa_add_matched_state(winstate, ctx, state, currentPos); + } + else if (RPRElemIsAlt(elem)) + { + RPRElemIdx altIdx = elem->next; + bool first = true; + + /* + * ALT doesn't consume a row - it's just a dispatch point. + * All branches should be evaluated on the CURRENT row. + * Set altPriority to branch's elemIdx for lexical order tracking. + * Append to pending_tail to maintain lexical order. + */ + while (altIdx >= 0 && altIdx < pattern->numElements) + { + RPRPatternElement *altElem = &elements[altIdx]; + RPRNFAState *newState; + + if (first) + { + state->elemIdx = altIdx; + state->altPriority = altIdx; + newState = state; + first = false; + } + else + { + newState = nfa_state_clone(winstate, altIdx, altIdx, + state->counts); + } + + /* Append to tail for lexical order */ + newState->next = NULL; + if (pending_tail) + pending_tail->next = newState; + else + pending = newState; + pending_tail = newState; + + altIdx = altElem->jump; + } + + if (first) + nfa_state_free(winstate, state); + } + else if (RPRElemIsEnd(elem)) + { + count = state->counts[depth] + 1; + + if (count < elem->min) + { + /* + * Haven't reached minimum yet - must loop back. + * Add to ctx->states (next row) not pending (same row). + */ + state->counts[depth] = count; + for (int d = depth + 1; d < pattern->maxDepth; d++) + state->counts[d] = 0; + state->elemIdx = elem->jump; + nfa_add_state_unique(winstate, ctx, state); + } + else if (elem->max != RPR_QUANTITY_INF && count >= elem->max) + { + /* Reached maximum - must exit, continue processing */ + state->counts[depth] = 0; + state->elemIdx = elem->next; + state->next = NULL; + if (pending_tail) + pending_tail->next = state; + else + pending = state; + pending_tail = state; + } + else + { + /* + * Between min and max - can exit or continue. + * Exit state handling depends on what comes next: + * - If next is FIN: process on same row (match ends here) + * - If next is VAR/ALT: wait for next row (needs new input) + * Loop state always waits for next row. + */ + RPRPatternElement *nextElem = &elements[elem->next]; + RPRElemIdx exitAltPriority; + RPRNFAState *exitState; + + /* + * For greedy extension: if context already has a match recorded, + * preserve its altPriority. This ensures that iterations of a + * quantified group don't compete on lexical order - the first + * iteration's choice sets the preference, and subsequent + * iterations can extend it if they produce a longer match. + */ + exitAltPriority = state->altPriority; + if (ctx->matchedState != NULL) + exitAltPriority = ctx->matchedState->altPriority; + + exitState = nfa_state_clone(winstate, elem->next, + exitAltPriority, + state->counts); + exitState->counts[depth] = 0; + + if (RPRElemIsFin(nextElem)) + { + /* Match ends at current row - append to pending */ + exitState->next = NULL; + if (pending_tail) + pending_tail->next = exitState; + else + pending = exitState; + pending_tail = exitState; + } + else + { + /* Next element (VAR/ALT) needs new row input */ + nfa_add_state_unique(winstate, ctx, exitState); + } + + state->counts[depth] = count; + for (int d = depth + 1; d < pattern->maxDepth; d++) + state->counts[d] = 0; + state->elemIdx = elem->jump; + nfa_add_state_unique(winstate, ctx, state); + } + } + else + { + state->elemIdx = elem->next; + state->next = NULL; + if (pending_tail) + pending_tail->next = state; + else + pending = state; + pending_tail = state; + } + } +} + diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c index 78b7f05aba2..efb60c99052 100644 --- a/src/backend/utils/adt/windowfuncs.c +++ b/src/backend/utils/adt/windowfuncs.c @@ -41,7 +41,6 @@ static bool rank_up(WindowObject winobj); static Datum leadlag_common(FunctionCallInfo fcinfo, bool forward, bool withoffset, bool withdefault); - /* * utility routine for *_rank functions. */ @@ -683,7 +682,7 @@ window_last_value(PG_FUNCTION_ARGS) WinCheckAndInitializeNullTreatment(winobj, true, fcinfo); result = WinGetFuncArgInFrame(winobj, 0, - 0, WINDOW_SEEK_TAIL, true, + 0, WINDOW_SEEK_TAIL, false, &isnull, NULL); if (isnull) PG_RETURN_NULL(); @@ -724,3 +723,25 @@ window_nth_value(PG_FUNCTION_ARGS) PG_RETURN_DATUM(result); } + +/* + * prev + * Dummy function to invoke RPR's navigation operator "PREV". + * This is *not* a window function. + */ +Datum +window_prev(PG_FUNCTION_ARGS) +{ + PG_RETURN_DATUM(PG_GETARG_DATUM(0)); +} + +/* + * next + * Dummy function to invoke RPR's navigation operation "NEXT". + * This is *not* a window function. + */ +Datum +window_next(PG_FUNCTION_ARGS) +{ + PG_RETURN_DATUM(PG_GETARG_DATUM(0)); +} diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 894b6a1b6d6..7736a2c3360 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -10853,6 +10853,12 @@ { oid => '3114', descr => 'fetch the Nth row value', proname => 'nth_value', prokind => 'w', prorettype => 'anyelement', proargtypes => 'anyelement int4', prosrc => 'window_nth_value' }, +{ oid => '8126', descr => 'previous value', + proname => 'prev', provolatile => 's', prorettype => 'anyelement', + proargtypes => 'anyelement', prosrc => 'window_prev' }, +{ oid => '8127', descr => 'next value', + proname => 'next', provolatile => 's', prorettype => 'anyelement', + proargtypes => 'anyelement', prosrc => 'window_next' }, # functions for range types { oid => '3832', descr => 'I/O', diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index f8053d9e572..21ce39d0797 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -41,6 +41,7 @@ #include "nodes/plannodes.h" #include "nodes/tidbitmap.h" #include "partitioning/partdefs.h" +#include "regex/regex.h" #include "storage/condition_variable.h" #include "utils/hsearch.h" #include "utils/queryenvironment.h" @@ -2512,6 +2513,39 @@ typedef enum WindowAggStatus * tuples during spool */ } WindowAggStatus; +#define RF_NOT_DETERMINED 0 +#define RF_FRAME_HEAD 1 +#define RF_SKIPPED 2 +#define RF_UNMATCHED 3 + +/* + * RPRNFAState - single NFA state for pattern matching + * + * counts[] tracks repetition counts at each nesting depth. + * altPriority tracks lexical order for alternation (lower = earlier alternative). + */ +typedef struct RPRNFAState +{ + struct RPRNFAState *next; /* next state in linked list */ + int16 elemIdx; /* current pattern element index */ + int16 altPriority; /* lexical order priority (lower = preferred) */ + int16 counts[FLEXIBLE_ARRAY_MEMBER]; /* repetition counts by depth */ +} RPRNFAState; + +/* + * RPRNFAContext - context for NFA pattern matching execution + */ +typedef struct RPRNFAContext +{ + struct RPRNFAContext *next; /* next context in linked list */ + struct RPRNFAContext *prev; /* previous context (for reverse traversal) */ + RPRNFAState *states; /* active states (linked list) */ + + int64 matchStartRow; /* row where match started */ + int64 matchEndRow; /* row where match ended (-1 = no match) */ + RPRNFAState *matchedState; /* FIN state for greedy fallback (cloned) */ +} RPRNFAContext; + typedef struct WindowAggState { ScanState ss; /* its first field is NodeTag */ @@ -2571,6 +2605,23 @@ typedef struct WindowAggState int64 groupheadpos; /* current row's peer group head position */ int64 grouptailpos; /* " " " " tail position (group end+1) */ + /* these fields are used in Row pattern recognition: */ + RPSkipTo rpSkipTo; /* Row Pattern Skip To type */ + struct RPRPattern *rpPattern; /* compiled pattern for NFA execution */ + List *defineVariableList; /* list of row pattern definition + * variables (list of String) */ + List *defineClauseList; /* expression for row pattern definition + * search conditions ExprState list */ + List *defineInitial; /* list of row pattern definition variable + * initials (list of String) */ + RPRNFAContext *nfaContext; /* active matching contexts (head) */ + RPRNFAContext *nfaContextTail; /* tail of active contexts (for reverse traversal) */ + RPRNFAContext *nfaContextFree; /* recycled NFA context nodes */ + RPRNFAState *nfaStateFree; /* recycled NFA state nodes */ + Size nfaStateSize; /* pre-calculated RPRNFAState size */ + bool *nfaVarMatched; /* per-row cache: varMatched[varId] for varId < numDefines */ + int64 nfaLastProcessedRow; /* last row processed by NFA (-1 = none) */ + MemoryContext partcontext; /* context for partition-lifespan data */ MemoryContext aggcontext; /* shared context for aggregate working data */ MemoryContext curaggcontext; /* current aggregate's working data */ @@ -2598,6 +2649,18 @@ typedef struct WindowAggState TupleTableSlot *agg_row_slot; TupleTableSlot *temp_slot_1; TupleTableSlot *temp_slot_2; + + /* temporary slots for RPR */ + TupleTableSlot *prev_slot; /* PREV row navigation operator */ + TupleTableSlot *next_slot; /* NEXT row navigation operator */ + TupleTableSlot *null_slot; /* all NULL slot */ + + /* + * Each byte corresponds to a row positioned at absolute its pos in + * partition. See above definition for RF_*. Used for RPR. + */ + char *reduced_frame_map; + int64 alloc_sz; /* size of the map */ } WindowAggState; /* ---------------- -- 2.43.0 [application/octet-stream] v39-0006-Row-pattern-recognition-patch-docs.patch (11.1K, ../../[email protected]/7-v39-0006-Row-pattern-recognition-patch-docs.patch) download | inline diff: From 4136a1d1f865d5b84ea6b21fe34c0af252d0bf3c Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii <[email protected]> Date: Fri, 16 Jan 2026 13:11:21 +0900 Subject: [PATCH v39 6/8] Row pattern recognition patch (docs). --- doc/src/sgml/advanced.sgml | 90 ++++++++++++++++++++++++++++-- doc/src/sgml/func/func-window.sgml | 53 ++++++++++++++++++ doc/src/sgml/ref/select.sgml | 56 ++++++++++++++++++- 3 files changed, 192 insertions(+), 7 deletions(-) diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml index 451bcb202ec..eec2a0a9346 100644 --- a/doc/src/sgml/advanced.sgml +++ b/doc/src/sgml/advanced.sgml @@ -540,13 +540,95 @@ WHERE pos < 3; two rows for each department). </para> + <para> + Row pattern common syntax can be used to perform row pattern recognition + in a query. The row pattern common syntax includes two sub + clauses: <literal>DEFINE</literal> + and <literal>PATTERN</literal>. <literal>DEFINE</literal> defines + definition variables along with an expression. The expression must be a + logical expression, which means it must + return <literal>TRUE</literal>, <literal>FALSE</literal> + or <literal>NULL</literal>. The expression may comprise column references + and functions. Window functions, aggregate functions and subqueries are + not allowed. An example of <literal>DEFINE</literal> is as follows. + +<programlisting> +DEFINE + LOWPRICE AS price <= 100, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +</programlisting> + + Note that <function>PREV</function> returns the <literal>price</literal> + column in the previous row if it's called in a context of row pattern + recognition. Thus in the second line the definition variable "UP" + is <literal>TRUE</literal> when the price column in the current row is + greater than the price column in the previous row. Likewise, "DOWN" + is <literal>TRUE</literal> when the + <literal>price</literal> column in the current row is lower than + the <literal>price</literal> column in the previous row. + </para> + <para> + Once <literal>DEFINE</literal> exists, <literal>PATTERN</literal> can be + used. <literal>PATTERN</literal> defines a sequence of rows that satisfies + conditions defined in the <literal>DEFINE</literal> clause. For example + following <literal>PATTERN</literal> defines a sequence of rows starting + with the a row satisfying "LOWPRICE", then one or more rows satisfying + "UP" and finally one or more rows satisfying "DOWN". Note that "+" means + one or more matches. Also you can use "*", which means zero or more + matches. If a sequence of rows which satisfies the PATTERN is found, in + the starting row all columns or functions are shown in the target + list. Note that aggregations only look into the matched rows, rather than + the whole frame. On the second or subsequent rows all window functions are + shown as NULL. Aggregates are NULL or 0 depending on its aggregation + definition. A count() aggregate shows 0. For rows that do not match on the + PATTERN, columns are shown AS NULL too. Example of + a <literal>SELECT</literal> using the <literal>DEFINE</literal> + and <literal>PATTERN</literal> clause is as follows. + +<programlisting> +SELECT company, tdate, price, + first_value(price) OVER w, + max(price) OVER w, + count(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 (LOWPRICE UP+ DOWN+) + DEFINE + LOWPRICE AS price <= 100, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); +</programlisting> +<screen> + company | tdate | price | first_value | max | count +----------+------------+-------+-------------+-----+------- + company1 | 2023-07-01 | 100 | 100 | 200 | 4 + company1 | 2023-07-02 | 200 | | | 0 + company1 | 2023-07-03 | 150 | | | 0 + company1 | 2023-07-04 | 140 | | | 0 + company1 | 2023-07-05 | 150 | | | 0 + company1 | 2023-07-06 | 90 | 90 | 130 | 4 + company1 | 2023-07-07 | 110 | | | 0 + company1 | 2023-07-08 | 130 | | | 0 + company1 | 2023-07-09 | 120 | | | 0 + company1 | 2023-07-10 | 130 | | | 0 +(10 rows) +</screen> + </para> + <para> When a query involves multiple window functions, it is possible to write out each one with a separate <literal>OVER</literal> clause, but this is - duplicative and error-prone if the same windowing behavior is wanted - for several functions. Instead, each windowing behavior can be named - in a <literal>WINDOW</literal> clause and then referenced in <literal>OVER</literal>. - For example: + duplicative and error-prone if the same windowing behavior is wanted for + several functions. Instead, each windowing behavior can be named in + a <literal>WINDOW</literal> clause and then referenced + in <literal>OVER</literal>. For example: <programlisting> SELECT sum(salary) OVER w, avg(salary) OVER w diff --git a/doc/src/sgml/func/func-window.sgml b/doc/src/sgml/func/func-window.sgml index bcf755c9ebc..ae36e0f3135 100644 --- a/doc/src/sgml/func/func-window.sgml +++ b/doc/src/sgml/func/func-window.sgml @@ -278,6 +278,59 @@ <function>nth_value</function>. </para> + <para> + Row pattern recognition navigation functions are listed in + <xref linkend="functions-rpr-navigation-table"/>. These functions + can be used to describe DEFINE clause of Row pattern recognition. + </para> + + <table id="functions-rpr-navigation-table"> + <title>Row Pattern Navigation Functions</title> + <tgroup cols="1"> + <thead> + <row> + <entry role="func_table_entry"><para role="func_signature"> + Function + </para> + <para> + Description + </para></entry> + </row> + </thead> + + <tbody> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>prev</primary> + </indexterm> + <function>prev</function> ( <parameter>value</parameter> <type>anyelement</type> ) + <returnvalue>anyelement</returnvalue> + </para> + <para> + Returns the column value at the previous row; + returns NULL if there is no previous row in the window frame. + </para></entry> + </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>next</primary> + </indexterm> + <function>next</function> ( <parameter>value</parameter> <type>anyelement</type> ) + <returnvalue>anyelement</returnvalue> + </para> + <para> + Returns the column value at the next row; + returns NULL if there is no next row in the window frame. + </para></entry> + </row> + + </tbody> + </tgroup> + </table> + <note> <para> The SQL standard defines a <literal>FROM FIRST</literal> or <literal>FROM LAST</literal> diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml index ca5dd14d627..49b3c00d9f2 100644 --- a/doc/src/sgml/ref/select.sgml +++ b/doc/src/sgml/ref/select.sgml @@ -979,8 +979,8 @@ WINDOW <replaceable class="parameter">window_name</replaceable> AS ( <replaceabl The <replaceable class="parameter">frame_clause</replaceable> can be one of <synopsis> -{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable>frame_exclusion</replaceable> ] -{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ] +{ RANGE | ROWS | GROUPS } <replaceable>frame_start</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [row_pattern_common_syntax] +{ RANGE | ROWS | GROUPS } BETWEEN <replaceable>frame_start</replaceable> AND <replaceable>frame_end</replaceable> [ <replaceable>frame_exclusion</replaceable> ] [row_pattern_common_syntax] </synopsis> where <replaceable>frame_start</replaceable> @@ -1087,9 +1087,59 @@ EXCLUDE NO OTHERS a given peer group will be in the frame or excluded from it. </para> + <para> + The + optional <replaceable class="parameter">row_pattern_common_syntax</replaceable> + defines the <firstterm>row pattern recognition condition</firstterm> for + this + window. <replaceable class="parameter">row_pattern_common_syntax</replaceable> + includes following subclauses. + +<synopsis> +[ { AFTER MATCH SKIP PAST LAST ROW | AFTER MATCH SKIP TO NEXT ROW } ] +[ INITIAL | SEEK ] +PATTERN ( <replaceable class="parameter">pattern_variable_name</replaceable>[*|+|?] [, ...] ) +DEFINE <replaceable class="parameter">definition_varible_name</replaceable> AS <replaceable class="parameter">expression</replaceable> [, ...] +</synopsis> + <literal>AFTER MATCH SKIP PAST LAST ROW</literal> or <literal>AFTER MATCH + SKIP TO NEXT ROW</literal> controls how to proceed to next row position + after a match found. With <literal>AFTER MATCH SKIP PAST LAST + ROW</literal> (the default) next row position is next to the last row of + previous match. On the other hand, with <literal>AFTER MATCH SKIP TO NEXT + ROW</literal> next row position is always next to the last row of previous + match. <literal>INITIAL</literal> or <literal>SEEK</literal> defines how a + successful pattern matching starts from which row in a + frame. If <literal>INITIAL</literal> is specified, the match must start + from the first row in the frame. If <literal>SEEK</literal> is specified, + the set of matching rows do not necessarily start from the first row. The + default is <literal>INITIAL</literal>. Currently + only <literal>INITIAL</literal> is supported. <literal>DEFINE</literal> + defines definition variables along with a boolean + expression. <literal>PATTERN</literal> defines a sequence of rows that + satisfies certain conditions using variables defined + in <literal>DEFINE</literal> clause. If the variable is not defined in + the <literal>DEFINE</literal> clause, it is implicitly assumed following + is defined in the <literal>DEFINE</literal> clause. + +<synopsis> +<literal>variable_name</literal> AS TRUE +</synopsis> + + Note that the maximum number of variables defined + in <literal>DEFINE</literal> clause is 26. + </para> + + <para> + The SQL standard defines more subclauses: <literal>MEASURES</literal> + and <literal>SUBSET</literal>. They are not currently supported + in <productname>PostgreSQL</productname>. Also in the standard there are + more variations in <literal>AFTER MATCH</literal> clause. + </para> + <para> The purpose of a <literal>WINDOW</literal> clause is to specify the - behavior of <firstterm>window functions</firstterm> appearing in the query's + behavior of <firstterm>window functions</firstterm> appearing in the + query's <link linkend="sql-select-list"><command>SELECT</command> list</link> or <link linkend="sql-orderby"><literal>ORDER BY</literal></link> clause. These functions -- 2.43.0 [application/octet-stream] v39-0007-Row-pattern-recognition-patch-tests.patch (181.5K, ../../[email protected]/8-v39-0007-Row-pattern-recognition-patch-tests.patch) download | inline diff: From 551dceb36e3311570cad31ba42960d4d53705e70 Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii <[email protected]> Date: Fri, 16 Jan 2026 13:11:21 +0900 Subject: [PATCH v39 7/8] Row pattern recognition patch (tests). --- src/test/regress/expected/rpr.out | 3252 ++++++++++++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/rpr.sql | 1795 +++++++++++++++ 3 files changed, 5048 insertions(+), 1 deletion(-) create mode 100644 src/test/regress/expected/rpr.out create mode 100644 src/test/regress/sql/rpr.sql diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out new file mode 100644 index 00000000000..5a37c2707ce --- /dev/null +++ b/src/test/regress/expected/rpr.out @@ -0,0 +1,3252 @@ +-- +-- Test for row pattern definition clause +-- +CREATE TEMP TABLE stock ( + company TEXT, + tdate DATE, + price INTEGER +); +INSERT INTO stock VALUES ('company1', '2023-07-01', 100); +INSERT INTO stock VALUES ('company1', '2023-07-02', 200); +INSERT INTO stock VALUES ('company1', '2023-07-03', 150); +INSERT INTO stock VALUES ('company1', '2023-07-04', 140); +INSERT INTO stock VALUES ('company1', '2023-07-05', 150); +INSERT INTO stock VALUES ('company1', '2023-07-06', 90); +INSERT INTO stock VALUES ('company1', '2023-07-07', 110); +INSERT INTO stock VALUES ('company1', '2023-07-08', 130); +INSERT INTO stock VALUES ('company1', '2023-07-09', 120); +INSERT INTO stock VALUES ('company1', '2023-07-10', 130); +INSERT INTO stock VALUES ('company2', '2023-07-01', 50); +INSERT INTO stock VALUES ('company2', '2023-07-02', 2000); +INSERT INTO stock VALUES ('company2', '2023-07-03', 1500); +INSERT INTO stock VALUES ('company2', '2023-07-04', 1400); +INSERT INTO stock VALUES ('company2', '2023-07-05', 1500); +INSERT INTO stock VALUES ('company2', '2023-07-06', 60); +INSERT INTO stock VALUES ('company2', '2023-07-07', 1100); +INSERT INTO stock VALUES ('company2', '2023-07-08', 1300); +INSERT INTO stock VALUES ('company2', '2023-07-09', 1200); +INSERT INTO stock VALUES ('company2', '2023-07-10', 1300); +SELECT * FROM stock; + company | tdate | price +----------+------------+------- + company1 | 07-01-2023 | 100 + company1 | 07-02-2023 | 200 + company1 | 07-03-2023 | 150 + company1 | 07-04-2023 | 140 + company1 | 07-05-2023 | 150 + company1 | 07-06-2023 | 90 + company1 | 07-07-2023 | 110 + company1 | 07-08-2023 | 130 + company1 | 07-09-2023 | 120 + company1 | 07-10-2023 | 130 + company2 | 07-01-2023 | 50 + company2 | 07-02-2023 | 2000 + company2 | 07-03-2023 | 1500 + company2 | 07-04-2023 | 1400 + company2 | 07-05-2023 | 1500 + company2 | 07-06-2023 | 60 + company2 | 07-07-2023 | 1100 + company2 | 07-08-2023 | 1300 + company2 | 07-09-2023 | 1200 + company2 | 07-10-2023 | 1300 +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value | nth_second +----------+------------+-------+-------------+------------+------------ + company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023 + company1 | 07-02-2023 | 200 | | | + company1 | 07-03-2023 | 150 | | | + company1 | 07-04-2023 | 140 | | | + company1 | 07-05-2023 | 150 | | | + company1 | 07-06-2023 | 90 | 90 | 120 | 07-07-2023 + company1 | 07-07-2023 | 110 | | | + company1 | 07-08-2023 | 130 | | | + company1 | 07-09-2023 | 120 | | | + company1 | 07-10-2023 | 130 | | | + company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023 + company2 | 07-02-2023 | 2000 | | | + company2 | 07-03-2023 | 1500 | | | + company2 | 07-04-2023 | 1400 | | | + company2 | 07-05-2023 | 1500 | | | + company2 | 07-06-2023 | 60 | 60 | 1200 | 07-07-2023 + company2 | 07-07-2023 | 1100 | | | + company2 | 07-08-2023 | 1300 | | | + company2 | 07-09-2023 | 1200 | | | + company2 | 07-10-2023 | 1300 | | | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value | nth_second +----------+------------+-------+-------------+------------+------------ + company1 | 07-01-2023 | 100 | 100 | 150 | 07-02-2023 + company1 | 07-02-2023 | 200 | | | + company1 | 07-03-2023 | 150 | | | + company1 | 07-04-2023 | 140 | | | + company1 | 07-05-2023 | 150 | | | + company1 | 07-06-2023 | 90 | 90 | 130 | 07-07-2023 + company1 | 07-07-2023 | 110 | | | + company1 | 07-08-2023 | 130 | | | + company1 | 07-09-2023 | 120 | | | + company1 | 07-10-2023 | 130 | | | + company2 | 07-01-2023 | 50 | 50 | 1500 | 07-02-2023 + company2 | 07-02-2023 | 2000 | | | + company2 | 07-03-2023 | 1500 | | | + company2 | 07-04-2023 | 1400 | | | + company2 | 07-05-2023 | 1500 | | | + company2 | 07-06-2023 | 60 | 60 | 1300 | 07-07-2023 + company2 | 07-07-2023 | 1100 | | | + company2 | 07-08-2023 | 1300 | | | + company2 | 07-09-2023 | 1200 | | | + company2 | 07-10-2023 | 1300 | | | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value | nth_second +----------+------------+-------+-------------+------------+------------ + company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023 + company1 | 07-02-2023 | 200 | | | + company1 | 07-03-2023 | 150 | | | + company1 | 07-04-2023 | 140 | | | + company1 | 07-05-2023 | 150 | 150 | 90 | 07-06-2023 + company1 | 07-06-2023 | 90 | | | + company1 | 07-07-2023 | 110 | 110 | 120 | 07-08-2023 + company1 | 07-08-2023 | 130 | | | + company1 | 07-09-2023 | 120 | | | + company1 | 07-10-2023 | 130 | | | + company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023 + company2 | 07-02-2023 | 2000 | | | + company2 | 07-03-2023 | 1500 | | | + company2 | 07-04-2023 | 1400 | | | + company2 | 07-05-2023 | 1500 | 1500 | 60 | 07-06-2023 + company2 | 07-06-2023 | 60 | | | + company2 | 07-07-2023 | 1100 | 1100 | 1200 | 07-08-2023 + company2 | 07-08-2023 | 1300 | | | + company2 | 07-09-2023 | 1200 | | | + company2 | 07-10-2023 | 1300 | | | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value | nth_second +----------+------------+-------+-------------+------------+------------ + company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023 + company1 | 07-02-2023 | 200 | | | + company1 | 07-03-2023 | 150 | | | + company1 | 07-04-2023 | 140 | | | + company1 | 07-05-2023 | 150 | 150 | 90 | 07-06-2023 + company1 | 07-06-2023 | 90 | | | + company1 | 07-07-2023 | 110 | 110 | 120 | 07-08-2023 + company1 | 07-08-2023 | 130 | | | + company1 | 07-09-2023 | 120 | | | + company1 | 07-10-2023 | 130 | | | + company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023 + company2 | 07-02-2023 | 2000 | | | + company2 | 07-03-2023 | 1500 | | | + company2 | 07-04-2023 | 1400 | | | + company2 | 07-05-2023 | 1500 | 1500 | 60 | 07-06-2023 + company2 | 07-06-2023 | 60 | | | + company2 | 07-07-2023 | 1100 | 1100 | 1200 | 07-08-2023 + company2 | 07-08-2023 | 1300 | | | + company2 | 07-09-2023 | 1200 | | | + company2 | 07-10-2023 | 1300 | | | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | 100 | 200 + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | 150 | 140 + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | 150 | 90 + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | 110 | 130 + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | 120 | 130 + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 2000 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | 1500 | 1400 + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | 1500 | 60 + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | 1100 | 1300 + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | 1200 | 1300 + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | 100 | 130 + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | | + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 1300 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | | + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | 100 | 150 + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | 140 | 90 + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | 110 | 120 + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 1500 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | 1400 | 60 + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | 1100 | 1200 + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | | + company1 | 07-02-2023 | 200 | 200 | 150 + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | 150 | 90 + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | | + company1 | 07-08-2023 | 130 | 130 | 120 + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | | + company2 | 07-02-2023 | 2000 | 2000 | 1500 + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | 1500 | 60 + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | | + company2 | 07-08-2023 | 1300 | 1300 | 1200 + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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 +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | | + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | | + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 2000 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | 60 | 1100 + company2 | 07-07-2023 | 1100 | | + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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 +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | | + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | 90 | 130 + company1 | 07-07-2023 | 110 | | + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 1500 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | 60 | 1300 + company2 | 07-07-2023 | 1100 | | + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- basic test with none-greedy pattern +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 +); + company | tdate | price | count +----------+------------+-------+------- + company1 | 07-01-2023 | 100 | 0 + company1 | 07-02-2023 | 200 | 0 + company1 | 07-03-2023 | 150 | 3 + company1 | 07-04-2023 | 140 | 0 + company1 | 07-05-2023 | 150 | 0 + company1 | 07-06-2023 | 90 | 0 + company1 | 07-07-2023 | 110 | 0 + company1 | 07-08-2023 | 130 | 0 + company1 | 07-09-2023 | 120 | 0 + company1 | 07-10-2023 | 130 | 0 + company2 | 07-01-2023 | 50 | 0 + company2 | 07-02-2023 | 2000 | 0 + company2 | 07-03-2023 | 1500 | 0 + company2 | 07-04-2023 | 1400 | 0 + company2 | 07-05-2023 | 1500 | 0 + company2 | 07-06-2023 | 60 | 0 + company2 | 07-07-2023 | 1100 | 0 + company2 | 07-08-2023 | 1300 | 0 + company2 | 07-09-2023 | 1200 | 0 + company2 | 07-10-2023 | 1300 | 0 +(20 rows) + +-- 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 +); + company | tdate | price | count +----------+------------+-------+------- + company1 | 07-01-2023 | 100 | 0 + company1 | 07-02-2023 | 200 | 0 + company1 | 07-03-2023 | 150 | 3 + company1 | 07-04-2023 | 140 | 0 + company1 | 07-05-2023 | 150 | 0 + company1 | 07-06-2023 | 90 | 0 + company1 | 07-07-2023 | 110 | 0 + company1 | 07-08-2023 | 130 | 0 + company1 | 07-09-2023 | 120 | 0 + company1 | 07-10-2023 | 130 | 0 + company2 | 07-01-2023 | 50 | 0 + company2 | 07-02-2023 | 2000 | 0 + company2 | 07-03-2023 | 1500 | 0 + company2 | 07-04-2023 | 1400 | 0 + company2 | 07-05-2023 | 1500 | 0 + company2 | 07-06-2023 | 60 | 0 + company2 | 07-07-2023 | 1100 | 0 + company2 | 07-08-2023 | 1300 | 0 + company2 | 07-09-2023 | 1200 | 0 + company2 | 07-10-2023 | 1300 | 0 +(20 rows) + +-- 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 +); + company | tdate | price | count +----------+------------+-------+------- + company1 | 07-01-2023 | 100 | 0 + company1 | 07-02-2023 | 200 | 4 + company1 | 07-03-2023 | 150 | 0 + company1 | 07-04-2023 | 140 | 0 + company1 | 07-05-2023 | 150 | 0 + company1 | 07-06-2023 | 90 | 0 + company1 | 07-07-2023 | 110 | 4 + company1 | 07-08-2023 | 130 | 0 + company1 | 07-09-2023 | 120 | 0 + company1 | 07-10-2023 | 130 | 0 + company2 | 07-01-2023 | 50 | 0 + company2 | 07-02-2023 | 2000 | 4 + company2 | 07-03-2023 | 1500 | 0 + company2 | 07-04-2023 | 1400 | 0 + company2 | 07-05-2023 | 1500 | 0 + company2 | 07-06-2023 | 60 | 0 + company2 | 07-07-2023 | 1100 | 4 + company2 | 07-08-2023 | 1300 | 0 + company2 | 07-09-2023 | 1200 | 0 + company2 | 07-10-2023 | 1300 | 0 +(20 rows) + +-- 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 +); + company | tdate | price | count +----------+------------+-------+------- + company1 | 07-01-2023 | 100 | 0 + company1 | 07-02-2023 | 200 | 4 + company1 | 07-03-2023 | 150 | 0 + company1 | 07-04-2023 | 140 | 0 + company1 | 07-05-2023 | 150 | 0 + company1 | 07-06-2023 | 90 | 0 + company1 | 07-07-2023 | 110 | 4 + company1 | 07-08-2023 | 130 | 0 + company1 | 07-09-2023 | 120 | 0 + company1 | 07-10-2023 | 130 | 0 + company2 | 07-01-2023 | 50 | 0 + company2 | 07-02-2023 | 2000 | 4 + company2 | 07-03-2023 | 1500 | 0 + company2 | 07-04-2023 | 1400 | 0 + company2 | 07-05-2023 | 1500 | 0 + company2 | 07-06-2023 | 60 | 0 + company2 | 07-07-2023 | 1100 | 4 + company2 | 07-08-2023 | 1300 | 0 + company2 | 07-09-2023 | 1200 | 0 + company2 | 07-10-2023 | 1300 | 0 +(20 rows) + +-- 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) +); + company | tdate | price | last_value +----------+------------+-------+------------ + company1 | 07-01-2023 | 100 | 140 + company1 | 07-02-2023 | 200 | + company1 | 07-03-2023 | 150 | + company1 | 07-04-2023 | 140 | + company1 | 07-05-2023 | 150 | + company1 | 07-06-2023 | 90 | 120 + company1 | 07-07-2023 | 110 | + company1 | 07-08-2023 | 130 | + company1 | 07-09-2023 | 120 | + company1 | 07-10-2023 | 130 | + company2 | 07-01-2023 | 50 | 1400 + company2 | 07-02-2023 | 2000 | + company2 | 07-03-2023 | 1500 | + company2 | 07-04-2023 | 1400 | + company2 | 07-05-2023 | 1500 | + company2 | 07-06-2023 | 60 | 1200 + company2 | 07-07-2023 | 1100 | + company2 | 07-08-2023 | 1300 | + company2 | 07-09-2023 | 1200 | + company2 | 07-10-2023 | 1300 | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value | nth_second +----------+------------+-------+-------------+------------+------------ + company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023 + company1 | 07-02-2023 | 200 | | | + company1 | 07-03-2023 | 150 | | | + company1 | 07-04-2023 | 140 | | | + company1 | 07-05-2023 | 150 | | | + company1 | 07-06-2023 | 90 | 90 | 120 | 07-07-2023 + company1 | 07-07-2023 | 110 | | | + company1 | 07-08-2023 | 130 | | | + company1 | 07-09-2023 | 120 | | | + company1 | 07-10-2023 | 130 | | | + company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023 + company2 | 07-02-2023 | 2000 | | | + company2 | 07-03-2023 | 1500 | | | + company2 | 07-04-2023 | 1400 | | | + company2 | 07-05-2023 | 1500 | | | + company2 | 07-06-2023 | 60 | 60 | 1200 | 07-07-2023 + company2 | 07-07-2023 | 1100 | | | + company2 | 07-08-2023 | 1300 | | | + company2 | 07-09-2023 | 1200 | | | + company2 | 07-10-2023 | 1300 | | | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | 100 | 140 + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | 90 | 120 + company1 | 07-07-2023 | 110 | | + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 1400 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | 60 | 1200 + company2 | 07-07-2023 | 1100 | | + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | 100 | 140 + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | | + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 1400 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | | + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | 100 | 200 + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | 140 | 150 + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | 110 | 130 + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 2000 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | 1400 | 1500 + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | 1100 | 1300 + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- using AFTER MATCH SKIP TO NEXT ROW +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) +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | 100 | 200 + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | 140 | 150 + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | 110 | 130 + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 2000 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | 1400 | 1500 + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | 1100 | 1300 + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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 +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | 100 | 130 + company1 | 07-02-2023 | 200 | | + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | | + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | 50 | 1300 + company2 | 07-02-2023 | 2000 | | + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | | + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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 +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | | + company1 | 07-02-2023 | 200 | 07-02-2023 | 07-05-2023 + company1 | 07-03-2023 | 150 | | + company1 | 07-04-2023 | 140 | | + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | 07-07-2023 | 07-10-2023 + company1 | 07-08-2023 | 130 | | + company1 | 07-09-2023 | 120 | | + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | | + company2 | 07-02-2023 | 2000 | 07-02-2023 | 07-05-2023 + company2 | 07-03-2023 | 1500 | | + company2 | 07-04-2023 | 1400 | | + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | 07-07-2023 | 07-10-2023 + company2 | 07-08-2023 | 1300 | | + company2 | 07-09-2023 | 1200 | | + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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 +); + company | tdate | price | first_value | last_value +----------+------------+-------+-------------+------------ + company1 | 07-01-2023 | 100 | | + company1 | 07-02-2023 | 200 | 07-02-2023 | 07-05-2023 + company1 | 07-03-2023 | 150 | 07-03-2023 | 07-05-2023 + company1 | 07-04-2023 | 140 | 07-04-2023 | 07-05-2023 + company1 | 07-05-2023 | 150 | | + company1 | 07-06-2023 | 90 | | + company1 | 07-07-2023 | 110 | 07-07-2023 | 07-10-2023 + company1 | 07-08-2023 | 130 | 07-08-2023 | 07-10-2023 + company1 | 07-09-2023 | 120 | 07-09-2023 | 07-10-2023 + company1 | 07-10-2023 | 130 | | + company2 | 07-01-2023 | 50 | | + company2 | 07-02-2023 | 2000 | 07-02-2023 | 07-05-2023 + company2 | 07-03-2023 | 1500 | 07-03-2023 | 07-05-2023 + company2 | 07-04-2023 | 1400 | 07-04-2023 | 07-05-2023 + company2 | 07-05-2023 | 1500 | | + company2 | 07-06-2023 | 60 | | + company2 | 07-07-2023 | 1100 | 07-07-2023 | 07-10-2023 + company2 | 07-08-2023 | 1300 | 07-08-2023 | 07-10-2023 + company2 | 07-09-2023 | 1200 | 07-09-2023 | 07-10-2023 + company2 | 07-10-2023 | 1300 | | +(20 rows) + +-- 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 +); + gid | id | array_agg +-----+----+----------- + A | 1 | {1,2} + A | 2 | {2} + B | 3 | {3,4} + B | 4 | {4} +(4 rows) + +-- 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' +); + id | flag | match_start | match_end +----+------+-------------+----------- + 0 | A | | + 1 | A | 1 | 3 + 2 | A | | + 3 | B | | +(4 rows) + +-- 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) +); + company | tdate | price | first_value | last_value | count +----------+------------+-------+-------------+------------+------- + company1 | 07-01-2023 | 100 | 07-01-2023 | 07-03-2023 | 3 + company1 | 07-02-2023 | 200 | | | 0 + company1 | 07-03-2023 | 150 | | | 0 + company1 | 07-04-2023 | 140 | 07-04-2023 | 07-06-2023 | 3 + company1 | 07-05-2023 | 150 | | | 0 + company1 | 07-06-2023 | 90 | | | 0 + company1 | 07-07-2023 | 110 | 07-07-2023 | 07-09-2023 | 3 + company1 | 07-08-2023 | 130 | | | 0 + company1 | 07-09-2023 | 120 | | | 0 + company1 | 07-10-2023 | 130 | | | 0 + company2 | 07-01-2023 | 50 | 07-01-2023 | 07-03-2023 | 3 + company2 | 07-02-2023 | 2000 | | | 0 + company2 | 07-03-2023 | 1500 | | | 0 + company2 | 07-04-2023 | 1400 | 07-04-2023 | 07-06-2023 | 3 + company2 | 07-05-2023 | 1500 | | | 0 + company2 | 07-06-2023 | 60 | | | 0 + company2 | 07-07-2023 | 1100 | 07-07-2023 | 07-09-2023 | 3 + company2 | 07-08-2023 | 1300 | | | 0 + company2 | 07-09-2023 | 1200 | | | 0 + company2 | 07-10-2023 | 1300 | | | 0 +(20 rows) + +-- +-- 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) +); + company | tdate | price | first_value | last_value | max | min | sum | avg | count +----------+------------+-------+-------------+------------+------+-----+------+-----------------------+------- + company1 | 07-01-2023 | 100 | 100 | 140 | 200 | 100 | 590 | 147.5000000000000000 | 4 + company1 | 07-02-2023 | 200 | | | | | | | 0 + company1 | 07-03-2023 | 150 | | | | | | | 0 + company1 | 07-04-2023 | 140 | | | | | | | 0 + company1 | 07-05-2023 | 150 | | | | | | | 0 + company1 | 07-06-2023 | 90 | 90 | 120 | 130 | 90 | 450 | 112.5000000000000000 | 4 + company1 | 07-07-2023 | 110 | | | | | | | 0 + company1 | 07-08-2023 | 130 | | | | | | | 0 + company1 | 07-09-2023 | 120 | | | | | | | 0 + company1 | 07-10-2023 | 130 | | | | | | | 0 + company2 | 07-01-2023 | 50 | 50 | 1400 | 2000 | 50 | 4950 | 1237.5000000000000000 | 4 + company2 | 07-02-2023 | 2000 | | | | | | | 0 + company2 | 07-03-2023 | 1500 | | | | | | | 0 + company2 | 07-04-2023 | 1400 | | | | | | | 0 + company2 | 07-05-2023 | 1500 | | | | | | | 0 + company2 | 07-06-2023 | 60 | 60 | 1200 | 1300 | 60 | 3660 | 915.0000000000000000 | 4 + company2 | 07-07-2023 | 1100 | | | | | | | 0 + company2 | 07-08-2023 | 1300 | | | | | | | 0 + company2 | 07-09-2023 | 1200 | | | | | | | 0 + company2 | 07-10-2023 | 1300 | | | | | | | 0 +(20 rows) + +-- 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) +); + company | tdate | price | first_value | last_value | max | min | sum | avg | count +----------+------------+-------+-------------+------------+------+------+------+-----------------------+------- + company1 | 07-01-2023 | 100 | 100 | 140 | 200 | 100 | 590 | 147.5000000000000000 | 4 + company1 | 07-02-2023 | 200 | | | | | | | 0 + company1 | 07-03-2023 | 150 | | | | | | | 0 + company1 | 07-04-2023 | 140 | 140 | 90 | 150 | 90 | 380 | 126.6666666666666667 | 3 + company1 | 07-05-2023 | 150 | | | | | | | 0 + company1 | 07-06-2023 | 90 | 90 | 120 | 130 | 90 | 450 | 112.5000000000000000 | 4 + company1 | 07-07-2023 | 110 | 110 | 120 | 130 | 110 | 360 | 120.0000000000000000 | 3 + company1 | 07-08-2023 | 130 | | | | | | | 0 + company1 | 07-09-2023 | 120 | | | | | | | 0 + company1 | 07-10-2023 | 130 | | | | | | | 0 + company2 | 07-01-2023 | 50 | 50 | 1400 | 2000 | 50 | 4950 | 1237.5000000000000000 | 4 + company2 | 07-02-2023 | 2000 | | | | | | | 0 + company2 | 07-03-2023 | 1500 | | | | | | | 0 + company2 | 07-04-2023 | 1400 | 1400 | 60 | 1500 | 60 | 2960 | 986.6666666666666667 | 3 + company2 | 07-05-2023 | 1500 | | | | | | | 0 + company2 | 07-06-2023 | 60 | 60 | 1200 | 1300 | 60 | 3660 | 915.0000000000000000 | 4 + company2 | 07-07-2023 | 1100 | 1100 | 1200 | 1300 | 1100 | 3600 | 1200.0000000000000000 | 3 + company2 | 07-08-2023 | 1300 | | | | | | | 0 + company2 | 07-09-2023 | 1200 | | | | | | | 0 + company2 | 07-10-2023 | 1300 | | | | | | | 0 +(20 rows) + +-- 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; + i | v1 | j | v2 +---+----+---+---- + 1 | 10 | 2 | 10 + 1 | 10 | 2 | 11 + 1 | 11 | 2 | 10 + 1 | 11 | 2 | 11 +(4 rows) + +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 +); + i | v1 | j | v2 | count +---+----+---+----+------- + 1 | 10 | 2 | 10 | 1 + 1 | 10 | 2 | 11 | 1 + 1 | 10 | 2 | 12 | 0 + 1 | 11 | 2 | 10 | 1 + 1 | 11 | 2 | 11 | 1 + 1 | 11 | 2 | 12 | 0 + 1 | 12 | 2 | 10 | 0 + 1 | 12 | 2 | 11 | 0 + 1 | 12 | 2 | 12 | 0 +(9 rows) + +-- 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) +); + tdate | price | first_value | count +------------+-------+-------------+------- + 07-01-2023 | 100 | 07-01-2023 | 4 + 07-02-2023 | 200 | | 0 + 07-03-2023 | 150 | | 0 + 07-04-2023 | 140 | | 0 + 07-05-2023 | 150 | | 0 + 07-06-2023 | 90 | | 0 + 07-07-2023 | 110 | | 0 + 07-01-2023 | 50 | 07-01-2023 | 4 + 07-02-2023 | 2000 | | 0 + 07-03-2023 | 1500 | | 0 + 07-04-2023 | 1400 | | 0 + 07-05-2023 | 1500 | | 0 + 07-06-2023 | 60 | | 0 + 07-07-2023 | 1100 | | 0 +(14 rows) + +-- 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 +); + id | i | j | count +----+----+----+------- + 1 | 1 | 2 | 3 + 1 | 2 | 4 | 0 + 1 | 3 | 6 | 0 + 1 | 4 | 8 | 0 + 1 | 5 | 10 | 0 + 1 | 6 | 12 | 0 + 1 | 7 | 14 | 0 + 1 | 8 | 16 | 0 + 1 | 9 | 18 | 0 + 1 | 10 | 20 | 0 +(10 rows) + +-- 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; + v | c +---+------ + 1 | 5000 +(1 row) + +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; + count +------- + 5000 +(1 row) + +-- View and pg_get_viewdef tests. +CREATE TEMP VIEW v_window AS +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) +); +SELECT * FROM v_window; + company | tdate | price | first_value | last_value | nth_second +----------+------------+-------+-------------+------------+------------ + company1 | 07-01-2023 | 100 | 100 | 140 | 07-02-2023 + company1 | 07-02-2023 | 200 | | | + company1 | 07-03-2023 | 150 | | | + company1 | 07-04-2023 | 140 | | | + company1 | 07-05-2023 | 150 | | | + company1 | 07-06-2023 | 90 | 90 | 120 | 07-07-2023 + company1 | 07-07-2023 | 110 | | | + company1 | 07-08-2023 | 130 | | | + company1 | 07-09-2023 | 120 | | | + company1 | 07-10-2023 | 130 | | | + company2 | 07-01-2023 | 50 | 50 | 1400 | 07-02-2023 + company2 | 07-02-2023 | 2000 | | | + company2 | 07-03-2023 | 1500 | | | + company2 | 07-04-2023 | 1400 | | | + company2 | 07-05-2023 | 1500 | | | + company2 | 07-06-2023 | 60 | 60 | 1200 | 07-07-2023 + company2 | 07-07-2023 | 1100 | | | + company2 | 07-08-2023 | 1300 | | | + company2 | 07-09-2023 | 1200 | | | + company2 | 07-10-2023 | 1300 | | | +(20 rows) + +SELECT pg_get_viewdef('v_window'); + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + first_value(price) OVER w AS first_value, + + last_value(price) OVER w AS last_value, + + nth_value(tdate, 2) OVER w AS nth_second + + 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)) ); +(1 row) + +-- +-- Pattern optimization tests +-- VIEW shows original pattern, EXPLAIN shows optimized pattern +-- +-- Test: duplicate alternatives removal (A | B | A)+ -> (A | B)+ +CREATE TEMP VIEW v_opt_dup AS +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 | B | A)+) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_dup'); -- original: ((a | b | a)+) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ((a | b | a)+) + + DEFINE + + a AS (price > 100), + + b AS (price <= 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup; -- optimized: ((a | b)+) + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_dup + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a | b))+ + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: duplicate group removal ((A | B)+ | (A | B)+) -> (A | B)+ +CREATE TEMP VIEW v_opt_dup_group AS +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 | B)+ | (A | B)+) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_dup_group'); -- original: ((a | b)+ | (a | b)+) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ((a | b)+ | (a | b)+) + + DEFINE + + a AS (price > 100), + + b AS (price <= 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup_group; -- optimized: ((a | b)+) + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_dup_group + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a | b))+ + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: consecutive vars merge (A A A) -> A{3} +CREATE TEMP VIEW v_opt_merge AS +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 +); +SELECT pg_get_viewdef('v_opt_merge'); -- original: (a a a) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a a a) + + DEFINE + + a AS ((price >= 140) AND (price <= 150)) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge; -- optimized: a{3} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_merge + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{3} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: quantified vars merge (A A+ A) -> A{3,} +CREATE TEMP VIEW v_opt_merge_quant AS +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 > 100 +); +SELECT pg_get_viewdef('v_opt_merge_quant'); -- original: (a a+ a) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a a+ a) + + DEFINE + + a AS (price > 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_quant; -- optimized: a{3,} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_merge_quant + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{3,} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: merge two unbounded (A+ A+) -> A{2,} +CREATE TEMP VIEW v_opt_merge_unbounded AS +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+) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_merge_unbounded'); -- original: (a+ a+) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a+ a+) + + DEFINE + + a AS (price > 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_unbounded; -- optimized: a{2,} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_merge_unbounded + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{2,} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: merge with zero-min (A* A+) -> A+ +CREATE TEMP VIEW v_opt_merge_star AS +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+) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_merge_star'); -- original: (a* a+) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a* a+) + + DEFINE + + a AS (price > 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_star; -- optimized: a+ + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_merge_star + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a+ + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: complex merge (A A{2} A+ A{3}) -> A{7,} +CREATE TEMP VIEW v_opt_merge_complex AS +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{2} A+ A{3}) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_merge_complex'); -- original: (a a{2} a+ a{3}) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a a{2} a+ a{3}) + + DEFINE + + a AS (price > 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_complex; -- optimized: a{7,} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_merge_complex + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{7,} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: group merge ((A B) (A B)+) -> (A B){2,} +CREATE TEMP VIEW v_opt_merge_group AS +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 B) (A B)+) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_merge_group'); -- original: ((a b) (a b)+) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ((a b) (a b)+) + + DEFINE + + a AS (price > 100), + + b AS (price <= 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group; -- expected: (a b){2,} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_merge_group + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b){2,} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: group merge A B (A B)+ -> (A B){2,} +CREATE TEMP VIEW v_opt_merge_group2 AS +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 B (A B)+) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_merge_group2'); -- original: (a b (a b)+) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b (a b)+) + + DEFINE + + a AS (price > 100), + + b AS (price <= 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group2; -- expected: (a b){2,} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_merge_group2 + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b){2,} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: group merge (A B) (A B)+ (A B) -> (A B){3,} +CREATE TEMP VIEW v_opt_merge_group3 AS +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 B) (A B)+ (A B)) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_merge_group3'); -- original: ((a b) (a b)+ (a b)) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ((a b) (a b)+ (a b)) + + DEFINE + + a AS (price > 100), + + b AS (price <= 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group3; -- expected: (a b){3,} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_merge_group3 + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b){3,} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: group merge A B A B (A B)+ A B A B -> (A B){5,} +CREATE TEMP VIEW v_opt_merge_group4 AS +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 B A B (A B)+ A B A B) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_merge_group4'); -- original: (a b a b (a b)+ a b a b) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a b a b (a b)+ a b a b) + + DEFINE + + a AS (price > 100), + + b AS (price <= 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group4; -- expected: (a b){5,} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_merge_group4 + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: (a b){5,} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: group merge C A B (A B)+ A B C -> C (A B){3,} C +CREATE TEMP VIEW v_opt_merge_group5 AS +SELECT company, tdate, price, count(*) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (C A B (A B)+ A B C) + DEFINE + A AS price > 100, + B AS price <= 100, + C AS price > 200 +); +SELECT pg_get_viewdef('v_opt_merge_group5'); -- original: (c a b (a b)+ a b c) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (c a b (a b)+ a b c) + + DEFINE + + a AS (price > 100), + + b AS (price <= 100), + + c AS (price > 200) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group5; -- expected: c (a b){3,} c + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_merge_group5 + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: c (a b){3,} c + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test {n} quantifier display +CREATE TEMP VIEW v_quantifier_n AS +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 > 100 +); +SELECT pg_get_viewdef('v_quantifier_n'); + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a{3}) + + DEFINE + + a AS (price > 100) ); +(1 row) + +-- Test {n,} quantifier display +CREATE TEMP VIEW v_quantifier_n_plus AS +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 +); +SELECT pg_get_viewdef('v_quantifier_n_plus'); + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a{2,}) + + DEFINE + + a AS (price > 100) ); +(1 row) + +-- Test: flatten nested SEQ (A (B C)) -> A B C +CREATE TEMP VIEW v_opt_flatten_seq AS +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 (B C)) + DEFINE + A AS price > 100, + B AS price > 150, + C AS price < 150 +); +SELECT pg_get_viewdef('v_opt_flatten_seq'); -- original: (a (b c)) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (a (b c)) + + DEFINE + + a AS (price > 100), + + b AS (price > 150), + + c AS (price < 150) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_seq; -- optimized: a b c + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_flatten_seq + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a b c + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: flatten nested ALT (A | (B | C)) -> (A | B | C) +CREATE TEMP VIEW v_opt_flatten_alt AS +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 | (B | C))+) + DEFINE + A AS price > 200, + B AS price > 100, + C AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_flatten_alt'); -- original: ((a | (b | c))+) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ((a | (b | c))+) + + DEFINE + + a AS (price > 200), + + b AS (price > 100), + + c AS (price <= 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_alt; -- optimized: ((a | b | c))+ + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_flatten_alt + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: ((a | b | c))+ + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: unwrap GROUP{1,1} ((A)) -> A +CREATE TEMP VIEW v_opt_unwrap_group AS +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))) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_unwrap_group'); -- original: (((a))) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN (((a))) + + DEFINE + + a AS (price > 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_unwrap_group; -- optimized: a + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_unwrap_group + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: quantifier multiplication (A{2}){3} -> A{6} +CREATE TEMP VIEW v_opt_quant_mult AS +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}){3}) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_quant_mult'); -- original: ((a{2}){3}) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ((a{2}){3}) + + DEFINE + + a AS (price > 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult; -- optimized: a{6} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_quant_mult + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{6} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: quantifier multiplication (A{2,4}){3} -> A{6,12} +CREATE TEMP VIEW v_opt_quant_mult_range AS +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}){3}) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_quant_mult_range'); -- original: ((a{2,4}){3}) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ((a{2,4}){3}) + + DEFINE + + a AS (price > 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range; -- optimized: a{6,12} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_quant_mult_range + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{6,12} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- Test: quantifier multiplication (A{2}){3,5} -> A{6,10} +CREATE TEMP VIEW v_opt_quant_mult_range2 AS +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}){3,5}) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_quant_mult_range2'); -- original: ((a{2}){3,5}) + pg_get_viewdef +--------------------------------------------------------------------------------------- + SELECT company, + + tdate, + + price, + + count(*) OVER w AS count + + FROM stock + + WINDOW w AS (PARTITION BY company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + + AFTER MATCH SKIP PAST LAST ROW + + INITIAL + + PATTERN ((a{2}){3,5}) + + DEFINE + + a AS (price > 100) ); +(1 row) + +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range2; -- optimized: a{6,10} + QUERY PLAN +---------------------------------------------------------------------------------------------------- + Subquery Scan on v_opt_quant_mult_range2 + -> WindowAgg + Window: w AS (PARTITION BY stock.company ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) + Pattern: a{6,10} + -> Sort + Sort Key: stock.company + -> Seq Scan on stock +(7 rows) + +-- +-- Error cases +-- +-- row pattern definition variable name must not appear more than once +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 + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price), + UP AS price > PREV(price) +); +ERROR: row pattern definition variable name "up" appears more than once in DEFINE clause +LINE 11: UP AS price > PREV(price), + ^ +-- subqueries in DEFINE clause are not supported +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 + INITIAL + PATTERN (START LOWPRICE) + DEFINE + START AS TRUE, + LOWPRICE AS price < (SELECT 100) +); +ERROR: cannot use subquery in DEFINE expression +LINE 11: LOWPRICE AS price < (SELECT 100) + ^ +-- aggregates in DEFINE clause are not supported +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 + INITIAL + PATTERN (START LOWPRICE) + DEFINE + START AS TRUE, + LOWPRICE AS price < count(*) +); +ERROR: aggregate functions are not allowed in DEFINE +LINE 11: LOWPRICE AS price < count(*) + ^ +-- FRAME must start at current row when row pattern recognition is used +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 UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); +ERROR: FRAME must start at current row when row pattern recognition is used +-- EXCLUDE is not permitted +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 + EXCLUDE CURRENT ROW + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); +ERROR: EXCLUDE options are not permitted when row pattern recognition is used +-- SEEK is not supported +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 TO NEXT ROW + SEEK + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); +ERROR: SEEK is not supported +LINE 8: SEEK + ^ +HINT: Use INITIAL instead. +-- PREV's argument must have at least 1 column reference +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 TO NEXT ROW + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(1), + DOWN AS price < PREV(1) +); +ERROR: row pattern navigation operation's argument must include at least one column reference +-- Unsupported quantifier +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 TO NEXT ROW + INITIAL + PATTERN (START UP~ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(1), + DOWN AS price < PREV(1) +); +ERROR: unsupported quantifier "~" +LINE 9: PATTERN (START UP~ DOWN+) + ^ +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 TO NEXT ROW + INITIAL + PATTERN (START UP+? DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(1), + DOWN AS price < PREV(1) +); +ERROR: row pattern navigation operation's argument must include at least one column reference +-- Number of row pattern definition variable names must not exceed 26 +-- Ok +SELECT * FROM (SELECT 1 AS x) t + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (a b c d e f g h i j k l m n o p q r s t u v w x y z) + DEFINE a AS TRUE +); + x +--- + 1 +(1 row) + +-- Error +SELECT * FROM (SELECT 1 AS x) t + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (a b c d e f g h i j k l m n o p q r s t u v w x y z aa) + DEFINE a AS TRUE +); +ERROR: number of row pattern definition variable names exceeds 26 + 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) + ); + company | tdate | price | match_count +---------+------------+-------+------------- + c1 | 07-01-2023 | 100 | 0 + c1 | 07-02-2023 | | 0 + c1 | 07-03-2023 | 200 | 0 + c1 | 07-04-2023 | 150 | 0 +(4 rows) + +-- Overlapping match tests (requires multi-context for correct behavior) +-- Using array flags: 'X' = ANY(flags) for multi-TRUE support +-- Test 1: A B C D E | B C D | C D E F - three overlapping patterns +-- Different end points: B C D (4), A B C D E (5), C D E F (6) +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {B} | | + 3 | {C} | | + 4 | {D} | | + 5 | {E} | | + 6 | {F} | | +(6 rows) + +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {B} | 2 | 4 + 3 | {C} | 3 | 6 + 4 | {D} | | + 5 | {E} | | + 6 | {F} | | +(6 rows) + +-- PAST LAST: only one match +-- TO NEXT ROW with multi-context: three matches +-- Row 1: A B C D E (1-5) +-- Row 2: B C D (2-4) <- ends first! +-- Row 3: C D E F (3-6) <- ends last! +-- Test 2: A B+ C | B+ D - long B sequence with different endings +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 6 + 2 | {B} | | + 3 | {B} | | + 4 | {B} | | + 5 | {B} | | + 6 | {C} | | + 7 | {B} | 7 | 10 + 8 | {B} | 8 | 10 + 9 | {B} | 9 | 10 + 10 | {D} | | +(10 rows) + +-- Current result (correct): +-- Row 1: A B+ C (1-6) +-- Row 7-9: B+ D (7-10, 8-10, 9-10) +-- Note: Row 2-6 cannot match B+ D because Row 6 is C, not D +-- With absorption: 8-10 and 9-10 would be absorbed by 7-10 (earlier context covers later) +-- Test 3: Greedy quantifier with late failure - A B C+ D | A B +-- Pattern expects D after C+, but E comes instead ("betrayal") +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {B} | | + 3 | {C} | | + 4 | {C} | | + 5 | {C} | | + 6 | {E} | | +(6 rows) + +-- A B C+ D fails at Row 6 (E instead of D) +-- Question: Does it fallback to A B (1-2)? +-- Test 4: Lexical Order test - A B C | A B C D E +-- SQL standard: first matching alternative wins +WITH test_lexical AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (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_lexical +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C | 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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {B} | | + 3 | {C} | | + 4 | {D} | | + 5 | {E} | | +(5 rows) + +-- SQL standard Lexical Order: A B C (1-3) wins (first alternative) +-- Test 4b: Reversed pattern order - A B C D E | A B C +WITH test_lexical2 AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (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_lexical2 +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 | A 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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {B} | | + 3 | {C} | | + 4 | {D} | | + 5 | {E} | | +(5 rows) + +-- SQL standard Lexical Order: A B C D E (1-5) wins (first alternative) +-- Test 5: Multiple TRUE in single row (overlapping pattern variables) +-- Each row matches multiple DEFINE conditions simultaneously +WITH test_multi_true AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), -- A and B both TRUE + (2, ARRAY['B','C']), -- B and C both TRUE + (3, ARRAY['C','D']), -- C and D both TRUE + (4, ARRAY['D','E']), -- D and E both TRUE + (5, ARRAY['E','_']) -- E only + ) 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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,B} | 1 | 5 + 2 | {B,C} | | + 3 | {C,D} | | + 4 | {D,E} | | + 5 | {E,_} | | +(5 rows) + +-- Row 1: A=T, B=T → matches A +-- Row 2: B=T, C=T → matches B +-- Row 3: C=T, D=T → matches C +-- Row 4: D=T, E=T → matches D +-- Row 5: E=T → matches E +-- Result: match 1-5 (A B C D E) +-- Test 6: Diagonal pattern with multi-TRUE (shifted 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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A,_} | 1 | 4 + 2 | {B,A} | 2 | 5 + 3 | {C,B} | | + 4 | {D,C} | | + 5 | {_,D} | | +(5 rows) + +-- Possible matches: +-- Start Row 1: A(1) B(2) C(3) D(4) → 1-4 +-- Start Row 2: A(2) B(3) C(4) D(5) → 2-5 (because Row 2 has A too!) +-- =================================================================== +-- Context Absorption Tests +-- =================================================================== +-- Test absorption 1: Basic A+ pattern - later contexts absorbed by earlier +WITH test_absorb_basic AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (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_absorb_basic +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {A} | 4 | 4 + 5 | {B} | | +(5 rows) + +-- Pattern A+ is absorbable (unbounded first element, only one unbounded) +-- Without absorption: 4 matches (1-4, 2-4, 3-4, 4-4) +-- With absorption: Row 1 match (1-4), rows 2-4 absorbed +-- Test absorption 2: A+ B pattern - absorption with fixed suffix +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {A} | 3 | 4 + 4 | {B} | | + 5 | {X} | | +(5 rows) + +-- Pattern A+ B is absorbable (A+ unbounded first, B bounded suffix) +-- All potential matches end at same row (row 4 with B) +-- With absorption: Row 1 match (1-4), rows 2-3 absorbed +-- Test absorption 3: 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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 4 + 2 | {B} | 2 | 4 + 3 | {B} | 3 | 4 + 4 | {D} | | + 5 | {X} | | +(5 rows) + +-- Both branches B+ C and B+ D are absorbable (B+ unbounded first) +-- B+ D branch matches: potential 1-4, 2-4, 3-4 +-- Row 1 (1-4) absorbs Row 2 (2-4) and Row 3 (3-4) - same endpoint +-- Test absorption 4: Non-absorbable pattern (A B+ - unbounded not first) +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {B} | | + 3 | {B} | | + 4 | {B} | | + 5 | {X} | | +(5 rows) + +-- Pattern A B+ is NOT absorbable (A bounded first, B+ unbounded but not first) +-- Only Row 1 can start match (only row with A), so only one match: 1-4 +-- Test absorption 5: GROUP merge enables absorption +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 6 + 2 | {B} | | + 3 | {A} | 3 | 6 + 4 | {B} | | + 5 | {A} | | + 6 | {B} | | + 7 | {X} | | +(7 rows) + +-- Pattern optimized: (A B) (A B)+ -> (A B){2,} +-- Potential matches: 1-6 (3 reps), 3-6 (2 reps), 5-6 needs 2 reps (fail) +-- Row 1 (1-6) absorbs Row 3 (3-6) - same endpoint, top-level unbounded GROUP +-- Test absorption 6: Multiple unbounded - 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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | 2 | 4 + 3 | {B} | | + 4 | {B} | | + 5 | {X} | | +(5 rows) + +-- Row 1: 1-4, Row 2: absorbed (same endpoint 4) +-- ============================================ +-- Jacob's RPR Patterns (from jacob branch) +-- ============================================ +-- Test: A? (optional, greedy) +WITH jacob_optional AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, 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 jacob_optional +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 1 + 2 | {X} | | +(2 rows) + +-- Expected: 1-1 (matches A) +-- Test: A{2} (exact count) +WITH jacob_exact AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (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 jacob_exact +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {A} | | + 3 | {A} | | + 4 | {X} | | +(4 rows) + +-- Expected: 1-2 +-- Test: A{1,3} (bounded range, greedy) +WITH jacob_bounded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (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 jacob_bounded +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 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | | + 3 | {A} | | + 4 | {A} | 4 | 4 + 5 | {X} | | +(5 rows) + +-- Expected: 1-3 (greedy takes max), then 4-4 +-- Test: A | B (simple alternation) +WITH jacob_simple_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, 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 jacob_simple_alt +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 1 + 2 | {B} | 2 | 2 + 3 | {X} | | +(3 rows) + +-- Expected: 1-1 (A), 2-2 (B) +-- Test: A | B | C (three-way alternation) +WITH jacob_three_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, 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 jacob_three_alt +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {B} | 1 | 1 + 2 | {X} | | +(2 rows) + +-- Expected: 1-1 (B) +-- Test: A B C (concatenation) +WITH jacob_concat AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (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 jacob_concat +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {B} | | + 3 | {C} | | + 4 | {X} | | +(4 rows) + +-- Expected: 1-3 +-- Test: A B? C (optional middle) +WITH jacob_optional_mid AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['C']), + (3, 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 jacob_optional_mid +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 2 + 2 | {C} | | + 3 | {X} | | +(3 rows) + +-- Expected: 1-2 (A C, B skipped) +-- Test: (A B){2} (nested group with quantifier) +WITH jacob_nested_group 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 jacob_nested_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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {B} | | + 3 | {A} | | + 4 | {B} | | + 5 | {X} | | +(5 rows) + +-- Expected: 1-4 (A B A B) +-- Test: (A){3} (quantifier on grouped single element) +WITH jacob_group_quant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (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 jacob_group_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A){3}) + DEFINE A AS 'A' = ANY(flags) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {A} | | + 3 | {A} | | + 4 | {X} | | +(4 rows) + +-- Expected: 1-3 +-- Test: A B C | A B C D E (lexical order - first alt wins) +WITH jacob_lex_first AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (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 jacob_lex_first +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C | 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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 3 + 2 | {B} | | + 3 | {C} | | + 4 | {D} | | + 5 | {E} | | +(5 rows) + +-- Expected: 1-3 (A B C wins by lexical order) +-- Test: A B C D E | A B C (lexical order - longer first wins) +WITH jacob_lex_long AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (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 jacob_lex_long +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 | A 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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 5 + 2 | {B} | | + 3 | {C} | | + 4 | {D} | | + 5 | {E} | | +(5 rows) + +-- Expected: 1-5 (A B C D E wins by lexical order) +-- ============================================ +-- Alternation with quantifiers (BUG cases from Jacob's tests) +-- ============================================ +-- Test: (A | B)+ C - alternation inside quantified group followed by C +-- BUG: Currently returns NULL instead of matching 1-4 +WITH jacob_alt_quant 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 jacob_alt_quant +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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {B} | | + 3 | {A} | | + 4 | {C} | | +(4 rows) + +-- Expected: 1-4 (A B A C) +-- Test: ((A | B) C)+ - alternation inside group with outer quantifier +-- Currently returns two separate matches (1-2, 3-4) instead of single 1-4 +WITH jacob_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 jacob_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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {C} | | + 3 | {B} | | + 4 | {C} | | + 5 | {X} | | +(5 rows) + +-- Expected: 1-4 (A C B C) +-- ============================================ +-- RELUCTANT quantifiers (parser only - executor TODO) +-- ============================================ +-- Test: A+? B (reluctant) - CURRENTLY ACTS LIKE A+ (GREEDY) +-- Parser accepts the syntax but executor doesn't differentiate +WITH jacob_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 jacob_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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | | + 3 | {A} | | + 4 | {B} | | +(4 rows) + +-- Current: 1-4 (A A A B) - greedy behavior +-- Expected when RELUCTANT implemented: 3-4 (A B) - shortest A before B +-- Test: A{1,3}? B (reluctant bounded) +WITH jacob_reluctant_bounded 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 jacob_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) +); + id | flags | match_start | match_end +----+-------+-------------+----------- + 1 | {A} | 1 | 4 + 2 | {A} | | + 3 | {A} | | + 4 | {B} | | +(4 rows) + +-- Current: 1-4 (greedy, takes max A before B) +-- Expected when RELUCTANT implemented: 3-4 (takes min A before B) diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 021d57f66bb..f3f4ca7dfbe 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -102,7 +102,7 @@ test: publication subscription # Another group of parallel tests # select_views depends on create_view # ---------- -test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite +test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock indirect_toast equivclass stats_rewrite rpr # ---------- # 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..89d54772072 --- /dev/null +++ b/src/test/regress/sql/rpr.sql @@ -0,0 +1,1795 @@ +-- +-- Test for row pattern definition clause +-- + +CREATE TEMP TABLE stock ( + company TEXT, + tdate DATE, + price INTEGER +); +INSERT INTO stock VALUES ('company1', '2023-07-01', 100); +INSERT INTO stock VALUES ('company1', '2023-07-02', 200); +INSERT INTO stock VALUES ('company1', '2023-07-03', 150); +INSERT INTO stock VALUES ('company1', '2023-07-04', 140); +INSERT INTO stock VALUES ('company1', '2023-07-05', 150); +INSERT INTO stock VALUES ('company1', '2023-07-06', 90); +INSERT INTO stock VALUES ('company1', '2023-07-07', 110); +INSERT INTO stock VALUES ('company1', '2023-07-08', 130); +INSERT INTO stock VALUES ('company1', '2023-07-09', 120); +INSERT INTO stock VALUES ('company1', '2023-07-10', 130); +INSERT INTO stock VALUES ('company2', '2023-07-01', 50); +INSERT INTO stock VALUES ('company2', '2023-07-02', 2000); +INSERT INTO stock VALUES ('company2', '2023-07-03', 1500); +INSERT INTO stock VALUES ('company2', '2023-07-04', 1400); +INSERT INTO stock VALUES ('company2', '2023-07-05', 1500); +INSERT INTO stock VALUES ('company2', '2023-07-06', 60); +INSERT INTO stock VALUES ('company2', '2023-07-07', 1100); +INSERT INTO stock VALUES ('company2', '2023-07-08', 1300); +INSERT INTO stock VALUES ('company2', '2023-07-09', 1200); +INSERT INTO stock VALUES ('company2', '2023-07-10', 1300); + +SELECT * FROM stock; + +-- 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 none-greedy pattern +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 +); + +-- 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 +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) +); + +-- 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 +); + +-- 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) +); + +-- 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) +); + +-- 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 +); + +-- 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; + +-- View and pg_get_viewdef tests. +CREATE TEMP VIEW v_window AS +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) +); + +SELECT * FROM v_window; +SELECT pg_get_viewdef('v_window'); + +-- +-- Pattern optimization tests +-- VIEW shows original pattern, EXPLAIN shows optimized pattern +-- + +-- Test: duplicate alternatives removal (A | B | A)+ -> (A | B)+ +CREATE TEMP VIEW v_opt_dup AS +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 | B | A)+) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_dup'); -- original: ((a | b | a)+) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup; -- optimized: ((a | b)+) + +-- Test: duplicate group removal ((A | B)+ | (A | B)+) -> (A | B)+ +CREATE TEMP VIEW v_opt_dup_group AS +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 | B)+ | (A | B)+) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_dup_group'); -- original: ((a | b)+ | (a | b)+) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_dup_group; -- optimized: ((a | b)+) + +-- Test: consecutive vars merge (A A A) -> A{3} +CREATE TEMP VIEW v_opt_merge AS +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 +); +SELECT pg_get_viewdef('v_opt_merge'); -- original: (a a a) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge; -- optimized: a{3} + +-- Test: quantified vars merge (A A+ A) -> A{3,} +CREATE TEMP VIEW v_opt_merge_quant AS +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 > 100 +); +SELECT pg_get_viewdef('v_opt_merge_quant'); -- original: (a a+ a) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_quant; -- optimized: a{3,} + +-- Test: merge two unbounded (A+ A+) -> A{2,} +CREATE TEMP VIEW v_opt_merge_unbounded AS +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+) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_merge_unbounded'); -- original: (a+ a+) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_unbounded; -- optimized: a{2,} + +-- Test: merge with zero-min (A* A+) -> A+ +CREATE TEMP VIEW v_opt_merge_star AS +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+) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_merge_star'); -- original: (a* a+) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_star; -- optimized: a+ + +-- Test: complex merge (A A{2} A+ A{3}) -> A{7,} +CREATE TEMP VIEW v_opt_merge_complex AS +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{2} A+ A{3}) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_merge_complex'); -- original: (a a{2} a+ a{3}) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_complex; -- optimized: a{7,} + +-- Test: group merge ((A B) (A B)+) -> (A B){2,} +CREATE TEMP VIEW v_opt_merge_group AS +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 B) (A B)+) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_merge_group'); -- original: ((a b) (a b)+) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group; -- expected: (a b){2,} + +-- Test: group merge A B (A B)+ -> (A B){2,} +CREATE TEMP VIEW v_opt_merge_group2 AS +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 B (A B)+) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_merge_group2'); -- original: (a b (a b)+) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group2; -- expected: (a b){2,} + +-- Test: group merge (A B) (A B)+ (A B) -> (A B){3,} +CREATE TEMP VIEW v_opt_merge_group3 AS +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 B) (A B)+ (A B)) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_merge_group3'); -- original: ((a b) (a b)+ (a b)) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group3; -- expected: (a b){3,} + +-- Test: group merge A B A B (A B)+ A B A B -> (A B){5,} +CREATE TEMP VIEW v_opt_merge_group4 AS +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 B A B (A B)+ A B A B) + DEFINE + A AS price > 100, + B AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_merge_group4'); -- original: (a b a b (a b)+ a b a b) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group4; -- expected: (a b){5,} + +-- Test: group merge C A B (A B)+ A B C -> C (A B){3,} C +CREATE TEMP VIEW v_opt_merge_group5 AS +SELECT company, tdate, price, count(*) OVER w + FROM stock + WINDOW w AS ( + PARTITION BY company + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (C A B (A B)+ A B C) + DEFINE + A AS price > 100, + B AS price <= 100, + C AS price > 200 +); +SELECT pg_get_viewdef('v_opt_merge_group5'); -- original: (c a b (a b)+ a b c) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_merge_group5; -- expected: c (a b){3,} c + +-- Test {n} quantifier display +CREATE TEMP VIEW v_quantifier_n AS +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 > 100 +); +SELECT pg_get_viewdef('v_quantifier_n'); + +-- Test {n,} quantifier display +CREATE TEMP VIEW v_quantifier_n_plus AS +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 +); +SELECT pg_get_viewdef('v_quantifier_n_plus'); + +-- Test: flatten nested SEQ (A (B C)) -> A B C +CREATE TEMP VIEW v_opt_flatten_seq AS +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 (B C)) + DEFINE + A AS price > 100, + B AS price > 150, + C AS price < 150 +); +SELECT pg_get_viewdef('v_opt_flatten_seq'); -- original: (a (b c)) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_seq; -- optimized: a b c + +-- Test: flatten nested ALT (A | (B | C)) -> (A | B | C) +CREATE TEMP VIEW v_opt_flatten_alt AS +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 | (B | C))+) + DEFINE + A AS price > 200, + B AS price > 100, + C AS price <= 100 +); +SELECT pg_get_viewdef('v_opt_flatten_alt'); -- original: ((a | (b | c))+) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_flatten_alt; -- optimized: ((a | b | c))+ + +-- Test: unwrap GROUP{1,1} ((A)) -> A +CREATE TEMP VIEW v_opt_unwrap_group AS +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))) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_unwrap_group'); -- original: (((a))) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_unwrap_group; -- optimized: a + +-- Test: quantifier multiplication (A{2}){3} -> A{6} +CREATE TEMP VIEW v_opt_quant_mult AS +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}){3}) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_quant_mult'); -- original: ((a{2}){3}) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult; -- optimized: a{6} + +-- Test: quantifier multiplication (A{2,4}){3} -> A{6,12} +CREATE TEMP VIEW v_opt_quant_mult_range AS +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}){3}) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_quant_mult_range'); -- original: ((a{2,4}){3}) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range; -- optimized: a{6,12} + +-- Test: quantifier multiplication (A{2}){3,5} -> A{6,10} +CREATE TEMP VIEW v_opt_quant_mult_range2 AS +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}){3,5}) + DEFINE + A AS price > 100 +); +SELECT pg_get_viewdef('v_opt_quant_mult_range2'); -- original: ((a{2}){3,5}) +EXPLAIN (COSTS OFF) SELECT * FROM v_opt_quant_mult_range2; -- optimized: a{6,10} + +-- +-- Error cases +-- + +-- row pattern definition variable name must not appear more than once +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 + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price), + UP AS price > PREV(price) +); + +-- subqueries in DEFINE clause are not supported +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 + INITIAL + PATTERN (START LOWPRICE) + DEFINE + START AS TRUE, + LOWPRICE AS price < (SELECT 100) +); + +-- aggregates in DEFINE clause are not supported +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 + INITIAL + PATTERN (START LOWPRICE) + DEFINE + START AS TRUE, + LOWPRICE AS price < count(*) +); + +-- FRAME must start at current row when row pattern recognition is used +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 UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- EXCLUDE is not permitted +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 + EXCLUDE CURRENT ROW + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- SEEK is not supported +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 TO NEXT ROW + SEEK + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(price), + DOWN AS price < PREV(price) +); + +-- PREV's argument must have at least 1 column reference +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 TO NEXT ROW + INITIAL + PATTERN (START UP+ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(1), + DOWN AS price < PREV(1) +); + +-- Unsupported quantifier +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 TO NEXT ROW + INITIAL + PATTERN (START UP~ DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(1), + DOWN AS price < PREV(1) +); + +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 TO NEXT ROW + INITIAL + PATTERN (START UP+? DOWN+) + DEFINE + START AS TRUE, + UP AS price > PREV(1), + DOWN AS price < PREV(1) +); + +-- Number of row pattern definition variable names must not exceed 26 + +-- Ok +SELECT * FROM (SELECT 1 AS x) t + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (a b c d e f g h i j k l m n o p q r s t u v w x y z) + DEFINE a AS TRUE +); + +-- Error +SELECT * FROM (SELECT 1 AS x) t + WINDOW w AS ( + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + PATTERN (a b c d e f g h i j k l m n o p q r s t u v w x y z aa) + DEFINE a AS TRUE +); + + 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) + ); + + +-- Overlapping match tests (requires multi-context for correct behavior) +-- Using array flags: 'X' = ANY(flags) for multi-TRUE support + +-- Test 1: A B C D E | B C D | C D E F - three overlapping patterns +-- Different end points: B C D (4), A B C D E (5), C D E F (6) +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) +); + +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) +); +-- PAST LAST: only one match +-- TO NEXT ROW with multi-context: three matches +-- Row 1: A B C D E (1-5) +-- Row 2: B C D (2-4) <- ends first! +-- Row 3: C D E F (3-6) <- ends last! + +-- Test 2: A B+ C | B+ D - long B sequence with different endings +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) +); +-- Current result (correct): +-- Row 1: A B+ C (1-6) +-- Row 7-9: B+ D (7-10, 8-10, 9-10) +-- Note: Row 2-6 cannot match B+ D because Row 6 is C, not D +-- With absorption: 8-10 and 9-10 would be absorbed by 7-10 (earlier context covers later) + +-- Test 3: Greedy quantifier with late failure - A B C+ D | A B +-- Pattern expects D after C+, but E comes instead ("betrayal") +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) +); +-- A B C+ D fails at Row 6 (E instead of D) +-- Question: Does it fallback to A B (1-2)? + +-- Test 4: Lexical Order test - A B C | A B C D E +-- SQL standard: first matching alternative wins +WITH test_lexical AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (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_lexical +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C | 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) +); +-- SQL standard Lexical Order: A B C (1-3) wins (first alternative) + +-- Test 4b: Reversed pattern order - A B C D E | A B C +WITH test_lexical2 AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (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_lexical2 +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 | A 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) +); +-- SQL standard Lexical Order: A B C D E (1-5) wins (first alternative) + +-- Test 5: Multiple TRUE in single row (overlapping pattern variables) +-- Each row matches multiple DEFINE conditions simultaneously +WITH test_multi_true AS ( + SELECT * FROM (VALUES + (1, ARRAY['A','B']), -- A and B both TRUE + (2, ARRAY['B','C']), -- B and C both TRUE + (3, ARRAY['C','D']), -- C and D both TRUE + (4, ARRAY['D','E']), -- D and E both TRUE + (5, ARRAY['E','_']) -- E only + ) 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) +); +-- Row 1: A=T, B=T → matches A +-- Row 2: B=T, C=T → matches B +-- Row 3: C=T, D=T → matches C +-- Row 4: D=T, E=T → matches D +-- Row 5: E=T → matches E +-- Result: match 1-5 (A B C D E) + +-- Test 6: Diagonal pattern with multi-TRUE (shifted 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) +); +-- Possible matches: +-- Start Row 1: A(1) B(2) C(3) D(4) → 1-4 +-- Start Row 2: A(2) B(3) C(4) D(5) → 2-5 (because Row 2 has A too!) + +-- =================================================================== +-- Context Absorption Tests +-- =================================================================== + +-- Test absorption 1: Basic A+ pattern - later contexts absorbed by earlier +WITH test_absorb_basic AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (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_absorb_basic +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) +); +-- Pattern A+ is absorbable (unbounded first element, only one unbounded) +-- Without absorption: 4 matches (1-4, 2-4, 3-4, 4-4) +-- With absorption: Row 1 match (1-4), rows 2-4 absorbed + +-- Test absorption 2: A+ B pattern - absorption with fixed suffix +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) +); +-- Pattern A+ B is absorbable (A+ unbounded first, B bounded suffix) +-- All potential matches end at same row (row 4 with B) +-- With absorption: Row 1 match (1-4), rows 2-3 absorbed + +-- Test absorption 3: 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) +); +-- Both branches B+ C and B+ D are absorbable (B+ unbounded first) +-- B+ D branch matches: potential 1-4, 2-4, 3-4 +-- Row 1 (1-4) absorbs Row 2 (2-4) and Row 3 (3-4) - same endpoint + +-- Test absorption 4: Non-absorbable pattern (A B+ - unbounded not first) +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) +); +-- Pattern A B+ is NOT absorbable (A bounded first, B+ unbounded but not first) +-- Only Row 1 can start match (only row with A), so only one match: 1-4 + +-- Test absorption 5: GROUP merge enables absorption +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) +); +-- Pattern optimized: (A B) (A B)+ -> (A B){2,} +-- Potential matches: 1-6 (3 reps), 3-6 (2 reps), 5-6 needs 2 reps (fail) +-- Row 1 (1-6) absorbs Row 3 (3-6) - same endpoint, top-level unbounded GROUP + +-- Test absorption 6: Multiple unbounded - 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) +); +-- Row 1: 1-4, Row 2: absorbed (same endpoint 4) + +-- ============================================ +-- Jacob's RPR Patterns (from jacob branch) +-- ============================================ + +-- Test: A? (optional, greedy) +WITH jacob_optional AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, 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 jacob_optional +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) +); +-- Expected: 1-1 (matches A) + +-- Test: A{2} (exact count) +WITH jacob_exact AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (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 jacob_exact +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) +); +-- Expected: 1-2 + +-- Test: A{1,3} (bounded range, greedy) +WITH jacob_bounded AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (4, ARRAY['A']), + (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 jacob_bounded +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 'A' = ANY(flags) +); +-- Expected: 1-3 (greedy takes max), then 4-4 + +-- Test: A | B (simple alternation) +WITH jacob_simple_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, 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 jacob_simple_alt +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) +); +-- Expected: 1-1 (A), 2-2 (B) + +-- Test: A | B | C (three-way alternation) +WITH jacob_three_alt AS ( + SELECT * FROM (VALUES + (1, ARRAY['B']), + (2, 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 jacob_three_alt +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) +); +-- Expected: 1-1 (B) + +-- Test: A B C (concatenation) +WITH jacob_concat AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (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 jacob_concat +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) +); +-- Expected: 1-3 + +-- Test: A B? C (optional middle) +WITH jacob_optional_mid AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['C']), + (3, 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 jacob_optional_mid +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) +); +-- Expected: 1-2 (A C, B skipped) + +-- Test: (A B){2} (nested group with quantifier) +WITH jacob_nested_group 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 jacob_nested_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) +); +-- Expected: 1-4 (A B A B) + +-- Test: (A){3} (quantifier on grouped single element) +WITH jacob_group_quant AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['A']), + (3, ARRAY['A']), + (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 jacob_group_quant +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN ((A){3}) + DEFINE A AS 'A' = ANY(flags) +); +-- Expected: 1-3 + +-- Test: A B C | A B C D E (lexical order - first alt wins) +WITH jacob_lex_first AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (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 jacob_lex_first +WINDOW w AS ( + ORDER BY id + ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING + AFTER MATCH SKIP PAST LAST ROW + PATTERN (A B C | 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) +); +-- Expected: 1-3 (A B C wins by lexical order) + +-- Test: A B C D E | A B C (lexical order - longer first wins) +WITH jacob_lex_long AS ( + SELECT * FROM (VALUES + (1, ARRAY['A']), + (2, ARRAY['B']), + (3, ARRAY['C']), + (4, ARRAY['D']), + (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 jacob_lex_long +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 | A 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) +); +-- Expected: 1-5 (A B C D E wins by lexical order) + +-- ============================================ +-- Alternation with quantifiers (BUG cases from Jacob's tests) +-- ============================================ + +-- Test: (A | B)+ C - alternation inside quantified group followed by C +-- BUG: Currently returns NULL instead of matching 1-4 +WITH jacob_alt_quant 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 jacob_alt_quant +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) +); +-- Expected: 1-4 (A B A C) + +-- Test: ((A | B) C)+ - alternation inside group with outer quantifier +-- Currently returns two separate matches (1-2, 3-4) instead of single 1-4 +WITH jacob_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 jacob_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) +); +-- Expected: 1-4 (A C B C) + +-- ============================================ +-- RELUCTANT quantifiers (parser only - executor TODO) +-- ============================================ + +-- Test: A+? B (reluctant) - CURRENTLY ACTS LIKE A+ (GREEDY) +-- Parser accepts the syntax but executor doesn't differentiate +WITH jacob_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 jacob_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) +); +-- Current: 1-4 (A A A B) - greedy behavior +-- Expected when RELUCTANT implemented: 3-4 (A B) - shortest A before B + +-- Test: A{1,3}? B (reluctant bounded) +WITH jacob_reluctant_bounded 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 jacob_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) +); +-- Current: 1-4 (greedy, takes max A before B) +-- Expected when RELUCTANT implemented: 3-4 (takes min A before B) -- 2.43.0 [application/octet-stream] v39-0008-Row-pattern-recognition-patch-typedefs.list.patch (1.2K, ../../[email protected]/9-v39-0008-Row-pattern-recognition-patch-typedefs.list.patch) download | inline diff: From 79f6003a5e7aa24881bac411b039d836b5990d5b Mon Sep 17 00:00:00 2001 From: Tatsuo Ishii <[email protected]> Date: Fri, 16 Jan 2026 13:11:21 +0900 Subject: [PATCH v39 8/8] Row pattern recognition patch (typedefs.list). --- src/tools/pgindent/typedefs.list | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3f3a888fd0e..f92f047c0e1 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1771,6 +1771,7 @@ NamedLWLockTrancheRequest NamedTuplestoreScan NamedTuplestoreScanState NamespaceInfo +NavigationInfo NestLoop NestLoopParam NestLoopState @@ -2445,6 +2446,15 @@ RI_CompareKey RI_ConstraintInfo RI_QueryHashEntry RI_QueryKey +RPCommonSyntax +RPRDepth +RPRElemFlags +RPRElemIdx +RPRPattern +RPRPatternElement +RPRQuantity +RPRVarId +RPSkipTo RTEKind RTEPermissionInfo RWConflict @@ -2921,6 +2931,7 @@ StreamStopReason String StringInfo StringInfoData +StringSet StripnullState SubLink SubLinkType @@ -3259,6 +3270,7 @@ VarString VarStringSortSupport Variable VariableAssignHook +VariablePos VariableSetKind VariableSetStmt VariableShowStmt -- 2.43.0 ^ permalink raw reply [nested|flat] 24+ messages in thread
end of thread, other threads:[~2026-01-16 04:44 UTC | newest] Thread overview: 24+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2021-01-24 02:33 [PATCH 14/21] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2021-01-24 02:33 [PATCH 14/18] doc review: logical decode in prepare Justin Pryzby <[email protected]> 2026-01-16 04:44 Re: Row pattern recognition Tatsuo Ishii <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox