Row pattern recognition

Поиск
Список
Период
Сортировка
От Tatsuo Ishii
Тема Row pattern recognition
Дата
Msg-id 20230625.210509.1276733411677577841.t-ishii@sranhm.sra.co.jp
обсуждение исходный текст
Ответы Re: Row pattern recognition
Список pgsql-hackers
Attached is a PoC patch to implement "Row pattern recognition" (RPR)
in SQL:2016 (I know SQL:2023 is already out, but I don't have access
to it). Actually SQL:2016 defines RPR in two places[1]:

    Feature R010, “Row pattern recognition: FROM clause”
    Feature R020, “Row pattern recognition: WINDOW clause”

The patch includes partial support for R020 part.

- What is RPR?

RPR provides a way to search series of data using regular expression
patterns. Suppose you have a stock database.

CREATE TABLE stock (
       company TEXT,
       tdate DATE,
       price BIGINT);

You want to find a "V-shaped" pattern: i.e. price goes down for 1 or
more days, then goes up for 1 or more days. If we try to implement the
query in PostgreSQL, it could be quite complex and inefficient.

RPR provides convenient way to implement the query.

SELECT company, tdate, price, rpr(price) OVER w FROM stock
 WINDOW w AS (
 PARTITION BY company
 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 PATTERN (START DOWN+ UP+)
 DEFINE
  START AS TRUE,
  UP AS price > PREV(price),
  DOWN AS price < PREV(price)
);

"PATTERN" and "DEFINE" are the key clauses of RPR. DEFINE defines 3
"row pattern variables" namely START, UP and DOWN. They are associated
with logical expressions namely "TRUE", "price > PREV(price)", and
"price < PREV(price)". Note that "PREV" function returns price column
in the previous row. So, UP is true if price is higher than previous
day. On the other hand, DOWN is true if price is lower than previous
day.  PATTERN uses the row pattern variables to create a necessary
pattern.  In this case, the first row is always match because START is
always true, and second or more rows match with "UP" ('+' is a regular
expression representing one or more), and subsequent rows match with
"DOWN".

Here is the sample output.

 company  |   tdate    | price | rpr  
----------+------------+-------+------
 company1 | 2023-07-01 |   100 |     
 company1 | 2023-07-02 |   200 |  200 -- 200->150->140->150
 company1 | 2023-07-03 |   150 |  150 -- 150->140->150
 company1 | 2023-07-04 |   140 |     
 company1 | 2023-07-05 |   150 |  150 -- 150->90->110->130
 company1 | 2023-07-06 |    90 |     
 company1 | 2023-07-07 |   110 |     
 company1 | 2023-07-08 |   130 |     
 company1 | 2023-07-09 |   120 |     
 company1 | 2023-07-10 |   130 |     

rpr shows the first row if all the patterns are satisfied. In the
example above 200, 150, 150 are the cases.  Other rows are shown as
NULL. For example, on 2023-07-02 price starts with 200, then goes down
to 150 then 140 but goes up 150 next day.

As far as I know, only Oracle implements RPR (only R010. R020 is not
implemented) among OSS/commercial RDBMSs. There are a few DWH software
having RPR. According to [2] they are Snowflake and MS Stream
Analytics. It seems Trino is another one[3].

- Note about the patch

The current implementation is not only a subset of the standard, but
is different from it in some places. The example above is written as
follows according to the standard:

SELECT company, tdate, startprice OVER w FROM stock
 WINDOW w AS (
 PARTITION BY company
 MEASURES
  START.price AS startprice
 ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
 PATTERN (START DOWN+ UP+)
 DEFINE
  START AS TRUE,
  UP AS UP.price > PREV(UP.price),
  DOWN AS DOWN.price < PREV(DOWN.price)
);

Notice that rpr(price) is written as START.price and startprice in the
standard. MEASURES defines variable names used in the target list used
with "OVER window". As OVER only allows functions in PostgreSQL, I had
to make up a window function "rpr" which performs the row pattern
recognition task.  I was not able to find a way to implement
expressions like START.price (START is not a table alias). Any
suggestion is greatly appreciated.

The differences from the standard include:

o MEASURES is not supported
o SUBSET is not supported
o FIRST, LAST, CLASSIFIER are not supported
o PREV/NEXT in the standard accept more complex arguments
o Regular expressions other than "+" are not supported
o Only AFTER MATCH SKIP TO NEXT ROW is supported (if AFTER MATCH is
  not specified, AFTER MATCH SKIP TO NEXT ROW is assumed. In the
  standard AFTER MATCH SKIP PAST LAST ROW is assumed in this case). I
  have a plan to implement AFTER MATCH SKIP PAST LAST ROW though.
