From b6830fa69422ccf1d48f3d0fed649a4ea2f38026 Mon Sep 17 00:00:00 2001
From: "okbob@github.com" <pavel.stehule@gmail.com>
Date: Tue, 19 Nov 2024 08:14:53 +0100
Subject: [PATCH 21/21] variable fence syntax support and variable fence usage
 guard support

this patch introduces a concept of variable fence - syntax for variable
reference `VARIABLE(varname)` that is not in collision with column reference.
When variable fence usage guard warning is active, then usage variable
without variable fence in the case, where there can be column references,
the the warning is raised.

initial implementation of variable fence
---
 src/backend/nodes/nodeFuncs.c                 |  6 ++
 src/backend/parser/gram.y                     | 28 ++++++-
 src/backend/parser/parse_expr.c               | 74 ++++++++++++++++++-
 src/backend/parser/parse_target.c             |  7 ++
 src/backend/utils/adt/ruleutils.c             | 12 ++-
 src/backend/utils/misc/guc_tables.c           |  9 +++
 src/backend/utils/misc/postgresql.conf.sample |  1 +
 src/include/nodes/parsenodes.h                | 10 +++
 src/include/nodes/primnodes.h                 |  2 +
 src/include/parser/parse_expr.h               |  1 +
 .../regress/expected/session_variables.out    | 56 ++++++++++++++
 src/test/regress/sql/session_variables.sql    | 35 +++++++++
 12 files changed, 234 insertions(+), 7 deletions(-)

diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c
index 8fc67f08d76..d9288cefc79 100644
--- a/src/backend/nodes/nodeFuncs.c
+++ b/src/backend/nodes/nodeFuncs.c
@@ -1662,6 +1662,9 @@ exprLocation(const Node *expr)
 		case T_ParamRef:
 			loc = ((const ParamRef *) expr)->location;
 			break;
+		case T_VariableFence:
+			loc = ((const VariableFence *) expr)->location;
+			break;
 		case T_A_Const:
 			loc = ((const A_Const *) expr)->location;
 			break;
@@ -4681,6 +4684,9 @@ raw_expression_tree_walker_impl(Node *node,
 					return true;
 			}
 			break;
+		case T_VariableFence:
+			/* we assume the fields contain nothing interesting */
+			break;
 		default:
 			elog(ERROR, "unrecognized node type: %d",
 				 (int) nodeTag(node));
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index af9d640c0e2..ad66eb450b4 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -519,7 +519,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <node>	def_arg columnElem where_clause where_or_current_clause
 				a_expr b_expr c_expr AexprConst indirection_el opt_slice_bound
 				columnref in_expr having_clause func_table xmltable array_expr
-				OptWhereClause operator_def_arg
+				OptWhereClause operator_def_arg variable_fence
 %type <list>	opt_column_and_period_list
 %type <list>	rowsfrom_item rowsfrom_list opt_col_def_list
 %type <boolean> opt_ordinality opt_without_overlaps
@@ -879,7 +879,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
  */
 %nonassoc	UNBOUNDED NESTED /* ideally would have same precedence as IDENT */
 %nonassoc	IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP
-			SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH
+			SET KEYS OBJECT_P SCALAR VALUE_P WITH WITHOUT PATH VARIABLE
 %left		Op OPERATOR		/* multi-character ops and user-defined operators */
 %left		'+' '-'
 %left		'*' '/' '%'
@@ -15656,6 +15656,19 @@ c_expr:		columnref								{ $$ = $1; }
 					else
 						$$ = $2;
 				}
+			| variable_fence opt_indirection
+				{
+					if ($2)
+					{
+						A_Indirection *n = makeNode(A_Indirection);
+
+						n->arg = (Node *) $1;
+						n->indirection = check_indirection($2, yyscanner);
+						$$ = (Node *) n;
+					}
+					else
+						$$ = $1;
+				}
 			| case_expr
 				{ $$ = $1; }
 			| func_expr
@@ -17042,6 +17055,17 @@ case_arg:	a_expr									{ $$ = $1; }
 			| /*EMPTY*/								{ $$ = NULL; }
 		;
 
