nocfbot-0011-Fix-quote_identifier-deparse.txt

text/plain

Filename: nocfbot-0011-Fix-quote_identifier-deparse.txt
Type: text/plain
Part: 10
Message: Re: Row pattern recognition
From 5a5e04eb3becfc5cd185b67243fe15eff5ef6f56 Mon Sep 17 00:00:00 2001
From: Henson Choi <assam258@gmail.com>
Date: Tue, 7 Apr 2026 08:53:46 +0900
Subject: [PATCH 11/40] Fix quote_identifier() for RPR pattern variable name
 deparse

Add quote_identifier() to PATTERN and DEFINE variable name output
in ruleutils.c and explain.c.  Without quoting, mixed-case or
reserved-word variable names (e.g., "Start", "Up") lose their
case or conflict with keywords in pg_get_viewdef() output,
breaking pg_dump/pg_restore round-trips.

Add regression test with quoted identifiers ("Start", "Up") to
verify correct deparse in both pg_get_viewdef and EXPLAIN output.
---
 src/backend/commands/explain.c            |  2 +-
 src/backend/utils/adt/ruleutils.c         |  4 ++--
 src/test/regress/expected/rpr_base.out    | 24 +++++++++++++++++++++++
 src/test/regress/expected/rpr_explain.out | 19 ++++++++++++++++++
 src/test/regress/sql/rpr_base.sql         | 10 ++++++++++
 src/test/regress/sql/rpr_explain.sql      | 12 ++++++++++++
 6 files changed, 68 insertions(+), 3 deletions(-)

diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7f0367ce546..933eadab71e 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -3176,7 +3176,7 @@ deparse_rpr_var(RPRPattern *pattern, int *idx, StringInfoData *buf,
 		appendStringInfoChar(buf, ' ');
 
 	Assert(elem->varId < pattern->numVars);
-	appendStringInfoString(buf, pattern->varNames[elem->varId]);
+	appendStringInfoString(buf, quote_identifier(pattern->varNames[elem->varId]));
 	append_rpr_quantifier(buf, elem);
 	*needSpace = true;
 
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index cfe24de43cf..c755a42efd6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -7160,7 +7160,7 @@ get_rule_pattern_node(RPRPatternNode *node, deparse_context *context)
 	switch (node->nodeType)
 	{
 		case RPR_PATTERN_VAR:
-			appendStringInfoString(buf, node->varName);
+			appendStringInfoString(buf, quote_identifier(node->varName));
 			append_pattern_quantifier(buf, node);
 			break;
 
@@ -7229,7 +7229,7 @@ get_rule_define(List *defineClause, bool force_colno, deparse_context *context)
 	{
 		TargetEntry *te = (TargetEntry *) lfirst(lc_def);
 
-		appendStringInfo(buf, "%s%s AS ", sep, te->resname);
+		appendStringInfo(buf, "%s%s AS ", sep, quote_identifier(te->resname));
 		get_rule_expr((Node *) te->expr, context, false);
 		sep = ",\n  ";
 	}
diff --git a/src/test/regress/expected/rpr_base.out b/src/test/regress/expected/rpr_base.out
index 7452cf1b3ab..6526365dd6a 100644
--- a/src/test/regress/expected/rpr_base.out
+++ b/src/test/regress/expected/rpr_base.out
@@ -2252,6 +2252,30 @@ SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
    b AS (val > 0) );
 (1 row)
 
+-- Quoted identifier round-trip: mixed case and reserved words need quoting
+CREATE VIEW rpr_serial_quoted AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ("Start" "Up"+)
+             DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
+SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
+                                pg_get_viewdef                                
+------------------------------------------------------------------------------
+  SELECT id,                                                                 +
+     val,                                                                    +
+     count(*) OVER w AS count                                                +
+    FROM rpr_serial                                                          +
+   WINDOW w AS (ORDER BY id ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING +
+   AFTER MATCH SKIP PAST LAST ROW                                            +
+   INITIAL                                                                   +
+   PATTERN ("Start" "Up"+)                                                   +
+   DEFINE                                                                    +
+   "Start" AS true,                                                          +
+   "Up" AS (val > prev(val)) );
+(1 row)
+
 -- Materialized view (if supported)
 CREATE TABLE rpr_mview (id INT, val INT);
 INSERT INTO rpr_mview VALUES (1, 10), (2, 20), (3, 30);
diff --git a/src/test/regress/expected/rpr_explain.out b/src/test/regress/expected/rpr_explain.out
index f66caf8908e..a68ec61e10f 100644
--- a/src/test/regress/expected/rpr_explain.out
+++ b/src/test/regress/expected/rpr_explain.out
@@ -301,6 +301,25 @@ WINDOW w AS (
    ->  Function Scan on generate_series s (actual rows=30.00 loops=1)
 (8 rows)
 
+-- Regression test: Quoted identifiers in EXPLAIN pattern deparse
+-- Mixed case names must be quoted to preserve round-trip safety
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ("Start" "Up"+)
+    DEFINE "Start" AS TRUE, "Up" AS v > PREV(v)
+);');
+                        rpr_explain_filter                         
+-------------------------------------------------------------------
+ WindowAgg
+   Window: w AS (ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)
+   Pattern: "Start" "Up"+
+   ->  Function Scan on generate_series s
+(4 rows)
+
 -- ============================================================
 -- State Statistics Tests (peak, total, merged)
 -- ============================================================
diff --git a/src/test/regress/sql/rpr_base.sql b/src/test/regress/sql/rpr_base.sql
index 8c23c7598a3..3accecb73ba 100644
--- a/src/test/regress/sql/rpr_base.sql
+++ b/src/test/regress/sql/rpr_base.sql
@@ -1559,6 +1559,16 @@ WINDOW w AS (ORDER BY id
              DEFINE A AS val > 0, B AS val > 0);
 SELECT pg_get_viewdef('rpr_quant_reluctant_v'::regclass);
 
+-- Quoted identifier round-trip: mixed case and reserved words need quoting
+CREATE VIEW rpr_serial_quoted AS
+SELECT id, val, count(*) OVER w
+FROM rpr_serial
+WINDOW w AS (ORDER BY id
+             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+             PATTERN ("Start" "Up"+)
+             DEFINE "Start" AS TRUE, "Up" AS val > PREV(val));
+SELECT pg_get_viewdef('rpr_serial_quoted'::regclass);
+
 -- Materialized view (if supported)
 
 CREATE TABLE rpr_mview (id INT, val INT);
diff --git a/src/test/regress/sql/rpr_explain.sql b/src/test/regress/sql/rpr_explain.sql
index 65a775fdad9..703ecd3b23b 100644
--- a/src/test/regress/sql/rpr_explain.sql
+++ b/src/test/regress/sql/rpr_explain.sql
@@ -226,6 +226,18 @@ WINDOW w AS (
     DEFINE A AS v % 5 = 1, B AS v % 5 = 2, C AS v % 5 = 3, D AS v % 5 = 4, E AS v % 5 = 0
 );');
 
+-- Regression test: Quoted identifiers in EXPLAIN pattern deparse
+-- Mixed case names must be quoted to preserve round-trip safety
+SELECT rpr_explain_filter('
+EXPLAIN (COSTS OFF)
+SELECT count(*) OVER w
+FROM generate_series(1, 10) AS s(v)
+WINDOW w AS (
+    ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
+    PATTERN ("Start" "Up"+)
+    DEFINE "Start" AS TRUE, "Up" AS v > PREV(v)
+);');
+
 -- ============================================================
 -- State Statistics Tests (peak, total, merged)
 -- ============================================================
-- 
2.50.1 (Apple Git-155)