o INITIAL or SEEK are not supported ((behaves as if INITIAL is specified)
o Aggregate functions associated with window clause using RPR do not respect RPR

It seems RPR in the standard is quite complex. I think we can start
with a small subset of RPR then we could gradually enhance the
implementation.

Comments and suggestions are welcome.

[1] https://sqlperformance.com/2019/04/t-sql-queries/row-pattern-recognition-in-sql
[2] https://link.springer.com/article/10.1007/s13222-022-00404-3
[3] https://trino.io/docs/current/sql/pattern-recognition-in-window.html
--
Tatsuo Ishii
SRA OSS LLC
English: http://www.sraoss.co.jp/index_en/
Japanese:http://www.sraoss.co.jp
From a4d9fc7d243ea598ebd526e3218df33bde5162c1 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Sun, 25 Jun 2023 20:48:14 +0900
Subject: [PATCH v1 1/7] Row pattern recognition patch for raw parser.

---
 src/backend/parser/gram.y       | 218 +++++++++++++++++++++++++++++---
 src/include/nodes/parsenodes.h  |  54 ++++++++
 src/include/parser/kwlist.h     |   8 ++
 src/include/parser/parse_node.h |   1 +
 4 files changed, 266 insertions(+), 15 deletions(-)

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 39ab7eac0d..9520b5a07f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -251,6 +251,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
     DefElem       *defelt;
     SortBy       *sortby;
     WindowDef  *windef;
+    RPCommonSyntax    *rpcom;
+    RPSubsetItem    *rpsubset;
     JoinExpr   *jexpr;
     IndexElem  *ielem;
     StatsElem  *selem;
@@ -453,8 +455,12 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
                 TriggerTransitions TriggerReferencing
                 vacuum_relation_list opt_vacuum_relation_list
                 drop_option_list pub_obj_list
-
-%type <node>    opt_routine_body
+                row_pattern_measure_list row_pattern_definition_list
+                opt_row_pattern_subset_clause
+                row_pattern_subset_list row_pattern_subset_rhs
+                row_pattern
+%type <rpsubset>     row_pattern_subset_item
+%type <node>    opt_routine_body row_pattern_term
 %type <groupclause> group_clause
 %type <list>    group_by_list
 %type <node>    group_by_item empty_grouping_set rollup_clause cube_clause
@@ -551,6 +557,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <range>    relation_expr_opt_alias
 %type <node>    tablesample_clause opt_repeatable_clause
 %type <target>    target_el set_target insert_column_item
+                row_pattern_measure_item row_pattern_definition
 
 %type <str>        generic_option_name
 %type <node>    generic_option_arg
@@ -633,6 +640,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>    window_clause window_definition_list opt_partition_clause
 %type <windef>    window_definition over_clause window_specification
                 opt_frame_clause frame_extent frame_bound
+%type <rpcom>    opt_row_pattern_common_syntax opt_row_pattern_skip_to
+%type <boolean>    opt_row_pattern_initial_or_seek
+%type <list>    opt_row_pattern_measures
 %type <ival>    opt_window_exclusion_clause
 %type <str>        opt_existing_window_name
 %type <boolean> opt_if_not_exists
@@ -645,7 +655,6 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <list>        hash_partbound
 %type <defelt>        hash_partbound_elem
 
-
 %type <node>        json_format_clause_opt
                     json_value_expr
                     json_output_clause_opt
@@ -705,7 +714,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
 
@@ -721,7 +730,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
     HANDLER HAVING HEADER_P HOLD HOUR_P
 
     IDENTITY_P IF_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
 
@@ -733,7 +742,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
     LEADING LEAKPROOF LEAST LEFT LEVEL LIKE LIMIT LISTEN LOAD LOCAL
     LOCALTIME LOCALTIMESTAMP LOCATION LOCK_P LOCKED LOGGED
 
-    MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD
+    MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MEASURES MERGE METHOD
     MINUTE_P MINVALUE MODE MONTH_P MOVE
 
     NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE
@@ -745,9 +754,9 @@ 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 PASSING PASSWORD
-    PLACING PLANS POLICY
-    POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
+    PARALLEL PARAMETER PARSER PARTIAL PARTITION PASSING PASSWORD PAST
+    PATTERN PLACING PLANS POLICY
+    POSITION PRECEDING PRECISION PREMUTE PRESERVE PREPARE PREPARED PRIMARY
     PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
     QUOTE
@@ -757,12 +766,13 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
     RESET 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 SQL_P STABLE STANDALONE_P
     START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P
-    SUBSCRIPTION SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER
+    SUBSCRIPTION SUBSET SUBSTRING SUPPORT SYMMETRIC SYSID SYSTEM_P SYSTEM_USER
 
     TABLE TABLES TABLESAMPLE TABLESPACE TEMP TEMPLATE TEMPORARY TEXT_P THEN
     TIES TIME TIMESTAMP TO TRAILING TRANSACTION TRANSFORM
@@ -855,6 +865,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
  */
 %nonassoc    UNBOUNDED        /* ideally would have same precedence as IDENT */
 %nonassoc    IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
+%nonassoc    MEASURES AFTER INITIAL SEEK PATTERN
 %left        Op OPERATOR        /* multi-character ops and user-defined operators */
 %left        '+' '-'
 %left        '*' '/' '%'
@@ -15773,7 +15784,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_row_pattern_measures opt_frame_clause
+                        opt_row_pattern_common_syntax ')'
                 {
                     WindowDef  *n = makeNode(WindowDef);
 
@@ -15781,10 +15793,12 @@ window_specification: '(' opt_existing_window_name opt_partition_clause
                     n->refname = $2;
                     n->partitionClause = $3;
                     n->orderClause = $4;
+                    n->rowPatternMeasures = $5;
                     /* copy relevant fields of opt_frame_clause */
-                    n->frameOptions = $5->frameOptions;
-                    n->startOffset = $5->startOffset;
-                    n->endOffset = $5->endOffset;
+                    n->frameOptions = $6->frameOptions;
+                    n->startOffset = $6->startOffset;
+                    n->endOffset = $6->endOffset;
+                    n->rpCommonSyntax = $7;
                     n->location = @1;
                     $$ = n;
                 }
@@ -15808,6 +15822,31 @@ opt_partition_clause: PARTITION BY expr_list        { $$ = $3; }
             | /*EMPTY*/                                { $$ = NIL; }
         ;
 
+/*
+ * ROW PATTERN MEASURES
+ */
+opt_row_pattern_measures: MEASURES row_pattern_measure_list    { $$ = $2; }
+            | /*EMPTY*/                                { $$ = NIL; }
+        ;
+
+row_pattern_measure_list:
+            row_pattern_measure_item
+                    { $$ = list_make1($1); }
+            | row_pattern_measure_list ',' row_pattern_measure_item
+                    { $$ = lappend($1, $3); }
+        ;
+
+row_pattern_measure_item:
+            a_expr AS ColLabel
+                {
+                    $$ = makeNode(ResTarget);
+                    $$->name = $3;
+                    $$->indirection = NIL;
+                    $$->val = (Node *) $1;
+                    $$->location = @1;
+                }
+        ;
+
 /*
  * For frame clauses, we return a WindowDef, but only some fields are used:
  * frameOptions, startOffset, and endOffset.
@@ -15967,6 +16006,139 @@ opt_window_exclusion_clause:
             | /*EMPTY*/                { $$ = 0; }
         ;
 
+opt_row_pattern_common_syntax:
+opt_row_pattern_skip_to opt_row_pattern_initial_or_seek
+                PATTERN '(' row_pattern ')'
+                opt_row_pattern_subset_clause
+                DEFINE row_pattern_definition_list
+            {
+                RPCommonSyntax *n = makeNode(RPCommonSyntax);
+                n->rpSkipTo = $1->rpSkipTo;
+                n->rpSkipVariable = $1->rpSkipVariable;
+                n->initial = $2;
+                n->rpPatterns = $5;
+                n->rpSubsetClause = $7;
+                n->rpDefs = $9;
+                $$ = n;
+            }
+            | /*EMPTY*/        { $$ = NULL; }
+    ;
+
+opt_row_pattern_skip_to:
+            AFTER MATCH SKIP TO NEXT ROW
+                {
+                    RPCommonSyntax *n = makeNode(RPCommonSyntax);
+                    n->rpSkipTo = ST_NEXT_ROW;
+                    n->rpSkipVariable = NULL;
+                    $$ = n;
+            }
+            | AFTER MATCH SKIP PAST LAST_P ROW
+                {
+                    RPCommonSyntax *n = makeNode(RPCommonSyntax);
+                    n->rpSkipTo = ST_PAST_LAST_ROW;
+                    n->rpSkipVariable = NULL;
+                    $$ = n;
+                }
+/*
+            | AFTER MATCH SKIP TO FIRST_P ColId        %prec FIRST_P
+                {
+                    RPCommonSyntax *n = makeNode(RPCommonSyntax);
+                    n->rpSkipTo = ST_FIRST_VARIABLE;
+                    n->rpSkipVariable = $6;
+                    $$ = n;
+                }
+            | AFTER MATCH SKIP TO LAST_P ColId        %prec LAST_P
+                {
+                    RPCommonSyntax *n = makeNode(RPCommonSyntax);
+                    n->rpSkipTo = ST_LAST_VARIABLE;
+                    n->rpSkipVariable = $6;
+                    $$ = n;
+                }
+ * Shift/reduce
+            | AFTER MATCH SKIP TO ColId
+                {
+                    RPCommonSyntax *n = makeNode(RPCommonSyntax);
+                    n->rpSkipTo = ST_VARIABLE;
+                    n->rpSkipVariable = $5;
+                    $$ = n;
+                }
+*/
+            | /*EMPTY*/
+                {
+                    RPCommonSyntax *n = makeNode(RPCommonSyntax);
+                    /* temporary set default to ST_NEXT_ROW */
+                    n->rpSkipTo = ST_NEXT_ROW;
+                    n->rpSkipVariable = NULL;
+                    $$ = n;
+                }
+    ;
+
+opt_row_pattern_initial_or_seek:
+            INITIAL            { $$ = true; }
+            | SEEK
+                {
+                    ereport(ERROR,
+                            (errcode(ERRCODE_SYNTAX_ERROR),
+                             errmsg("SEEK is not supported"),
+                             errhint("Use INITIAL."),
+                             parser_errposition(@1)));
+                }
+            | /*EMPTY*/        { $$ = true; }
+        ;
+
+row_pattern:
+            row_pattern_term                            { $$ = list_make1($1); }
+            | row_pattern row_pattern_term                { $$ = lappend($1, $2); }
+        ;
+
+row_pattern_term:
+            ColId    { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "", (Node *)makeString($1), NULL, @1); }
+            | ColId '*'    { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "*", (Node *)makeString($1), NULL, @1); }
+            | ColId '+'    { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "+", (Node *)makeString($1), NULL, @1); }
+            | ColId '?'    { $$ = (Node *) makeSimpleA_Expr(AEXPR_OP, "?", (Node *)makeString($1), NULL, @1); }
+        ;
+
+opt_row_pattern_subset_clause:
+            SUBSET row_pattern_subset_list    { $$ = $2; }
+            | /*EMPTY*/                                                { $$ = NIL; }
+        ;
+
+row_pattern_subset_list:
+            row_pattern_subset_item                                    { $$ = list_make1($1); }
+            | row_pattern_subset_list ',' row_pattern_subset_item    { $$ = lappend($1, $3); }
+            | /*EMPTY*/                                                { $$ = NIL; }
+        ;
+
+row_pattern_subset_item: ColId '=' '(' row_pattern_subset_rhs ')'
+            {
+                RPSubsetItem *n = makeNode(RPSubsetItem);
+                n->name = $1;
+                n->rhsVariable = $4;
+                $$ = n;
+            }
+        ;
+
+row_pattern_subset_rhs:
+            ColId                                { $$ = list_make1(makeStringConst($1, @1)); }
+            | row_pattern_subset_rhs ',' ColId    { $$ = lappend($1, makeStringConst($3, @1)); }
+            | /*EMPTY*/                            { $$ = NIL; }
+        ;
+
+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.
@@ -17060,6 +17232,7 @@ unreserved_keyword:
             | INDEXES
             | INHERIT
             | INHERITS
