diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index c5dccef1e275caac34208d77032897d0a592ee78..9d5a9f76c6f650ced14443c3fcdfe9c92b051ec4 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -696,7 +696,13 @@ get_relation_constraints(PlannerInfo *root, att->attcollation, 0); ntest->nulltesttype = IS_NOT_NULL; - ntest->argisrow = type_is_rowtype(att->atttypid); + + /* + * argisrow=false is correct even for a composite column, + * because attnotnull does not represent a SQL-spec IS NOT + * NULL test in such a case, just IS DISTINCT FROM NULL. + */ + ntest->argisrow = false; result = lappend(result, ntest); } } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 1ec1db9081b22b1bcd703f0d323532912c1f599f..8f21eedac4c5b33b72af5c8d5fa08657d17c8e4b 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -5893,17 +5893,43 @@ get_rule_expr(Node *node, deparse_context *context, if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, '('); get_rule_expr_paren((Node *) ntest->arg, context, true, node); - switch (ntest->nulltesttype) + + /* + * For scalar inputs, we prefer to print as IS [NOT] NULL, + * which is shorter and traditional. If it's a rowtype input + * but we're applying a scalar test, must print IS [NOT] + * DISTINCT FROM NULL to be semantically correct. + */ + if (ntest->argisrow || + !type_is_rowtype(exprType((Node *) ntest->arg))) { - case IS_NULL: - appendStringInfo(buf, " IS NULL"); - break; - case IS_NOT_NULL: - appendStringInfo(buf, " IS NOT NULL"); - break; - default: - elog(ERROR, "unrecognized nulltesttype: %d", - (int) ntest->nulltesttype); + switch (ntest->nulltesttype) + { + case IS_NULL: + appendStringInfo(buf, " IS NULL"); + break; + case IS_NOT_NULL: + appendStringInfo(buf, " IS NOT NULL"); + break; + default: + elog(ERROR, "unrecognized nulltesttype: %d", + (int) ntest->nulltesttype); + } + } + else + { + switch (ntest->nulltesttype) + { + case IS_NULL: + appendStringInfo(buf, " IS NOT DISTINCT FROM NULL"); + break; + case IS_NOT_NULL: + appendStringInfo(buf, " IS DISTINCT FROM NULL"); + break; + default: + elog(ERROR, "unrecognized nulltesttype: %d", + (int) ntest->nulltesttype); + } } if (!PRETTY_PAREN(context)) appendStringInfoChar(buf, ')'); diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 57a6c5ce3741081e46217639e57912b656118b06..bf96148e88423121d397c8f73c9e39c1e86b80de 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1000,8 +1000,16 @@ typedef struct XmlExpr * NullTest represents the operation of testing a value for NULLness. * The appropriate test is performed and returned as a boolean Datum. * - * NOTE: the semantics of this for rowtype inputs are noticeably different - * from the scalar case. We provide an "argisrow" flag to reflect that. + * When argisrow is false, this simply represents a test for the null value. + * + * When argisrow is true, the input expression must yield a rowtype, and + * the node implements "row IS [NOT] NULL" per the SQL standard. This + * includes checking individual fields for NULLness when the row datum + * itself isn't NULL. + * + * NOTE: the combination of a rowtype input and argisrow==false does NOT + * correspond to the SQL notation "row IS [NOT] NULL"; instead, this case + * represents the SQL notation "row IS [NOT] DISTINCT FROM NULL". * ---------------- */ @@ -1015,7 +1023,7 @@ typedef struct NullTest Expr xpr; Expr *arg; /* input expression */ NullTestType nulltesttype; /* IS NULL, IS NOT NULL */ - bool argisrow; /* T if input is of a composite type */ + bool argisrow; /* T to perform field-by-field null checks */ } NullTest; /* diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out index 4679d51ff7d60a19bfb6f11b33f391686d42a01c..1ba4517931ea8e9b0beb846a6b6f9cfdb707887b 100644 --- a/src/test/regress/expected/rowtypes.out +++ b/src/test/regress/expected/rowtypes.out @@ -667,10 +667,10 @@ explain (verbose, costs off) select r, r is null as isnull, r is not null as isnotnull from (values (1,row(1,2)), (1,row(null,null)), (1,null), (null,row(1,2)), (null,row(null,null)), (null,null) ) r(a,b); - QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Values Scan on "*VALUES*" - Output: ROW("*VALUES*".column1, "*VALUES*".column2), (("*VALUES*".column1 IS NULL) AND ("*VALUES*".column2 IS NULL)), (("*VALUES*".column1 IS NOT NULL) AND ("*VALUES*".column2 IS NOT NULL)) + Output: ROW("*VALUES*".column1, "*VALUES*".column2), (("*VALUES*".column1 IS NULL) AND ("*VALUES*".column2 IS NOT DISTINCT FROM NULL)), (("*VALUES*".column1 IS NOT NULL) AND ("*VALUES*".column2 IS DISTINCT FROM NULL)) (2 rows) select r, r is null as isnull, r is not null as isnotnull