From 115425a057767ebf2b6e4c877641ed156b0ed07e Mon Sep 17 00:00:00 2001 From: Henson Choi Date: Tue, 7 Apr 2026 08:53:46 +0900 Subject: [PATCH] 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)