+            | INITIAL
             | INLINE_P
             | INPUT_P
             | INSENSITIVE
@@ -17088,6 +17261,7 @@ unreserved_keyword:
             | MATCHED
             | MATERIALIZED
             | MAXVALUE
+            | MEASURES
             | MERGE
             | METHOD
             | MINUTE_P
@@ -17130,9 +17304,12 @@ unreserved_keyword:
             | PARTITION
             | PASSING
             | PASSWORD
+            | PAST
+            | PATTERN
             | PLANS
             | POLICY
             | PRECEDING
+            | PREMUTE
             | PREPARE
             | PREPARED
             | PRESERVE
@@ -17180,6 +17357,7 @@ unreserved_keyword:
             | SEARCH
             | SECOND_P
             | SECURITY
+            | SEEK
             | SEQUENCE
             | SEQUENCES
             | SERIALIZABLE
@@ -17205,6 +17383,7 @@ unreserved_keyword:
             | STRICT_P
             | STRIP_P
             | SUBSCRIPTION
+            | SUBSET
             | SUPPORT
             | SYSID
             | SYSTEM_P
@@ -17389,6 +17568,7 @@ reserved_keyword:
             | CURRENT_USER
             | DEFAULT
             | DEFERRABLE
+            | DEFINE
             | DESC
             | DISTINCT
             | DO
@@ -17551,6 +17731,7 @@ bare_label_keyword:
             | DEFAULTS
             | DEFERRABLE
             | DEFERRED
+            | DEFINE
             | DEFINER
             | DELETE_P
             | DELIMITER
@@ -17626,6 +17807,7 @@ bare_label_keyword:
             | INDEXES
             | INHERIT
             | INHERITS
+            | INITIAL
             | INITIALLY
             | INLINE_P
             | INNER_P
@@ -17673,6 +17855,7 @@ bare_label_keyword:
             | MATCHED
             | MATERIALIZED
             | MAXVALUE
+            | MEASURES
             | MERGE
             | METHOD
             | MINVALUE
@@ -17726,11 +17909,14 @@ bare_label_keyword:
             | PARTITION
             | PASSING
             | PASSWORD
+            | PAST
+            | PATTERN
             | PLACING
             | PLANS
             | POLICY
             | POSITION
             | PRECEDING
+            | PREMUTE
             | PREPARE
             | PREPARED
             | PRESERVE
@@ -17782,6 +17968,7 @@ bare_label_keyword:
             | SCROLL
             | SEARCH
             | SECURITY
+            | SEEK
             | SELECT
             | SEQUENCE
             | SEQUENCES
@@ -17813,6 +18000,7 @@ bare_label_keyword:
             | STRICT_P
             | STRIP_P
             | SUBSCRIPTION
+            | SUBSET
             | SUBSTRING
             | SUPPORT
             | SYMMETRIC
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index b3bec90e52..22e1a01a0e 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -548,6 +548,44 @@ typedef struct SortBy
     int            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 */
+    ST_FIRST_VARIABLE,            /* SKIP TO FIRST variable name */
+    ST_LAST_VARIABLE,            /* SKIP TO LAST variable name */
+    ST_VARIABLE                    /* SKIP TO variable name */
+} RPSkipTo;
+
+/*
+ * Row Pattern SUBSET clause item
+ */
+typedef struct RPSubsetItem
+{
+    NodeTag        type;
+    char       *name;            /* Row Pattern SUBSET clause variable name */
+    List       *rhsVariable;    /* Row Pattern SUBSET rhs variables (list of char *string) */
+} RPSubsetItem;
+
+/*
+ * RowPatternCommonSyntax - raw representation of row pattern common syntax
+ *
+ */
+typedef struct RPCommonSyntax
+{
+    NodeTag        type;
+    RPSkipTo    rpSkipTo;        /* Row Pattern AFTER MATCH SKIP type */
+    char       *rpSkipVariable;    /* Row Pattern Skip To variable name, if any */
+    bool        initial;        /* true if <row pattern initial or seek> is initial */
+    List       *rpPatterns;        /* PATTERN variables (list of A_Expr) */
+    List       *rpSubsetClause;    /* row pattern subset clause (list of RPSubsetItem), if any */
+    List       *rpDefs;            /* row pattern definitions clause (list of ResTarget) */
+} RPCommonSyntax;
+
 /*
  * WindowDef - raw representation of WINDOW and OVER clauses
  *
@@ -563,6 +601,8 @@ typedef struct WindowDef
     char       *refname;        /* referenced window name, if any */
     List       *partitionClause;    /* PARTITION BY expression list */
     List       *orderClause;    /* ORDER BY (list of SortBy) */
+    List       *rowPatternMeasures;    /* row pattern measures (list of ResTarget) */
+    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 */
@@ -1482,6 +1522,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. "patternVariable" and "patternRegexp" represents PATTERN
+ * clause.
+ * 
  * The information relevant for the query jumbling is the partition clause
  * type and its bounds.
  */
@@ -1513,6 +1558,15 @@ 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 Recognition AFTER MACH SKIP clause */
+    RPSkipTo    rpSkipTo;        /* Row Pattern Skip To type */    
+    bool        initial;        /* true if <row pattern initial or seek> is initial */
+    /* Row Pattern Recognition DEFINE clause (list of TargetEntry) */
+    List        *defineClause;
+    /* Row Pattern Recognition PATTERN variable name (list of String) */
+    List        *patternVariable;
+    /* Row Pattern Recognition PATTERN regular expression quantifier ('+' or ''. list of String) */
+    List        *patternRegexp;
 } WindowClause;
 
 /*
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index f5b2e61ca5..12603b311c 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -128,6 +128,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, RESERVED_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)
@@ -212,6 +213,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)
@@ -263,6 +265,7 @@ PG_KEYWORD("match", MATCH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("matched", MATCHED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("materialized", MATERIALIZED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("maxvalue", MAXVALUE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("measures", MEASURES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("merge", MERGE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("method", METHOD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("minute", MINUTE_P, UNRESERVED_KEYWORD, AS_LABEL)
@@ -324,12 +327,15 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("partition", PARTITION, 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("pattern", PATTERN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("precision", PRECISION, COL_NAME_KEYWORD, AS_LABEL)
+PG_KEYWORD("premute", PREMUTE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("prepare", PREPARE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("prepared", PREPARED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("preserve", PRESERVE, UNRESERVED_KEYWORD, BARE_LABEL)
@@ -383,6 +389,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)
@@ -414,6 +421,7 @@ PG_KEYWORD("stored", STORED, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("strict", STRICT_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("strip", STRIP_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("subscription", SUBSCRIPTION, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("subset", SUBSET, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("substring", SUBSTRING, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("support", SUPPORT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("symmetric", SYMMETRIC, RESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index f589112d5e..6640090910 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.25.1

From 8ee66b4dede4ffe76116914ef1afaf7935c4041e Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Sun, 25 Jun 2023 20:48:14 +0900
Subject: [PATCH v1 2/7] Row pattern recognition patch (parse/analysis).

---
 src/backend/parser/parse_agg.c    |   7 ++
 src/backend/parser/parse_clause.c | 196 +++++++++++++++++++++++++++++-
 src/backend/parser/parse_expr.c   |   4 +
 src/backend/parser/parse_func.c   |   3 +
 4 files changed, 209 insertions(+), 1 deletion(-)

diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c
index 85cd47b7ae..aa7a1cee80 100644
--- a/src/backend/parser/parse_agg.c
+++ b/src/backend/parser/parse_agg.c
@@ -564,6 +564,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
@@ -953,6 +957,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 f61f794755..5bb11add3c 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -100,7 +100,10 @@ static WindowClause *findWindowClause(List *wclist, const char *name);
 static Node *transformFrameOffset(ParseState *pstate, int frameOptions,
                                   Oid rangeopfamily, Oid rangeopcintype, Oid *inRangeFunc,
                                   Node *clause);
-
+static void  transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef);
+static List *transformDefineClause(ParseState *pstate, WindowClause *wc, WindowDef *windef);
+static List *transformPatternClause(ParseState *pstate, WindowClause *wc, WindowDef *windef);
+static List *transformMeasureClause(ParseState *pstate, WindowClause *wc, WindowDef *windef);
 
 /*
  * transformFromClause -
@@ -2949,6 +2952,10 @@ transformWindowDefinitions(ParseState *pstate,
                                              rangeopfamily, rangeopcintype,
                                              &wc->endInRangeFunc,
                                              windef->endOffset);
+
+        /* Process Row Pattern Recognition related clauses */
+        transformRPR(pstate, wc, windef);
+
         wc->runCondition = NIL;
         wc->winref = winref;
 