+variable_fence:
+			VARIABLE '(' any_name ')'
+				{
+					VariableFence *vf = makeNode(VariableFence);
+
+					vf->varname = $3;
+					vf->location = @3;
+					$$ = (Node *) vf;
+				}
+		;
+
 columnref:	ColId
 				{
 					$$ = makeColumnRef($1, NIL, @1, yyscanner);
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 16bf52c9854..fecf22b8b9e 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -46,6 +46,7 @@
 /* GUC parameters */
 bool		Transform_null_equals = false;
 bool		session_variables_ambiguity_warning = false;
+bool		session_variables_use_fence_warning_guard = false;
 
 
 static Node *transformExprRecurse(ParseState *pstate, Node *expr);
@@ -81,6 +82,7 @@ static Node *transformWholeRowRef(ParseState *pstate,
 static Node *transformIndirection(ParseState *pstate, A_Indirection *ind);
 static Node *transformTypeCast(ParseState *pstate, TypeCast *tc);
 static Node *transformCollateClause(ParseState *pstate, CollateClause *c);
+static Node *transformVariableFence(ParseState *pstate, VariableFence *vf);
 static Node *transformJsonObjectConstructor(ParseState *pstate,
 											JsonObjectConstructor *ctor);
 static Node *transformJsonArrayConstructor(ParseState *pstate,
@@ -112,7 +114,7 @@ static Node *make_nulltest_from_distinct(ParseState *pstate,
 										 A_Expr *distincta, Node *arg);
 static Node *makeParamSessionVariable(ParseState *pstate,
 									  Oid varid, Oid typid, int32 typmod, Oid collid,
-									  char *attrname, int location);
+									  char *attrname, bool fenced, int location);
 
 
 /*
@@ -377,6 +379,10 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 			result = transformJsonFuncExpr(pstate, (JsonFuncExpr *) expr);
 			break;
 
+		case T_VariableFence:
+			result = transformVariableFence(pstate, (VariableFence *) expr);
+			break;
+
 		default:
 			/* should not reach here */
 			elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr));
@@ -1030,7 +1036,20 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 
 				node = makeParamSessionVariable(pstate,
 												varid, typid, typmod, collid,
-												attrname, cref->location);
+												attrname, false, cref->location);
+
+				/*
+				 * The variable is not inside variable's fence, so raise warning
+				 * when variable fence guard is active and the query has FROM
+				 * clause.
+				 */
+				if (session_variables_use_fence_warning_guard && pstate->p_rtable)
+					ereport(WARNING,
+							(errcode(ERRCODE_AMBIGUOUS_COLUMN),
+							 errmsg("session variable \"%s\" is not used inside variable fence",
+									NameListToString(cref->fields)),
+							 errdetail("The collision of session variable' names and column names is possible."),
+							 parser_errposition(pstate, cref->location)));
 			}
 		}
 	}
