nocfbot-0041-Reject-column-less-compound-navigation-in-row-pat.txt

text/plain

Filename: nocfbot-0041-Reject-column-less-compound-navigation-in-row-pat.txt
Type: text/plain
Part: 40
Message: Re: Row pattern recognition
From 8112fe3f2c1bee4fc330df0b12d737abd4d27903 Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Tue, 2 Jun 2026 00:05:05 +0900
Subject: [PATCH 41/68] Reject column-less compound navigation in row pattern
 recognition

A row pattern navigation argument must contain at least one column
reference.  The check was applied only to simple navigation; a compound
form such as PREV(FIRST(1)) took the nesting-validation/flatten branch in
parse_rpr.c's define_walker, which never re-checked has_column_ref.  So
column-less compound navigation -- PREV(FIRST(1)), NEXT(LAST(1 + 2)),
PREV(FIRST(1), 2), and so on -- was silently accepted, while the simple
PREV(1) was correctly rejected.

Apply the same has_column_ref check after flattening a compound
navigation, so both forms are rejected consistently.  A compound form
whose argument references a column, such as PREV(FIRST(price)), is
unaffected.

Add regression tests covering column-less compound navigation with the
offset on the inner nav, the outer nav, both, or neither.

Per a report from a static analysis tool.
---
 src/backend/parser/parse_rpr.c    | 10 +++++
 src/test/regress/expected/rpr.out | 62 +++++++++++++++++++++++++++++++
 src/test/regress/sql/rpr.sql      | 52 ++++++++++++++++++++++++++
 3 files changed, 124 insertions(+)

diff --git a/src/backend/parser/parse_rpr.c b/src/backend/parser/parse_rpr.c
index fa8c375f48b..6f44edfc50b 100644
--- a/src/backend/parser/parse_rpr.c
+++ b/src/backend/parser/parse_rpr.c
@@ -641,6 +641,16 @@ define_walker(Node *node, void *context)
 					nav->offset_arg = inner->offset_arg;
 					nav->arg = inner->arg;
 					flattened = true;
+
+					/*
+					 * The flattened argument must include a column reference,
+					 * just like the simple-nav case below.
+					 */
+					if (!ctx->has_column_ref)
+						ereport(ERROR,
+								errcode(ERRCODE_SYNTAX_ERROR),
+								errmsg("argument of row pattern navigation operation must include at least one column reference"),
+								parser_errposition(ctx->pstate, nav->location));
 				}
 				else if (!outer_phys && inner_phys)
 					ereport(ERROR,
diff --git a/src/test/regress/expected/rpr.out b/src/test/regress/expected/rpr.out
index 8793dda3cc3..550113700a9 100644
--- a/src/test/regress/expected/rpr.out
+++ b/src/test/regress/expected/rpr.out
@@ -1133,6 +1133,68 @@ WINDOW w AS (
 ERROR:  argument of row pattern navigation operation must include at least one column reference
 LINE 7:     DEFINE A AS PREV(1, 1) > 0
                         ^
+-- Compound navigation without a column reference must be rejected too,
+-- consistent with the simple forms above.
+-- PREV(FIRST(1)): compound, constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1)) > 0
+                        ^
+-- NEXT(LAST(1 + 2)): compound, constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS NEXT(LAST(1 + 2)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS NEXT(LAST(1 + 2)) > 0
+                        ^
+-- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2)) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1, 2)) > 0
+                        ^
+-- PREV(FIRST(1), 2): compound, outer offset only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1), 2) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1), 2) > 0
+                        ^
+-- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+);
+ERROR:  argument of row pattern navigation operation must include at least one column reference
+LINE 7:     DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+                        ^
 -- Non-constant offset: column reference as offset
 SELECT price FROM stock
 WINDOW w AS (
diff --git a/src/test/regress/sql/rpr.sql b/src/test/regress/sql/rpr.sql
index e4790f75b0a..0aa17f01e84 100644
--- a/src/test/regress/sql/rpr.sql
+++ b/src/test/regress/sql/rpr.sql
@@ -531,6 +531,58 @@ WINDOW w AS (
     DEFINE A AS PREV(1, 1) > 0
 );
 
+-- Compound navigation without a column reference must be rejected too,
+-- consistent with the simple forms above.
+-- PREV(FIRST(1)): compound, constant only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1)) > 0
+);
+
+-- NEXT(LAST(1 + 2)): compound, constant expression, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS NEXT(LAST(1 + 2)) > 0
+);
+
+-- PREV(FIRST(1, 2)): compound, two-arg inner, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2)) > 0
+);
+
+-- PREV(FIRST(1), 2): compound, outer offset only, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1), 2) > 0
+);
+
+-- PREV(FIRST(1, 2), 3): compound, inner and outer offsets, no column reference
+SELECT price FROM stock
+WINDOW w AS (
+    PARTITION BY company
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    INITIAL
+    PATTERN (A)
+    DEFINE A AS PREV(FIRST(1, 2), 3) > 0
+);
+
 -- Non-constant offset: column reference as offset
 SELECT price FROM stock
 WINDOW w AS (
-- 
2.50.1 (Apple Git-155)