@@ -3814,3 +3821,190 @@ transformFrameOffset(ParseState *pstate, int frameOptions,
 
     return node;
 }
+
+/*
+ * transformRPR
+ *        Process Row Pattern Recognition related clauses
+ */
+static void
+transformRPR(ParseState *pstate, WindowClause *wc, WindowDef *windef)
+{
+    /* Check Frame option. Frame must start at current row */
+
+    /*
+     * Window definition exists?
+     */
+    if (windef == NULL)
+        return;
+
+    /*
+     * Row Pattern Common Syntax clause exists?
+     */
+    if (windef->rpCommonSyntax == NULL)
+        return;
+
+    if ((wc->frameOptions & FRAMEOPTION_START_CURRENT_ROW) == 0)
+        ereport(ERROR,
+                (errcode(ERRCODE_SYNTAX_ERROR),
+                 errmsg("FRAME must start at current row when row patttern recognition is used")));
+
+    /* Transform AFTER MACH SKIP TO clause */
+    wc->rpSkipTo = windef->rpCommonSyntax->rpSkipTo;
+    if (wc->rpSkipTo != ST_NEXT_ROW)
+        ereport(ERROR,
+                (errcode(ERRCODE_SYNTAX_ERROR),
+                 errmsg("only AFTER MATCH SKIP TO NEXT_ROW is supported")));
+
+    /* Transform SEEK or INITIAL clause */
+    wc->initial = windef->rpCommonSyntax->initial;
+
+    /* Transform DEFINE clause into list of TargetEntry's */
+    wc->defineClause = transformDefineClause(pstate, wc, windef);
+
+    /* Check PATTERN clause and copy to patternClause */
+    transformPatternClause(pstate, wc, windef);
+
+    /* Transform MEASURE clause */
+    transformMeasureClause(pstate, wc, windef);
+}
+
+/*
+ * 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)
+{
+    ListCell    *lc;
+    ResTarget    *restarget, *r;
+    List        *restargets;
+
+
+    /*
+     * If Row Definition Common Syntax exists, DEFINE clause must exist.
+     * (the raw parser should have already checked it.)
+     */
+    Assert(windef->rpCommonSyntax->rpDefs != NULL);
+
+    /*
+     * Check for duplicate row pattern definition variables.  The standard
+     * requires that no two row pattern definition variable names shall be
+     * equivalent.
+     */
+    restargets = NIL;
+    foreach(lc, windef->rpCommonSyntax->rpDefs)
+    {
+        char    *name;
+        ListCell    *l;
+
+        restarget = (ResTarget *)lfirst(lc);
+        name = restarget->name;
+
+        /*
+         * Make sure that 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))));
+        }
+        restargets = lappend(restargets, restarget);
+    }
+    list_free(restargets);
+
+    return transformTargetList(pstate, windef->rpCommonSyntax->rpDefs,
+                               EXPR_KIND_RPR_DEFINE);
+}
+
+/*
+ * transformPatternClause
+ *        Process PATTERN clause and return PATTERN clause in the raw parse tree
+ */
+static List *
+transformPatternClause(ParseState *pstate, WindowClause *wc, WindowDef *windef)
+{
+    List        *patterns;
+    ListCell    *lc, *l;
+
+    /*
+     * Row Pattern Common Syntax clause exists?
+     */
+    if (windef->rpCommonSyntax == NULL)
+        return NULL;
+
+    /*
+     * Primary row pattern variable names in PATTERN clause must appear in
+     * DEFINE clause as row pattern definition variable names.
+     */
+    wc->patternVariable = NIL;
+    wc->patternRegexp = NIL;
+    foreach(lc, windef->rpCommonSyntax->rpPatterns)
+    {
+        A_Expr    *a;
+        char    *name;
+        char    *regexp;
+        bool    found = false;
+
+        if (!IsA(lfirst(lc), A_Expr))
+            ereport(ERROR,
+                    errmsg("node type is not A_Expr"));
+
+        a = (A_Expr *)lfirst(lc);
+        name = strVal(a->lexpr);
+
+        foreach(l, windef->rpCommonSyntax->rpDefs)
+        {
+            ResTarget    *restarget = (ResTarget *)lfirst(l);
+
+            if (!strcmp(restarget->name, name))
+            {
+                found = true;
+                break;
+            }
+        }
+
+        if (!found)
+            ereport(ERROR,
+                    (errcode(ERRCODE_SYNTAX_ERROR),
+                     errmsg("primary row pattern variable name \"%s\" does not appear in DEFINE clause",
+                            name),
+                     parser_errposition(pstate, exprLocation((Node *)a))));
+        wc->patternVariable = lappend(wc->patternVariable, makeString(pstrdup(name)));
+        regexp = strVal(lfirst(list_head(a->name)));
+        wc->patternRegexp = lappend(wc->patternRegexp, makeString(pstrdup(regexp)));
+    }
+    return patterns;
+}
+
+/*
+ * transformMeasureClause
+ *        Process MEASURE clause
+ */
+static List *
+transformMeasureClause(ParseState *pstate, WindowClause *wc, WindowDef *windef)
+{
+    if (windef->rowPatternMeasures == NIL)
+        return NIL;
+
+    ereport(ERROR,
+            (errcode(ERRCODE_SYNTAX_ERROR),
+             errmsg("%s","MEASURE clause is not supported yet"),
+             parser_errposition(pstate, exprLocation((Node *)windef->rowPatternMeasures))));
+}
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 346fd272b6..20231d9ec0 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -541,6 +541,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;
 
@@ -1754,6 +1755,7 @@ transformSubLink(ParseState *pstate, SubLink *sublink)
         case EXPR_KIND_VALUES:
         case EXPR_KIND_VALUES_SINGLE:
         case EXPR_KIND_CYCLE_MARK:
+        case EXPR_KIND_RPR_DEFINE:
             /* okay */
             break;
         case EXPR_KIND_CHECK_CONSTRAINT:
@@ -3133,6 +3135,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 b3f0b6a137..2ff3699538 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -2656,6 +2656,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
-- 
2.25.1

From 20855de1fcd1ec35c4cac2367fd3d96684cd806d Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Sun, 25 Jun 2023 20:48:14 +0900
Subject: [PATCH v1 3/7] Row pattern recognition patch (planner).

---
 src/backend/optimizer/plan/createplan.c | 13 ++++++++++---
 src/include/nodes/plannodes.h           |  9 +++++++++
 2 files changed, 19 insertions(+), 3 deletions(-)

diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 4bb38160b3..5ac65248a2 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -287,8 +287,8 @@ static WindowAgg *make_windowagg(List *tlist, Index winref,
                                  int frameOptions, Node *startOffset, Node *endOffset,
                                  Oid startInRangeFunc, Oid endInRangeFunc,
                                  Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst,
-                                 List *runCondition, List *qual, bool topWindow,
-                                 Plan *lefttree);
+                                 List *runCondition, List *patternVariable, List *patternRegexp, List *defineClause,
+                                 List *qual, bool topWindow, Plan *lefttree);
 static Group *make_group(List *tlist, List *qual, int numGroupCols,
                          AttrNumber *grpColIdx, Oid *grpOperators, Oid *grpCollations,
                          Plan *lefttree);