@@ -1073,7 +1092,7 @@ transformColumnRef(ParseState *pstate, ColumnRef *cref)
 static Node *
 makeParamSessionVariable(ParseState *pstate,
 						 Oid varid, Oid typid, int32 typmod, Oid collid,
-						 char *attrname, int location)
+						 char *attrname, bool fenced, int location)
 {
 	Param	   *param;
 
@@ -1081,6 +1100,7 @@ makeParamSessionVariable(ParseState *pstate,
 
 	param->paramkind = PARAM_VARIABLE;
 	param->paramvarid = varid;
+	param->paramvarfenced = fenced;
 	param->paramtype = typid;
 	param->paramtypmod = typmod;
 	param->paramcollid = collid;
@@ -1156,6 +1176,54 @@ transformParamRef(ParseState *pstate, ParamRef *pref)
 	return result;
 }
 
+static Node *
+transformVariableFence(ParseState *pstate, VariableFence *vf)
+{
+	Node	   *result;
+	Oid			varid = InvalidOid;
+	char	   *attrname = NULL;
+	bool		not_unique;
+
+	/* VariableFence can be used only in context when variables are supported */
+	if (!expr_kind_allows_session_variables(pstate->p_expr_kind))
+		ereport(ERROR,
+			(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			 errmsg("session variable reference is not supported here"),
+			 parser_errposition(pstate, vf->location)));
+
+	/* takes an AccessShareLock on the session variable */
+	varid = IdentifyVariable(vf->varname, &attrname, &not_unique, false);
+
+	if (not_unique)
+		ereport(ERROR,
+				(errcode(ERRCODE_AMBIGUOUS_PARAMETER),
+				 errmsg("session variable reference \"%s\" is ambiguous",
+						NameListToString(vf->varname)),
+				 parser_errposition(pstate, vf->location)));
+
+	if (OidIsValid(varid))
+	{
+		Oid			typid;
+		int32		typmod;
+		Oid			collid;
+
+		get_session_variable_type_typmod_collid(varid, &typid, &typmod,
+												&collid);
+
+		result = makeParamSessionVariable(pstate,
+										varid, typid, typmod, collid,
+										attrname, true, vf->location);
+	}
+	else
+		ereport(ERROR,
+				(errcode(ERRCODE_UNDEFINED_OBJECT),
+				 errmsg("session variable \"%s\" doesn't exist",
+						NameListToString(vf->varname)),
+				 parser_errposition(pstate, vf->location)));
+
+	return result;
+}
+
 /* Test whether an a_expr is a plain NULL constant or not */
 static bool
 exprIsNullConstant(Node *arg)
diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c
index 6af6b801ad4..9d4adc0f5e9 100644
--- a/src/backend/parser/parse_target.c
+++ b/src/backend/parser/parse_target.c
@@ -2038,6 +2038,13 @@ FigureColnameInternal(Node *node, char **name)
 						 (int) ((JsonFuncExpr *) node)->op);
 			}
 			break;
