diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c index 33610c4040d6b26cdd7eb2397761835516dac78c..6476005f6d5443eff5f1755a104b2935b5315e2e 100644 --- a/contrib/postgres_fdw/deparse.c +++ b/contrib/postgres_fdw/deparse.c @@ -2347,10 +2347,27 @@ deparseNullTest(NullTest *node, deparse_expr_cxt *context) appendStringInfoChar(buf, '('); deparseExpr(node->arg, context); - if (node->nulltesttype == IS_NULL) - appendStringInfoString(buf, " IS NULL)"); + + /* + * 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 (node->argisrow || !type_is_rowtype(exprType((Node *) node->arg))) + { + if (node->nulltesttype == IS_NULL) + appendStringInfoString(buf, " IS NULL)"); + else + appendStringInfoString(buf, " IS NOT NULL)"); + } else - appendStringInfoString(buf, " IS NOT NULL)"); + { + if (node->nulltesttype == IS_NULL) + appendStringInfoString(buf, " IS NOT DISTINCT FROM NULL)"); + else + appendStringInfoString(buf, " IS DISTINCT FROM NULL)"); + } } /* diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 149bd62dca51f36386f3e35c512937d696324ba8..5d18206b91b3527e1e9f8741545b1fefea6cfde0 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -1198,7 +1198,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; ntest->location = -1; result = lappend(result, ntest); } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 2915e217642bc916066ae082096065bfec3c6001..51c765ad8772b2c38e625f5aa5940038bb95860e 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -8047,17 +8047,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: - appendStringInfoString(buf, " IS NULL"); - break; - case IS_NOT_NULL: - appendStringInfoString(buf, " IS NOT NULL"); - break; - default: - elog(ERROR, "unrecognized nulltesttype: %d", - (int) ntest->nulltesttype); + switch (ntest->nulltesttype) + { + case IS_NULL: + appendStringInfoString(buf, " IS NULL"); + break; + case IS_NOT_NULL: + appendStringInfoString(buf, " IS NOT NULL"); + break; + default: + elog(ERROR, "unrecognized nulltesttype: %d", + (int) ntest->nulltesttype); + } + } + else + { + switch (ntest->nulltesttype) + { + case IS_NULL: + appendStringInfoString(buf, " IS NOT DISTINCT FROM NULL"); + break; + case IS_NOT_NULL: + appendStringInfoString(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 057cc2ca85e969c671a22195352f722a904219ed..df2d27d77ca6ca8270dd3a3d85f448fb5e25f022 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1099,8 +1099,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". * ---------------- */ @@ -1114,7 +1122,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 */ int location; /* token location, or -1 if unknown */ } NullTest; diff --git a/src/test/regress/expected/rowtypes.out b/src/test/regress/expected/rowtypes.out index 2971640b4bd28b1cfccc2111b8232e526773ccb8..25b08281c85b781c6176a81caf5e6055aa1f6532 100644 --- a/src/test/regress/expected/rowtypes.out +++ b/src/test/regress/expected/rowtypes.out @@ -664,10 +664,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