@@ -2684,6 +2684,9 @@ create_windowagg_plan(PlannerInfo *root, WindowAggPath *best_path)
                           wc->inRangeAsc,
                           wc->inRangeNullsFirst,
                           wc->runCondition,
+                          wc->patternVariable,
+                          wc->patternRegexp,
+                          wc->defineClause,
                           best_path->qual,
                           best_path->topwindow,
                           subplan);
@@ -6581,7 +6584,8 @@ make_windowagg(List *tlist, Index winref,
                int frameOptions, Node *startOffset, Node *endOffset,
                Oid startInRangeFunc, Oid endInRangeFunc,
                Oid inRangeColl, bool inRangeAsc, bool inRangeNullsFirst,
-               List *runCondition, List *qual, bool topWindow, Plan *lefttree)
+               List *runCondition, List *patternVariable, List *patternRegexp, List *defineClause,
+               List *qual, bool topWindow, Plan *lefttree)
 {
     WindowAgg  *node = makeNode(WindowAgg);
     Plan       *plan = &node->plan;
@@ -6607,6 +6611,9 @@ make_windowagg(List *tlist, Index winref,
     node->inRangeAsc = inRangeAsc;
     node->inRangeNullsFirst = inRangeNullsFirst;
     node->topWindow = topWindow;
+    node->patternVariable = patternVariable;
+    node->patternRegexp = patternRegexp;
+    node->defineClause = defineClause;
 
     plan->targetlist = tlist;
     plan->lefttree = lefttree;
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 1b787fe031..eb511176ac 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -1096,6 +1096,15 @@ typedef struct WindowAgg
     /* nulls sort first for in_range tests? */
     bool        inRangeNullsFirst;
 
+    /* Row Pattern Recognition PATTERN variable name (list of String) */
+    List        *patternVariable;
+
+    /* Row Pattern Recognition PATTERN regular expression quantifier ('+' or ''. list of String) */
+    List        *patternRegexp;
+
+    /* Row Pattern Recognition DEFINE clause (list of TargetEntry) */
+    List       *defineClause;
+
     /*
      * false for all apart from the WindowAgg that's closest to the root of
      * the plan
-- 
2.25.1

From 7e738230cc8965cc139d5cda2341a930367adf7a Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Sun, 25 Jun 2023 20:48:14 +0900
Subject: [PATCH v1 4/7] Row pattern recognition patch (executor).

---
 src/backend/executor/nodeWindowAgg.c | 223 +++++++++++++++++++++-
 src/backend/utils/adt/windowfuncs.c  | 274 ++++++++++++++++++++++++++-
 src/include/catalog/pg_proc.dat      |   9 +
 src/include/nodes/execnodes.h        |  12 ++
 src/include/windowapi.h              |   9 +
 5 files changed, 517 insertions(+), 10 deletions(-)

diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index 310ac23e3a..ae997dff86 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -48,6 +48,7 @@
 #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"
@@ -159,6 +160,14 @@ typedef struct WindowStatePerAggData
     bool        restart;        /* need to restart this agg in this cycle? */
 } WindowStatePerAggData;
 
+/*
+ * Map between Var attno in a target list and the parsed attno.
+ */
+typedef struct AttnoMap {
+    List        *attno;            /* att number in target list (list of AttNumber) */
+    List        *attnosyn;        /* parsed att number (list of AttNumber) */
+} AttnoMap;
+
 static void initialize_windowaggregate(WindowAggState *winstate,
                                        WindowStatePerFunc perfuncstate,
                                        WindowStatePerAgg peraggstate);
@@ -195,9 +204,9 @@ static Datum GetAggInitVal(Datum textInitVal, Oid transtype);
 
 static bool are_peers(WindowAggState *winstate, TupleTableSlot *slot1,
                       TupleTableSlot *slot2);
-static bool window_gettupleslot(WindowObject winobj, int64 pos,
-                                TupleTableSlot *slot);
 
+static void attno_map(Node *node, AttnoMap *map);
+static bool attno_map_walker(Node *node, void *context);
 
 /*
  * initialize_windowaggregate
@@ -2388,6 +2397,12 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
     TupleDesc    scanDesc;
     ListCell   *l;
 
+    TargetEntry    *te;
+    Expr        *expr;
+    Var            *var;
+    int            nargs;
+    AttnoMap    attnomap;
+
     /* check for unsupported flags */
     Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
 
@@ -2483,6 +2498,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
@@ -2667,6 +2692,69 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
     winstate->inRangeAsc = node->inRangeAsc;
     winstate->inRangeNullsFirst = node->inRangeNullsFirst;
 
+    /* Set up row pattern recognition PATTERN clause */
+    winstate->patternVariableList = node->patternVariable;
+    winstate->patternRegexpList = node->patternRegexp;
+
+    /* Set up row pattern recognition DEFINE clause */
+
+    /*
+     * Collect mapping between varattno and varattnosyn in the targetlist.
+     * XXX: For now we only check RPR's argument. Eventually we have to
+     * recurse the targetlist to find out all mappings in Var nodes.
+     */
+    attnomap.attno = NIL;
+    attnomap.attnosyn = NIL;
+
+    foreach (l, node->plan.targetlist)
+    {
+        te = lfirst(l);
+        if (IsA(te->expr, WindowFunc))
+        {
+            WindowFunc    *func = (WindowFunc *)te->expr;
+            if (func->winfnoid != F_RPR)
+                continue;
+
+            /* sanity check */
+            nargs = list_length(func->args);
+            if (list_length(func->args) != 1)
+                elog(ERROR, "RPR must have 1 argument but function %d has %d args", func->winfnoid, nargs);
+
+            expr = (Expr *) lfirst(list_head(func->args));
+            if (!IsA(expr, Var))
+                elog(ERROR, "RPR's arg is not Var");
+
+            var = (Var *)expr;
+            elog(DEBUG1, "resname: %s varattno: %d varattnosyn: %d",
+                 te->resname, var->varattno, var->varattnosyn);
+            attnomap.attno = lappend_int(attnomap.attno, var->varattno);
+            attnomap.attnosyn = lappend_int(attnomap.attnosyn, var->varattnosyn);
+        }
+    }
+
+    winstate->defineVariableList = NIL;
+    winstate->defineClauseList = NIL;
+    if (node->defineClause != NIL)
+    {
+        foreach(l, node->defineClause)
+        {
+            char        *name;
+            ExprState    *exps;
+
+            te = lfirst(l);
+            name = te->resname;
+            expr = te->expr;
+
+            elog(DEBUG1, "defineVariable name: %s", name);
+            winstate->defineVariableList = lappend(winstate->defineVariableList,
+                                                   makeString(pstrdup(name)));
+            /* tweak expr so that it referes to outer slot */
+            attno_map((Node *)expr, &attnomap);
+            exps = ExecInitExpr(expr, (PlanState *) winstate);
+            winstate->defineClauseList = lappend(winstate->defineClauseList, exps);
+        }
+    }
+
     winstate->all_first = true;
     winstate->partition_spooled = false;
     winstate->more_partitions = false;
@@ -2674,6 +2762,76 @@ ExecInitWindowAgg(WindowAgg *node, EState *estate, int eflags)
     return winstate;
 }
 
