From 8cd3d19c3046ee26e73d7c7831e31cb38ba6c2ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=B8=80=E6=8C=83?= <yizhi.fzh@alibaba-inc.com>
Date: Sun, 19 Jul 2020 10:12:43 +0800
Subject: [PATCH v2] Detect the null type correctly in setop case by peeking
 the later exprs.

In the past, we will get error when issue select NULL union select NULL
union select 1;  that is mainly because we set the expr type to Text after
handling the first 2 expr with some reason. Now before we do that, we
first peak the first not null expr to get the real type we can use, so we can
overcome this issue.
---
 src/backend/parser/analyze.c        | 90 +++++++++++++++++++++++++++++
 src/backend/parser/parse_coerce.c   | 35 +++++++----
 src/backend/parser/parse_node.c     |  3 +
 src/include/parser/parse_node.h     |  2 +
 src/test/regress/expected/union.out | 25 ++++++++
 src/test/regress/sql/union.sql      | 11 ++++
 6 files changed, 155 insertions(+), 11 deletions(-)

diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 401da5dedf..2b835f22f2 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -80,6 +80,9 @@ static Query *transformCallStmt(ParseState *pstate,
 								CallStmt *stmt);
 static void transformLockingClause(ParseState *pstate, Query *qry,
 								   LockingClause *lc, bool pushedDown);
+
+static void preprocessNullTypes(ParseState *pstate, SelectStmt *stmt);
+
 #ifdef RAW_EXPRESSION_COVERAGE_TEST
 static bool test_raw_expression_coverage(Node *node, void *context);
 #endif
@@ -1651,6 +1654,10 @@ transformSetOperationStmt(ParseState *pstate, SelectStmt *stmt)
 	/*
 	 * Recursively transform the components of the tree.
 	 */
+	pstate->set_targetlist_types = palloc0(list_length(leftmostSelect->targetList)
+										   * sizeof(Oid));
+	preprocessNullTypes(pstate, stmt);
+
 	sostmt = castNode(SetOperationStmt,
 					  transformSetOperationTree(pstate, stmt, true, NULL));
 	Assert(sostmt);
@@ -2012,6 +2019,7 @@ transformSetOperationTree(ParseState *pstate, SelectStmt *stmt,
 		op->colTypmods = NIL;
 		op->colCollations = NIL;
 		op->groupClauses = NIL;
+		pstate->exprpos = 0;
 		forboth(ltl, ltargetlist, rtl, rtargetlist)
 		{
 			TargetEntry *ltle = (TargetEntry *) lfirst(ltl);
@@ -2161,6 +2169,7 @@ transformSetOperationTree(ParseState *pstate, SelectStmt *stmt,
 										 false);
 				*targetlist = lappend(*targetlist, restle);
 			}
+			pstate->exprpos += 1;
 		}
 
 		return (Node *) op;
@@ -3011,3 +3020,84 @@ test_raw_expression_coverage(Node *node, void *context)
 }
 
 #endif							/* RAW_EXPRESSION_COVERAGE_TEST */
+
+
+
+/*
+ * parseSelectTargetList
+ *
+ *	Fill the a list of TargetList in each setop query.
+ */
+static void
+parseSelectTargetList(SelectStmt *stmt, List **list_ctx)
+{
+	if (stmt->op == SETOP_NONE)
+		*list_ctx = lappend(*list_ctx,  stmt->targetList);
+	else
+	{
+		parseSelectTargetList(stmt->larg, list_ctx);
+		parseSelectTargetList(stmt->rarg, list_ctx);
+	}
+}
+
+
+/*
+ * getProperNullType
+ *
+ * Get the proper Null exprType for a SetOp Selection targetlist in idx position.
+ * by peaking the later expr.
+ *
+ */
+static Oid
+getProperNullType(ParseState *pstate, List *targetlist_list, int idx)
+{
+	ListCell	*lc;
+	int loops = 0;
+	foreach(lc, targetlist_list)
+	{
+		List *targetlist = lfirst_node(List, lc);
+		Node *node = list_nth_node(ResTarget, targetlist, idx)->val;
+		bool is_null = IsA(node, A_Const)
+			&& nodeTag(&castNode(A_Const, node)->val) == T_Null;
+		if (loops == 0)
+		{
+			if (!is_null)
+				/* The first expr is not null, no need to handle it here */
+				return 0;
+		}
+		else
+		{
+			if (!is_null)
+			{
+				Node *expr = transformExpr(pstate, node, EXPR_KIND_SELECT_TARGET);
+				return exprType(expr);
+			}
+			else /* is_null */
+			{
+				/* do nothing */
+			}
+		}
+		loops++;
+	}
+	return 0;
+}
+
+/*
+ * preprocessNullTypes
+ *
+ */
+static void
+preprocessNullTypes(ParseState *pstate, SelectStmt *stmt)
+{
+	List	*targetlist_list =  NIL;
+	int len, idx;
+
+	parseSelectTargetList(stmt, &targetlist_list);
+	len = list_length(linitial(targetlist_list));
+	for(idx = 0;  idx < len; idx++)
+	{
+		pstate->set_targetlist_types[idx] = getProperNullType(pstate,
+															  targetlist_list,
+															  idx);
+	}
+}
diff --git a/src/backend/parser/parse_coerce.c b/src/backend/parser/parse_coerce.c
index 1b11cf731c..6560828669 100644
--- a/src/backend/parser/parse_coerce.c
+++ b/src/backend/parser/parse_coerce.c
@@ -1373,19 +1373,32 @@ select_common_type(ParseState *pstate, List *exprs, const char *context,
 	}
 
 	/*
-	 * If all the inputs were UNKNOWN type --- ie, unknown-type literals ---
-	 * then resolve as type TEXT.  This situation comes up with constructs
-	 * like SELECT (CASE WHEN foo THEN 'bar' ELSE 'baz' END); SELECT 'foo'
-	 * UNION SELECT 'bar'; It might seem desirable to leave the construct's
-	 * output type as UNKNOWN, but that really doesn't work, because we'd
-	 * probably end up needing a runtime coercion from UNKNOWN to something
-	 * else, and we usually won't have it.  We need to coerce the unknown
-	 * literals while they are still literals, so a decision has to be made
-	 * now.
+	 * If all the inputs were UNKNOWN type --- It may because of unknown-type literals
+	 * or null values.
 	 */
 	if (ptype == UNKNOWNOID)