+		case T_VariableFence:
+			{
+				/* return last field name */
+				*name = strVal(llast(((VariableFence *) node)->varname));
+				return 2;
+			}
+			break;
 		default:
 			break;
 	}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 4932d87d83f..7b97e1749cf 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -8628,8 +8628,16 @@ get_parameter(Param *param, deparse_context *context)
 	/* translate paramvarid to session variable name */
 	if (param->paramkind == PARAM_VARIABLE)
 	{
-		appendStringInfo(context->buf, "%s",
-						 generate_session_variable_name(param->paramvarid));
+		if (param->paramvarfenced)
+		{
+			appendStringInfo(context->buf, "VARIABLE(%s)",
+							 generate_session_variable_name(param->paramvarid));
+		}
+		else
+		{
+			appendStringInfo(context->buf, "%s",
+							 generate_session_variable_name(param->paramvarid));
+		}
 		return;
 	}
 
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 005f961edbd..3a1cc3667e0 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1614,6 +1614,15 @@ struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
+	{
+		{"session_variables_use_fence_warning_guard", PGC_USERSET, CLIENT_CONN_OTHER,
+			gettext_noop("Raise a warning when variable is not used inside variable fence."),
+			NULL
+		},
+		&session_variables_use_fence_warning_guard,
+		false,
+		NULL, NULL, NULL
+	},
 	{
 		{"default_transaction_read_only", PGC_USERSET, CLIENT_CONN_STATEMENT,
 			gettext_noop("Sets the default read-only status of new transactions."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index f60ee5d2c44..50d71aa12a5 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -716,6 +716,7 @@
 #default_transaction_deferrable = off
 #session_replication_role = 'origin'
 #session_variables_ambiguity_warning = off
+#session_variables_use_fence_warning_guard = off
 #statement_timeout = 0				# in milliseconds, 0 is disabled
 #transaction_timeout = 0			# in milliseconds, 0 is disabled
 #lock_timeout = 0				# in milliseconds, 0 is disabled
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index a4372f184df..97ed845e073 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -312,6 +312,16 @@ typedef struct ParamRef
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } ParamRef;
 
+/*
+ * VariableFence - ensure so fields will be interpretted as a variable
+ */
+typedef struct VariableFence
+{
+	NodeTag		type;
+	List	   *varname;		/* variable name (String nodes) */
+	ParseLoc	location;		/* token location, or -1 if unknown */
+} VariableFence;
+
 /*
  * A_Expr - infix, prefix, and postfix expressions
  */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 6174a5562c5..2e5665ffebd 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -388,6 +388,8 @@ typedef struct Param
 	Oid			paramcollid pg_node_attr(query_jumble_ignore);
 	/* OID of session variable if it is used */
 	Oid			paramvarid;
+	/* true when variable is used inside an fence */
+	bool		paramvarfenced;
 	/* token location, or -1 if unknown */
 	ParseLoc	location;
 } Param;
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index e5aebf99b4a..d1f2e59d2dc 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -18,6 +18,7 @@
 /* GUC parameters */
 extern PGDLLIMPORT bool Transform_null_equals;
 extern PGDLLIMPORT bool session_variables_ambiguity_warning;
+extern PGDLLIMPORT bool session_variables_use_fence_warning_guard;
 
 extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
 
diff --git a/src/test/regress/expected/session_variables.out b/src/test/regress/expected/session_variables.out
index 3e6b73684fd..66f1959a329 100644
--- a/src/test/regress/expected/session_variables.out
+++ b/src/test/regress/expected/session_variables.out
@@ -1863,3 +1863,59 @@ SELECT tv;
 (1 row)
 
 DROP VARIABLE tv;
+SET session_variables_ambiguity_warning TO on;
+SET session_variables_use_fence_warning_guard TO on;
+SET search_path TO 'public';
+CREATE VARIABLE a AS int;
+LET a = 10;
+CREATE TABLE test_table(a int, b int);
+INSERT INTO test_table VALUES(20, 20);
+-- no warning
+SELECT a;
+ a  
+----
+ 10
+(1 row)
+
+-- warning variable is shadowed
+SELECT a, b FROM test_table;
+WARNING:  session variable "a" is shadowed
+LINE 1: SELECT a, b FROM test_table;
+               ^
+DETAIL:  Session variables can be shadowed by columns, routine's variables and routine's arguments with the same name.
+ a  | b  
+----+----
+ 20 | 20
+(1 row)
+
+-- no warning
+SELECT variable(a) FROM test_table;
+ a  
+----
+ 10
+(1 row)
+
+ALTER TABLE test_table DROP COLUMN a;
+-- warning - variable fence is not used
+SELECT a, b FROM test_table;
+WARNING:  session variable "a" is not used inside variable fence
+LINE 1: SELECT a, b FROM test_table;
+               ^
+DETAIL:  The collision of session variable' names and column names is possible.
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+-- no warning
+SELECT variable(a), b FROM test_table;
+ a  | b  
+----+----
+ 10 | 20
+(1 row)
+
+DROP VARIABLE a;
+DROP TABLE test_table;
+SET session_variables_ambiguity_warning TO DEFAULT;
+SET session_variables_use_fence_warning_guard TO DEFAULT;
+SET search_path TO DEFAULT;
diff --git a/src/test/regress/sql/session_variables.sql b/src/test/regress/sql/session_variables.sql
index 68fc0cde44a..45510e2ea54 100644
--- a/src/test/regress/sql/session_variables.sql
+++ b/src/test/regress/sql/session_variables.sql
@@ -1266,3 +1266,38 @@ COMMIT;
 SELECT tv;
 
 DROP VARIABLE tv;
+
+SET session_variables_ambiguity_warning TO on;
+SET session_variables_use_fence_warning_guard TO on;
+SET search_path TO 'public';
+
+CREATE VARIABLE a AS int;
+LET a = 10;
+
+CREATE TABLE test_table(a int, b int);
+
+INSERT INTO test_table VALUES(20, 20);
+
+-- no warning
+SELECT a;
+
+-- warning variable is shadowed
+SELECT a, b FROM test_table;
+
+-- no warning
+SELECT variable(a) FROM test_table;
+
+ALTER TABLE test_table DROP COLUMN a;
+
+-- warning - variable fence is not used
+SELECT a, b FROM test_table;
+
+-- no warning
+SELECT variable(a), b FROM test_table;
+
+DROP VARIABLE a;
+DROP TABLE test_table;
+
+SET session_variables_ambiguity_warning TO DEFAULT;
+SET session_variables_use_fence_warning_guard TO DEFAULT;
+SET search_path TO DEFAULT;
-- 
2.47.0