+/*
+ * Rewrite Var node's varattno to the varattno which is used in the target
+ * list using AttnoMap.  We also rewrite varno so that it sees outer tuple
+ * (PREV) or inner tuple (NEXT).
+ */
+static void
+attno_map(Node *node, AttnoMap *map)
+{
+    (void) expression_tree_walker(node, attno_map_walker, (void *) map);
+}
+
+static bool
+attno_map_walker(Node *node, void *context)
+{
+    FuncExpr    *func;
+    int            nargs;
+    Expr        *expr;
+    Var            *var;
+    AttnoMap    *attnomap;
+    ListCell    *lc1, *lc2;
+    
+    if (node == NULL)
+        return false;
+
+    attnomap = (AttnoMap *) context;
+
+    if (IsA(node, FuncExpr))
+    {
+        func = (FuncExpr *)node;
+
+        if (func->funcid == F_PREV || func->funcid == F_NEXT)
+        {
+            /* sanity check */
+            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);
+
+            expr = (Expr *) lfirst(list_head(func->args));
+            if (!IsA(expr, Var))
+                elog(ERROR, "PREV/NEXT's arg is not Var");    /* XXX: is it possible that arg type is Const? */
+            var = (Var *)expr;
+
+            if (func->funcid == F_PREV)
+                var->varno = OUTER_VAR;
+            else
+                var->varno = INNER_VAR;
+        }
+        return expression_tree_walker(node, attno_map_walker, (void *) context);
+    }
+    else if (IsA(node, Var))
+    {
+        var = (Var *)node;     
+
+        elog(DEBUG1, "original varno: %d varattno: %d", var->varno, var->varattno);
+
+        forboth(lc1, attnomap->attno, lc2, attnomap->attnosyn)
+        {
+            int    attno = lfirst_int(lc1);
+            int    attnosyn = lfirst_int(lc2);
+
+            if (var->varattno == attnosyn)
+            {
+                elog(DEBUG1, "loc: %d rewrite varattno from: %d to %d", var->location, attnosyn, attno);
+                var->varattno = attno;
+            }
+        }
+    }
+    return expression_tree_walker(node, attno_map_walker, (void *) context);
+}
+
 /* -----------------
  * ExecEndWindowAgg
  * -----------------
@@ -2691,6 +2849,8 @@ ExecEndWindowAgg(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)
@@ -2740,6 +2900,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)
@@ -3080,7 +3242,7 @@ are_peers(WindowAggState *winstate, TupleTableSlot *slot1,
  *
  * Returns true if successful, false if no such row
  */