-		ptype = TEXTOID;
-
+	{
+		if (pstate->set_targetlist_types != NULL &&
+			pstate->set_targetlist_types[pstate->exprpos] != 0)
+			/*			 * This situation comes up with constructs like
+			 * SELECT null UNION select null UNION select 1;
+			 * we set the type based on pstate->set_targetlist_types
+			 *which is set in preprocessNullTypes.
+			 */
+			ptype = pstate->set_targetlist_types[pstate->exprpos];
+		else
+			/*
+			 * Resolve as type TEXT.  This situation comes up with constructs
+			 * like SELECT (CASE WHEN foo THEN 'bar' ELSE 'baz' END); SELECT 'foo'
+			 * UNION SELECT 'bar'; It might seem desirable to leave the construct's
+			 * output type as UNKNOWN, but that really doesn't work, because we'd
+			 * probably end up needing a runtime coercion from UNKNOWN to something
+			 * else, and we usually won't have it.  We need to coerce the unknown
+			 * literals while they are still literals, so a decision has to be made
+			 * now.
+			 */
+			ptype = TEXTOID;
+	}
 	if (which_expr)
 		*which_expr = pexpr;
 	return ptype;
diff --git a/src/backend/parser/parse_node.c b/src/backend/parser/parse_node.c
index 6e98fe55fc..2f29f4241b 100644
--- a/src/backend/parser/parse_node.c
+++ b/src/backend/parser/parse_node.c
@@ -65,6 +65,9 @@ make_parsestate(ParseState *parentParseState)
 		pstate->p_queryEnv = parentParseState->p_queryEnv;
 	}
 
+	pstate->exprpos = 0;
+	pstate->set_targetlist_types = NULL;
+
 	return pstate;
 }
 
diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h
index d25819aa28..e2cfe89f0c 100644
--- a/src/include/parser/parse_node.h
+++ b/src/include/parser/parse_node.h
@@ -220,6 +220,8 @@ struct ParseState
 	ParseParamRefHook p_paramref_hook;
 	CoerceParamHook p_coerce_param_hook;
 	void	   *p_ref_hook_state;	/* common passthrough link for above */
+	int		exprpos;
+	Oid 	*set_targetlist_types;
 };
 
 /*
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index 6e72e92d80..9ed3f25ba1 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1052,3 +1052,28 @@ where (x = 0) or (q1 >= q2 and q1 <= q2);
  4567890123456789 |  4567890123456789 | 1
 (6 rows)
 
+-- Test we should be able to detect the null types correctly.
+select null union all select null union select 1;
+ ?column? 
+----------
+         
+        1
+(2 rows)
+
+select null except select null except select 1;
+ ?column? 
+----------
+(0 rows)
+
+select null intersect select null except select 1 union select 1;
+ ?column? 
+----------
+        1
+         
+(2 rows)
+
+-- The below case still fails which is expected.
+select null intersect select null except select 1 union select 1 union select 'a';
+ERROR:  invalid input syntax for type integer: "a"
+LINE 1: ...select null except select 1 union select 1 union select 'a';
+                                                                   ^
diff --git a/src/test/regress/sql/union.sql b/src/test/regress/sql/union.sql
index 5f4881d594..bc811db27f 100644
--- a/src/test/regress/sql/union.sql
+++ b/src/test/regress/sql/union.sql
@@ -440,3 +440,14 @@ select * from
    union all
    select *, 1 as x from int8_tbl b) ss
 where (x = 0) or (q1 >= q2 and q1 <= q2);
+
+
+-- Test we should be able to detect the null types correctly.
+select null union all select null union select 1;
+
+select null except select null except select 1;
+
+select null intersect select null except select 1 union select 1;
+
+-- The below case still fails which is expected.
+select null intersect select null except select 1 union select 1 union select 'a';
-- 
2.21.0