-static bool
+bool
 window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot)
 {
     WindowAggState *winstate = winobj->winstate;
@@ -3420,14 +3582,53 @@ WinGetFuncArgInFrame(WindowObject winobj, int argno,
     WindowAggState *winstate;
     ExprContext *econtext;
     TupleTableSlot *slot;
-    int64        abs_pos;
-    int64        mark_pos;
 
     Assert(WindowObjectIsValid(winobj));
     winstate = winobj->winstate;
     econtext = winstate->ss.ps.ps_ExprContext;
     slot = winstate->temp_slot_1;
 
+    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)
+ */
+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;
+
+    Assert(WindowObjectIsValid(winobj));
+    winstate = winobj->winstate;
+
     switch (seektype)
     {
         case WINDOW_SEEK_CURRENT:
@@ -3583,15 +3784,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;
 }
 
 /*
@@ -3622,3 +3821,9 @@ WinGetFuncArgCurrent(WindowObject winobj, int argno, bool *isnull)
     return ExecEvalExpr((ExprState *) list_nth(winobj->argstates, argno),
                         econtext, isnull);
 }
+
+WindowAggState *
+WinGetAggState(WindowObject winobj)
+{
+    return winobj->winstate;
+}
diff --git a/src/backend/utils/adt/windowfuncs.c b/src/backend/utils/adt/windowfuncs.c
index b87a624fb2..63b225271c 100644
--- a/src/backend/utils/adt/windowfuncs.c
+++ b/src/backend/utils/adt/windowfuncs.c
@@ -13,6 +13,9 @@
  */
 #include "postgres.h"
 
+#include "catalog/pg_collation_d.h"
+#include "executor/executor.h"
+#include "nodes/execnodes.h"
 #include "nodes/supportnodes.h"
 #include "utils/builtins.h"
 #include "windowapi.h"
@@ -39,7 +42,9 @@ typedef struct
 static bool rank_up(WindowObject winobj);
 static Datum leadlag_common(FunctionCallInfo fcinfo,
                             bool forward, bool withoffset, bool withdefault);
-
+static bool get_slots(WindowObject winobj, WindowAggState *winstate, int current_pos);
+static int evaluate_pattern(WindowObject winobj, WindowAggState *winstate,
+                            int relpos, char *vname, char *quantifier, bool *result);
 
 /*
  * utility routine for *_rank functions.
@@ -713,3 +718,270 @@ window_nth_value(PG_FUNCTION_ARGS)
 
     PG_RETURN_DATUM(result);
 }
+
+/*
+ * rpr
+ * allow to use "Row pattern recognition: WINDOW clause" (SQL:2016 R020) in
+ * the target list.
+ * Usage: SELECT rpr(colname) OVER (..)
+ * where colname is defined in PATTERN clause.
+ */
+Datum
+window_rpr(PG_FUNCTION_ARGS)
+{
+#define    MAX_PATTERNS    16    /* max variables in PATTERN clause */
+
+    WindowObject winobj = PG_WINDOW_OBJECT();
+    WindowAggState    *winstate = WinGetAggState(winobj);
+    Datum        result;
+    bool        expression_result;
+    bool        isnull;
+    int            relpos;
+    int64        curr_pos, markpos;
+    ListCell    *lc, *lc1;
+
+    curr_pos = WinGetCurrentPosition(winobj);
+    elog(DEBUG1, "rpr is called. row: " INT64_FORMAT, curr_pos);
+
+    /*
+     * Evaluate PATTERN until one of expressions is not true or out of frame.
+     */
+    relpos = 0;
+
+    forboth(lc, winstate->patternVariableList, lc1, winstate->patternRegexpList)
+    {
+        char    *vname = strVal(lfirst(lc));
+        char    *quantifier = strVal(lfirst(lc1));
+
+        elog(DEBUG1, "relpos: %d pattern vname: %s quantifier: %s", relpos, vname, quantifier);
+
+        /* evaluate row pattern against current row */
+        relpos = evaluate_pattern(winobj, winstate, relpos, vname, quantifier, &expression_result);
+
+        /*
+         * If the expression did not match, we are done.
+         */
+        if (!expression_result)
+            break;
+
+        /* out of frame? */
+        if (relpos < 0)
+            break;
+
+        /* count up relative row position */
+        relpos++;
+    }
+
+    elog(DEBUG1, "relpos: %d", relpos);
+
+    /*
+     * If current row satified the pattern, return argument expression.
+     */
+    if (expression_result)
+    {
+        result = WinGetFuncArgInFrame(winobj, 0,
+                                      0, WINDOW_SEEK_HEAD, false,
+                                      &isnull, NULL);
+    }
+
+    /*
+     * At this point we can set mark down to current pos -2.
+     */
+    markpos = curr_pos -2;
+    elog(DEBUG1, "markpos: " INT64_FORMAT, markpos);
+    if (markpos >= 0)
+        WinSetMarkPosition(winobj, markpos);
+
+    if (expression_result)
+        PG_RETURN_DATUM(result);
+
+    PG_RETURN_NULL();
+}
+
+/*
+ * Evaluate expression associated with PATTERN variable vname.
+ * relpos is relative row position in a frame (starting from 0).
+ * "quantifier" is the quatifier part of the PATTERN regular expression.
+ * Currently only '+' is allowed.
+ * result is out paramater representing the expression evaluation result
+ * is true of false.
+ * Return values are:
+ * >=0: the last match relative row position
+ * -1: current row is out of frame
+ */
+static
+int evaluate_pattern(WindowObject winobj, WindowAggState *winstate,
+                     int relpos, char *vname, char *quantifier, bool *result)
+{
+    ExprContext    *econtext = winstate->ss.ps.ps_ExprContext;
+    ListCell    *lc1, *lc2;
+    ExprState    *pat;
+    Datum        eval_result;
+    int            sts;
+    bool        out_of_frame = false;
+    bool        isnull;
+    StringInfo    encoded_str = makeStringInfo();
+    char        pattern_str[128];
+
+    forboth (lc1, winstate->defineVariableList, lc2, winstate->defineClauseList)
+    {
+        char    *name = strVal(lfirst(lc1));
+        bool    second_try_match = false;
+
+        if (strcmp(vname, name))
+            continue;
+
+        /* set expression to evaluate */
+        pat = lfirst(lc2);
+
+        for (;;)
+        {
+            if (!get_slots(winobj, winstate, relpos))
+            {
+                out_of_frame = true;
+                break;    /* current row is out of frame */
+            }
+
+            /* evaluate the expression */
+            eval_result = ExecEvalExpr(pat, econtext, &isnull);
+            if (isnull)
+            {
+                /* expression is NULL */
+                elog(DEBUG1, "expression for %s is NULL at row: %d", vname, relpos);
+                break;
+            }
+            else
+            {
+                if (!DatumGetBool(eval_result))
+                {
+                    /* expression is false */
+                    elog(DEBUG1, "expression for %s is false at row: %d", vname, relpos);
+                    break;
+                }
+                else
+                {
+                    /* expression is true */
+                    elog(DEBUG1, "expression for %s is true at row: %d", vname, relpos);
+                    appendStringInfoChar(encoded_str, vname[0]);
+
+                    /* If quantifier is "+", we need to look for more matching row */
+                    if (quantifier && !strcmp(quantifier, "+"))
+                    {
+                        /* remember that we want to try another row */
+                        second_try_match = true;
+                        relpos++;
+                    }
+                    else
+                        break;
+                }
+            }
+        }
+        if (second_try_match)
+            relpos--;
+
+        if (out_of_frame)
+        {
+            *result = false;
+            return -1;
+        }
+
+        /* build regular expression */
+        snprintf(pattern_str, sizeof(pattern_str), "%c%s", vname[0], quantifier);
+
+        /*
+         * Do regular expression matching against sequence of rows satisfying
+         * the expression using regexp_instr().
+         */
+        sts = DatumGetInt32(DirectFunctionCall2Coll(regexp_instr, DEFAULT_COLLATION_OID,
+                                                    PointerGetDatum(cstring_to_text(encoded_str->data)),
+                                                    PointerGetDatum(cstring_to_text(pattern_str))));
+        elog(DEBUG1, "regexp_instr returned: %d. str: %s regexp: %s",
+             sts, encoded_str->data, pattern_str);
+        *result = (sts > 0)? true : false;
+    }
+    return relpos;
+}
+
+/*
+ * Get current, previous and next tuple.
+ * Returns true if still within frame.
+ */
+static bool
+get_slots(WindowObject winobj, WindowAggState *winstate, int current_pos)
+{
+    TupleTableSlot *slot;
+    bool    isnull, isout;
+    int        sts;
+    ExprContext *econtext;
+
+    econtext = winstate->ss.ps.ps_ExprContext;
+
+    /* for current row */
+    slot = winstate->temp_slot_1;
+    sts = WinGetSlotInFrame(winobj, slot,
+                            current_pos, WINDOW_SEEK_HEAD, false,
+                            &isnull, &isout);
+    if (sts < 0)
+    {
+        elog(DEBUG1, "current row is out of frame");
+        econtext->ecxt_scantuple = winstate->null_slot;
+        return false;
+    }
+    else
+        econtext->ecxt_scantuple = slot;
+
+    /* for PREV */
+    if (current_pos > 0)
+    {
+        slot = winstate->prev_slot;
+        sts = WinGetSlotInFrame(winobj, slot,
+                                current_pos - 1, WINDOW_SEEK_HEAD, false,
+                                &isnull, &isout);
+        if (sts < 0)
+        {
+            elog(DEBUG1, "previous row out of frame at: %d", current_pos);
+            econtext->ecxt_outertuple = winstate->null_slot;
+        }
+        econtext->ecxt_outertuple = slot;
+    }
+    else
+        econtext->ecxt_outertuple = winstate->null_slot;
+
+    /* for NEXT */
+    slot = winstate->next_slot;
+    sts = WinGetSlotInFrame(winobj, slot,
+                            current_pos + 1, WINDOW_SEEK_HEAD, false,
+                            &isnull, &isout);
+    if (sts < 0)
+    {
+        elog(DEBUG1, "next row out of frame at: %d", current_pos);
+        econtext->ecxt_innertuple = winstate->null_slot;
+    }
+    else
+        econtext->ecxt_innertuple = slot;
+
+    return true;
+}
+
+/*
+ * 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 6996073989..e3a9e0ffeb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10397,6 +10397,15 @@
 { oid => '3114', descr => 'fetch the Nth row value',
   proname => 'nth_value', prokind => 'w', prorettype => 'anyelement',
   proargtypes => 'anyelement int4', prosrc => 'window_nth_value' },
+{ oid => '6122', descr => 'row pattern recognition in window',
+  proname => 'rpr', prokind => 'w', prorettype => 'anyelement',
+  proargtypes => 'anyelement', prosrc => 'window_rpr' },
+{ oid => '6123', descr => 'previous value',
+  proname => 'prev', provolatile => 's', prorettype => 'anyelement',
+  proargtypes => 'anyelement', prosrc => 'window_prev' },
+{ oid => '6124', 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 cb714f4a19..bc95c5fe9b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -2519,6 +2519,13 @@ 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: */
+    List       *patternVariableList;    /* list of row pattern variables names (list of String) */
+    List       *patternRegexpList;    /* list of row pattern regular expressions ('*', '+' or '?'. list of String) */
+    List       *defineVariableList;    /* list of row pattern definition variables (list of String) */
+    List       *defineClauseList;    /* expression for row pattern definition
+                                     * search conditions ExprState list */
+
     MemoryContext partcontext;    /* context for partition-lifespan data */
     MemoryContext aggcontext;    /* shared context for aggregate working data */
     MemoryContext curaggcontext;    /* current aggregate's working data */
@@ -2555,6 +2562,11 @@ 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 */
 } WindowAggState;
 
 /* ----------------
diff --git a/src/include/windowapi.h b/src/include/windowapi.h
index b8c2c565d1..a0facf38fe 100644
--- a/src/include/windowapi.h
+++ b/src/include/windowapi.h
@@ -58,7 +58,16 @@ extern Datum WinGetFuncArgInFrame(WindowObject winobj, int argno,
                                   int relpos, int seektype, bool set_mark,
                                   bool *isnull, bool *isout);
 
+extern int WinGetSlotInFrame(WindowObject winobj, TupleTableSlot *slot,
+                             int relpos, int seektype, bool set_mark,
+                             bool *isnull, bool *isout);
+
 extern Datum WinGetFuncArgCurrent(WindowObject winobj, int argno,
                                   bool *isnull);
 
+extern WindowAggState *WinGetAggState(WindowObject winobj);
+
+extern bool window_gettupleslot(WindowObject winobj, int64 pos, TupleTableSlot *slot);
+
+
 #endif                            /* WINDOWAPI_H */
-- 
2.25.1

From 02c30e83bfc1e274df76a852620478ad858f5f29 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Sun, 25 Jun 2023 20:48:14 +0900
Subject: [PATCH v1 5/7] Row pattern recognition patch (docs).

---
 doc/src/sgml/advanced.sgml   | 51 ++++++++++++++++++++++++++
 doc/src/sgml/func.sgml       | 69 ++++++++++++++++++++++++++++++++++++
 doc/src/sgml/ref/select.sgml | 18 ++++++++--
 3 files changed, 136 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/advanced.sgml b/doc/src/sgml/advanced.sgml
index 755c9f1485..23ee285b40 100644
--- a/doc/src/sgml/advanced.sgml
+++ b/doc/src/sgml/advanced.sgml
@@ -537,6 +537,57 @@ WHERE pos < 3;
     <literal>rank</literal> less than 3.
    </para>
 
+   <para>
+    Window function <function>rpr</function> can be used with row pattern
+    common syntax to perform row pattern recognition in a query. Row pattern
+    common syntax includes two sub clauses. <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>. Moreover if the expression comprises a column
+    reference, it must be the argument of <function>rpr</function>. 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 price column in the previous
+    row if it's called in a context of row pattern recognition. So in the
+    second line means the definition variable "UP" is <literal>TRUE</literal>
+    when price column in the current row is greater than the price column in
+    the previous row. Likewise, "DOWN" is <literal>TRUE</literal> when when
+    price column in the current row is lower than the price 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
+    certain conditions.  For example following <literal>PATTERN</literal>
+    defines that a row starts with the condition "LOWPRICE", then one or more
+    rows satisfy "UP" and finally one or more rows satisfy "DOWN". If a
+    sequence of rows found, rpr returns the column at the starting row.
+    Example of a <literal>SELECT</literal> using the <literal>DEFINE</literal>
+    and <literal>PATTERN</literal> clause is as follows.
+
+<programlisting>    
+SELECT company, tdate, price, rpr(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)
+);
+</programlisting>
+   </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
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 5a47ce4343..8069c58ca5 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -21648,6 +21648,22 @@ SELECT count(*) FROM sometable;
         returns <literal>NULL</literal> if there is no such row.
        </para></entry>
       </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>rpr</primary>
+        </indexterm>
+        <function>rpr</function> ( <parameter>value</parameter> <type>anyelement</type> )
+        <returnvalue>anyelement</returnvalue>
+       </para>
+       <para>
+        Perform row pattern recognition using column specified
+        by <parameter>value</parameter> and returns the value of the column if
+        current row is the first matching row;
+        returns <literal>NULL</literal> otherwise.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -21687,6 +21703,59 @@ SELECT count(*) FROM sometable;
    Other frame specifications can be used to obtain other effects.
   </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>RESPECT NULLS</literal> or
diff --git a/doc/src/sgml/ref/select.sgml b/doc/src/sgml/ref/select.sgml
index 0ee0cc7e64..c0fc16d773 100644
--- a/doc/src/sgml/ref/select.sgml
+++ b/doc/src/sgml/ref/select.sgml
@@ -966,8 +966,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>
@@ -1074,6 +1074,20 @@ 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>
+PATTERN <replaceable class="parameter">pattern_variable_name</replaceable>[+] [, ...]
+DEFINE <replaceable class="parameter">definition_varible_name</replaceable> AS <replaceable
class="parameter">expression</replaceable>[, ...]
 
+</synopsis>    
+   </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
-- 
2.25.1

From 4f4ced92392b32c810ab04bf42cf130fa8148c23 Mon Sep 17 00:00:00 2001
From: Tatsuo Ishii <ishii@postgresql.org>
Date: Sun, 25 Jun 2023 20:48:14 +0900
Subject: [PATCH v1 6/7] Row pattern recognition patch (tests).

---
 src/test/regress/expected/rpr.out  | 273 +++++++++++++++++++++++++++++
 src/test/regress/parallel_schedule |   2 +-
 src/test/regress/sql/rpr.sql       | 150 ++++++++++++++++
 3 files changed, 424 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 0000000000..9ede60ba39
--- /dev/null
+++ b/src/test/regress/expected/rpr.out
@@ -0,0 +1,273 @@
+--
+-- 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, rpr(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 | rpr  
+----------+------------+-------+------
+ company1 | 07-01-2023 |   100 |  100
+ company1 | 07-02-2023 |   200 |     
+ company1 | 07-03-2023 |   150 |     
+ company1 | 07-04-2023 |   140 |  140
+ company1 | 07-05-2023 |   150 |     
+ company1 | 07-06-2023 |    90 |   90
+ company1 | 07-07-2023 |   110 |  110
+ company1 | 07-08-2023 |   130 |     
+ company1 | 07-09-2023 |   120 |     
+ company1 | 07-10-2023 |   130 |     
+ company2 | 07-01-2023 |    50 |   50
+ company2 | 07-02-2023 |  2000 |     
+ company2 | 07-03-2023 |  1500 |     
+ company2 | 07-04-2023 |  1400 | 1400
+ company2 | 07-05-2023 |  1500 |     
+ company2 | 07-06-2023 |    60 |   60
+ company2 | 07-07-2023 |  1100 | 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, rpr(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 | rpr 
+----------+------------+-------+-----
+ company1 | 07-01-2023 |   100 | 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
+ 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
+ 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
+ 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, rpr(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 | rpr 
+----------+------------+-------+-----
+ company1 | 07-01-2023 |   100 | 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
+ 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, rpr(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 | rpr  
+----------+------------+-------+------
+ company1 | 07-01-2023 |   100 |  100
+ company1 | 07-02-2023 |   200 |     
+ company1 | 07-03-2023 |   150 |     
+ company1 | 07-04-2023 |   140 |  140
+ company1 | 07-05-2023 |   150 |     
+ company1 | 07-06-2023 |    90 |     
+ company1 | 07-07-2023 |   110 |  110
+ company1 | 07-08-2023 |   130 |     
+ company1 | 07-09-2023 |   120 |     
+ company1 | 07-10-2023 |   130 |     
+ company2 | 07-01-2023 |    50 |   50
+ company2 | 07-02-2023 |  2000 |     
+ company2 | 07-03-2023 |  1500 |     
+ company2 | 07-04-2023 |  1400 | 1400
+ company2 | 07-05-2023 |  1500 |     
+ company2 | 07-06-2023 |    60 |     
+ company2 | 07-07-2023 |  1100 | 1100
+ company2 | 07-08-2023 |  1300 |     
+ company2 | 07-09-2023 |  1200 |     
+ company2 | 07-10-2023 |  1300 |     
+(20 rows)
+
+-- row pattern definition variable name must not appear more than once
+SELECT company, tdate, price, rpr(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),
+  UP AS price > PREV(price)
+);
+ERROR:  row pattern definition variable name "up" appears more than once in DEFINE clause
+LINE 9:   UP AS price > PREV(price),
+          ^
+-- pattern variable name must appear in DEFINE
+SELECT company, tdate, price, rpr(price) OVER w FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+ END)
+ DEFINE
+  START AS TRUE,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+ERROR:  syntax error at or near "END"
+LINE 6:  PATTERN (START UP+ DOWN+ END)
+                                  ^
+-- FRAME must start at current row when row patttern recognition is used
+SELECT company, tdate, price, rpr(price) OVER w FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ 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 patttern recognition is used
+-- AFTER MATCH SKIP TO PAST LAST ROW is not supported
+SELECT company, tdate, price, rpr(price) OVER w FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO PAST LAST ROW
+ INITIAL
+ PATTERN (START UP+ DOWN+)
+ DEFINE
+  START AS TRUE,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+ERROR:  syntax error at or near "PAST"
+LINE 5:  AFTER MATCH SKIP TO PAST LAST ROW
+                             ^
+-- SEEK is not supported
+SELECT company, tdate, price, rpr(price) OVER w FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ 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 6:  SEEK
+         ^
+HINT:  Use INITIAL.
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index cf46fa3359..ebb741318a 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -98,7 +98,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
windowxmlmap functional_deps advisory_lock indirect_toast equivclass
 
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data
windowxmlmap functional_deps advisory_lock indirect_toast equivclass 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 0000000000..921a9fcdfa
--- /dev/null
+++ b/src/test/regress/sql/rpr.sql
@@ -0,0 +1,150 @@
+--
+-- 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, rpr(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)
+);
+
+-- the first row start with less than or equal to 100
+SELECT company, tdate, price, rpr(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, rpr(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, rpr(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)
+);
+
+-- row pattern definition variable name must not appear more than once
+SELECT company, tdate, price, rpr(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),
+  UP AS price > PREV(price)
+);
+
+-- pattern variable name must appear in DEFINE
+SELECT company, tdate, price, rpr(price) OVER w FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ INITIAL
+ PATTERN (START UP+ DOWN+ END)
+ DEFINE
+  START AS TRUE,
+  UP AS price > PREV(price),
+  DOWN AS price < PREV(price)
+);
+
+-- FRAME must start at current row when row patttern recognition is used
+SELECT company, tdate, price, rpr(price) OVER w FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ 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)
+);
+
+-- AFTER MATCH SKIP TO PAST LAST ROW is not supported
+SELECT company, tdate, price, rpr(price) OVER w FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+ AFTER MATCH SKIP TO PAST LAST 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, rpr(price) OVER w FROM stock
+ WINDOW w AS (
+ PARTITION BY company
+ 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)
+);
-- 
2.25.1


В списке pgsql-hackers по дате отправления:

Предыдущее
От: "Joel Jacobson"
Дата:
Сообщение: Re: Do we want a hashset type?
Следующее
От: Ranier Vilela
Дата:
Сообщение: Speeding Up Bitmapset