Make foo=null a warning by default.

Started by David Fetterover 7 years ago24 messages
#1David Fetter
david@fetter.org
1 attachment(s)

Folks,

Per a discussion with Andrew Gierth and Vik Fearing, both of whom
helped make this happen, please find attached a patch which makes it
possible to get SQL standard behavior for "= NULL", which is an error.
It's been upgraded to a warning, and can still be downgraded to
silence (off) and MS-SQL-compatible behavior (on).

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

Attachments:

0001-Make-foo-null-a-warning-by-default.patchtext/x-diff; charset=us-asciiDownload
From a9ff5dbd9139c83bd6241765f74c3b637059e87b Mon Sep 17 00:00:00 2001
From: David Fetter <david@fetter.org>
Date: Sun, 15 Jul 2018 16:11:08 -0700
Subject: [PATCH] Make foo=null a warning by default
To: pgsql-hackers@postgresql.org

The transform_null_equals GUC is now an enum consisting of on, off, warn (default), and error.
---
 doc/src/sgml/config.sgml                      | 14 +++---
 src/backend/parser/parse_expr.c               | 28 +++++++++---
 src/backend/utils/misc/guc.c                  | 44 +++++++++++++------
 src/backend/utils/misc/postgresql.conf.sample |  2 +-
 src/include/parser/parse_expr.h               | 11 ++++-
 src/test/regress/expected/guc.out             | 30 +++++++++++++
 src/test/regress/expected/inherit.out         |  1 +
 src/test/regress/sql/guc.sql                  | 18 ++++++++
 8 files changed, 122 insertions(+), 26 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e307bb4e8e..58ab2d213d 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8036,7 +8036,7 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
      <variablelist>
 
      <varlistentry id="guc-transform-null-equals" xreflabel="transform_null_equals">
-      <term><varname>transform_null_equals</varname> (<type>boolean</type>)
+      <term><varname>transform_null_equals</varname> (<type>enum</type>)
       <indexterm><primary>IS NULL</primary></indexterm>
       <indexterm>
        <primary><varname>transform_null_equals</varname> configuration parameter</primary>
@@ -8044,15 +8044,19 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </term>
       <listitem>
        <para>
-        When on, expressions of the form <literal><replaceable>expr</replaceable> =
+        When enabled, expressions of the form <literal><replaceable>expr</replaceable> =
         NULL</literal> (or <literal>NULL =
         <replaceable>expr</replaceable></literal>) are treated as
         <literal><replaceable>expr</replaceable> IS NULL</literal>, that is, they
         return true if <replaceable>expr</replaceable> evaluates to the null value,
         and false otherwise. The correct SQL-spec-compliant behavior of
-        <literal><replaceable>expr</replaceable> = NULL</literal> is to always
-        return null (unknown). Therefore this parameter defaults to
-        <literal>off</literal>.
+        <literal><replaceable>expr</replaceable> = <replacable>null_expression</replaceable></literal>
+        is to always return null (unknown); PostgreSQL allows <literal>NULL</literal>
+        as a null-valued expression of unknown type, which is an extension to the spec.
+        The transformation of <literal><replaceable>expr</replaceable> = NULL</literal>
+        to <literal><replaceable>expr</replaceable> IS NULL</literal> is inconsistent
+        with the normal semantics of the SQL specification, and is therefore set to "warn"
+        by default.
        </para>
 
        <para>
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 385e54a9b6..c4f714792e 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -41,8 +41,8 @@
 
 
 /* GUC parameters */
-bool		operator_precedence_warning = false;
-bool		Transform_null_equals = false;
+bool	operator_precedence_warning = false;
+enum	TransformNullEquals transform_null_equals = TRANSFORM_NULL_EQUALS_WARN;
 
 /*
  * Node-type groups for operator precedence warnings
@@ -850,6 +850,7 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	Node	   *lexpr = a->lexpr;
 	Node	   *rexpr = a->rexpr;
 	Node	   *result;
+	bool		need_transform_null_equals = false;
 
 	if (operator_precedence_warning)
 	{
@@ -872,17 +873,32 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	}
 
 	/*
-	 * Special-case "foo = NULL" and "NULL = foo" for compatibility with
+	 * Deal with "foo = NULL" and "NULL = foo" for compatibility with
 	 * standards-broken products (like Microsoft's).  Turn these into IS NULL
 	 * exprs. (If either side is a CaseTestExpr, then the expression was
 	 * generated internally from a CASE-WHEN expression, and
 	 * transform_null_equals does not apply.)
 	 */
-	if (Transform_null_equals &&
-		list_length(a->name) == 1 &&
+	need_transform_null_equals = (list_length(a->name) == 1 &&
 		strcmp(strVal(linitial(a->name)), "=") == 0 &&
 		(exprIsNullConstant(lexpr) || exprIsNullConstant(rexpr)) &&
-		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)))
+		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)));
+
+	/*
+	 * We don't need to handle TRANSFORM_NULL_EQUALS_OFF here because it's a noop
+	 */
+	if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_WARN)
+		ereport(WARNING,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL can only produce a NULL"),
+				 parser_errposition(pstate, a->location)));
+
+	if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ERR)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL does not comply with the SQL specification"),
+				 parser_errposition(pstate, a->location)));
+	if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ON)
 	{
 		NullTest   *n = makeNode(NullTest);
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 17292e04fe..ae76b3d335 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -310,6 +310,23 @@ static const struct config_enum_entry track_function_options[] = {
 	{NULL, 0, false}
 };
 
+/*
+ * Accept the usual variants of boolean-ish, along with warn and error.
+ */
+static const struct config_enum_entry transform_null_equals_options[] = {
+	{"off", TRANSFORM_NULL_EQUALS_OFF, false},
+	{"on", TRANSFORM_NULL_EQUALS_ON, false},
+	{"warn", TRANSFORM_NULL_EQUALS_WARN, false},
+	{"error", TRANSFORM_NULL_EQUALS_ERR, false},
+	{"false", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"no", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"0", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"true", TRANSFORM_NULL_EQUALS_ON, true},
+	{"yes", TRANSFORM_NULL_EQUALS_ON, true},
+	{"1", TRANSFORM_NULL_EQUALS_ON, true},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry xmlbinary_options[] = {
 	{"base64", XMLBINARY_BASE64, false},
 	{"hex", XMLBINARY_HEX, false},
@@ -1407,19 +1424,6 @@ static struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
-	{
-		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
-			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
-			gettext_noop("When turned on, expressions of the form expr = NULL "
-						 "(or NULL = expr) are treated as expr IS NULL, that is, they "
-						 "return true if expr evaluates to the null value, and false "
-						 "otherwise. The correct behavior of expr = NULL is to always "
-						 "return null (unknown).")
-		},
-		&Transform_null_equals,
-		false,
-		NULL, NULL, NULL
-	},
 	{
 		{"db_user_namespace", PGC_SIGHUP, CONN_AUTH_AUTH,
 			gettext_noop("Enables per-database user names."),
@@ -4067,6 +4071,20 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
+			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
+			gettext_noop("When turned on, expressions of the form expr = NULL "
+						 "(or NULL = expr) are treated as expr IS NULL, that is, they "
+						 "return true if expr evaluates to the null value, and false "
+						 "otherwise. The correct behavior of expr = NULL is to always "
+						 "return null (unknown) and issue a warning.")
+		},
+		&transform_null_equals,
+		TRANSFORM_NULL_EQUALS_WARN, transform_null_equals_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Set the level of information written to the WAL."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 657c3f81f8..d7ebeb83b2 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -655,7 +655,7 @@
 
 # - Other Platforms and Clients -
 
-#transform_null_equals = off
+#transform_null_equals = warn
 
 
 #------------------------------------------------------------------------------
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index e5aff61b8f..4b6df4c97a 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -17,10 +17,19 @@
 
 /* GUC parameters */
 extern bool operator_precedence_warning;
-extern bool Transform_null_equals;
 
 extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
 
 extern const char *ParseExprKindName(ParseExprKind exprKind);
 
+typedef enum TransformNullEquals
+{
+	TRANSFORM_NULL_EQUALS_OFF = 0,	/* disabled */
+	TRANSFORM_NULL_EQUALS_ON,		/* enabled */
+	TRANSFORM_NULL_EQUALS_WARN,		/* Issue a warning */
+	TRANSFORM_NULL_EQUALS_ERR		/* Error out */
+} TransformNullEquals;
+
+extern TransformNullEquals transform_null_equals;
+
 #endif							/* PARSE_EXPR_H */
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 43ac5f5f11..561b1b6d96 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -767,3 +767,33 @@ NOTICE:  text search configuration "no_such_config" does not exist
 select func_with_bad_set();
 ERROR:  invalid value for parameter "default_text_search_config": "no_such_config"
 reset check_function_bodies;
+set transform_null_equals = on;
+select 1=null;
+ ?column? 
+----------
+ f
+(1 row)
+
+set transform_null_equals = off;
+select 1=null;
+ ?column? 
+----------
+ 
+(1 row)
+
+set transform_null_equals = warn;
+select 1=null;
+WARNING:  = NULL can only produce a NULL
+LINE 1: select 1=null;
+                ^
+ ?column? 
+----------
+ 
+(1 row)
+
+set transform_null_equals = error;
+select 1=null;
+ERROR:  = NULL does not comply with the SQL specification
+LINE 1: select 1=null;
+                ^
+reset transform_null_equals;
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 4f29d9f891..afbf7cb37b 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1690,6 +1690,7 @@ reset enable_bitmapscan;
 --
 create table cnullparent (f1 int);
 create table cnullchild (check (f1 = 1 or f1 = null)) inherits(cnullparent);
+WARNING:  = NULL can only produce a NULL
 insert into cnullchild values(1);
 insert into cnullchild values(2);
 insert into cnullchild values(null);
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index 23e5029780..29b8c888e2 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -288,3 +288,21 @@ set default_text_search_config = no_such_config;
 select func_with_bad_set();
 
 reset check_function_bodies;
+
+set transform_null_equals = on;
+
+select 1=null;
+
+set transform_null_equals = off;
+
+select 1=null;
+
+set transform_null_equals = warn;
+
+select 1=null;
+
+set transform_null_equals = error;
+
+select 1=null;
+
+reset transform_null_equals;
-- 
2.17.1

#2Heikki Linnakangas
hlinnaka@iki.fi
In reply to: David Fetter (#1)
Re: Make foo=null a warning by default.

On 16/07/18 04:40, David Fetter wrote:

Folks,

Per a discussion with Andrew Gierth and Vik Fearing, both of whom
helped make this happen, please find attached a patch which makes it
possible to get SQL standard behavior for "= NULL", which is an error.
It's been upgraded to a warning, and can still be downgraded to
silence (off) and MS-SQL-compatible behavior (on).

I don't agree with changing the default to 'warn'. "foo = NULL" is
perfectly legal SQL, even if it's not very useful in practice.

The use case for transform_null_equals='warn' that I see is to wean off
an application that uses transform_null_equals='on', to find all the
queries that rely on it. But I'm not too excited about that either. The
documented case for using transform_null_equals='on' is compatibility
with Microsoft Access, and this wouldn't help with that. Besides, it's
easy enough to grep the application or the server log for " = NULL", to
find all such queries.

- Heikki

#3Fabien COELHO
coelho@cri.ensmp.fr
In reply to: David Fetter (#1)
Re: Make foo=null a warning by default.

Hello David,

Per a discussion with Andrew Gierth and Vik Fearing, both of whom
helped make this happen, please find attached a patch which makes it
possible to get SQL standard behavior for "= NULL", which is an error.
It's been upgraded to a warning, and can still be downgraded to
silence (off) and MS-SQL-compatible behavior (on).

That's nice, and good for students, and probably others as well:-)

A few comments:

Patch applies & compiles, "make check" ok.

#backslash_quote = safe_encoding # on, off, or safe_encoding
[...]
#transform_null_equals = warn

I think it would be nice to have the possible values as a comment in
"postgresql.conf".

* Code:

   -bool           operator_precedence_warning = false;
   +bool   operator_precedence_warning = false;

Is this reindentation useful/needed?

  +     parser_errposition(pstate, a->location)));
  +   if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ON)

For consistency, maybe skip a line before the if?

transform_null_equals_options[] = { [...]

I do not really understand the sort order of this array. Maybe it could be
alphabetical, or per value? Or maybe it is standard values and then
synonyms, in which is case maybe a comment on the end of the list.

Guc help text: ISTM that it should spell out the possible values and their
behavior. Maybe something like:

"""
When turned on, turns expr = NULL into expr IS NULL.
With off, warn or error, do not do so and be silent, issue a warning or an error.
The standard behavior is not to perform this transformation.
Default is warn.
"""

Or anything in better English.

* Test

+select 1=null;
+ ?column?

Maybe use as to describe the expected behavior, so that it is
easier to check, and I think that "?column?" looks silly eg:

select 1=null as expect_{true,false}[_and_a_warning/error];

create table cnullchild (check (f1 = 1 or f1 = null)) inherits(cnullparent);
+WARNING: = NULL can only produce a NULL

I'm not sure of this test case. Should it be turned into "is null"?

Maybe the behavior could be retested after the reset?

* Documentation:

The "error" value is not documented.

More generally, The value is said to be an enum, but the list of values is
not listed anywhere in the documentation.

ISTM that the doc would be clearer if for each of the four values of the
parameter the behavior is spelled out.

Maybe "warn" in the doc should be <literal>warn</literal> or something.

--
Fabien.

#4Fabien COELHO
coelho@cri.ensmp.fr
In reply to: Heikki Linnakangas (#2)
Re: Make foo=null a warning by default.

Hello Heikki,

The use case for transform_null_equals='warn' that I see is to wean off an
application that uses transform_null_equals='on', to find all the queries
that rely on it. But I'm not too excited about that either. The documented
case for using transform_null_equals='on' is compatibility with Microsoft
Access, and this wouldn't help with that. Besides, it's easy enough to grep
the application or the server log for " = NULL", to find all such queries.

I'm in favor in having this feature, even if off by default, because I
could enable it and it would help my students when learning SQL in
interactive sessions, where "grep '= *NULL'" cannot be issued.

--
Fabien.

#5Robert Haas
robertmhaas@gmail.com
In reply to: Heikki Linnakangas (#2)
Re: Make foo=null a warning by default.

On Mon, Jul 16, 2018 at 3:49 AM, Heikki Linnakangas <hlinnaka@iki.fi> wrote:

I don't agree with changing the default to 'warn'. "foo = NULL" is perfectly
legal SQL, even if it's not very useful in practice.

+1. I think that will probably generate lots of annoying warnings in
server logs compared to the value it adds. I don't object to having
an optional feature to generate warnings, but I think it should be
something a particular user needs to activate, not something we enable
by default.

--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

#6Tom Lane
tgl@sss.pgh.pa.us
In reply to: Heikki Linnakangas (#2)
Re: Make foo=null a warning by default.

Heikki Linnakangas <hlinnaka@iki.fi> writes:

On 16/07/18 04:40, David Fetter wrote:

Per a discussion with Andrew Gierth and Vik Fearing, both of whom
helped make this happen, please find attached a patch which makes it
possible to get SQL standard behavior for "= NULL", which is an error.
It's been upgraded to a warning, and can still be downgraded to
silence (off) and MS-SQL-compatible behavior (on).

I don't agree with changing the default to 'warn'. "foo = NULL" is
perfectly legal SQL, even if it's not very useful in practice.

I think that there's a very narrow argument to be made that SQL doesn't
allow a NULL literal with context-determined type in this context. But
we decided to generalize that restriction long ago, and suddenly deciding
to enforce it only in this one context makes no sense to me. The idea
that we would ever decide that it's an error seems flat out ridiculous.

TBH I'm not really excited about investing any work in this area at all.
Considering how seldom we hear any questions about transform_null_equals
anymore[1]In a quick search, the most recent discussion I could find was from 2011: /messages/by-id/201110070729.p977Tdcx075504@wwwmaster.postgresql.org Before that it came up maybe once a year or so, but it's just fallen off a cliff since then. I wonder whether Microsoft fixed Access to not need it., I'm wondering if we couldn't just rip the "feature" out
entirely. But to the extent that we want to leave it in place, it's
100% for backwards compatibility, and that is a strong argument against
changing anything about it.

I'm also pretty dubious that issuing a warning here will accomplish much
to help anyone find bugs. There are too many small variants of the same
problem that it will not catch[2]Compare for instance the discussion about bug #6064, /messages/by-id/4DFE481F020000250003E8EA@gw.wicourts.gov A very large fraction of the pre-2011 threads mentioning transform_null_equals are essentially of the form "why didn't/can't transform_null_equals fix this bad code for me?". Each of those cases would also be a case that the proposed warning doesn't catch..

regards, tom lane

[1]: In a quick search, the most recent discussion I could find was from 2011: /messages/by-id/201110070729.p977Tdcx075504@wwwmaster.postgresql.org Before that it came up maybe once a year or so, but it's just fallen off a cliff since then. I wonder whether Microsoft fixed Access to not need it.
2011:
/messages/by-id/201110070729.p977Tdcx075504@wwwmaster.postgresql.org
Before that it came up maybe once a year or so, but it's just fallen
off a cliff since then. I wonder whether Microsoft fixed Access to
not need it.

[2]: Compare for instance the discussion about bug #6064, /messages/by-id/4DFE481F020000250003E8EA@gw.wicourts.gov A very large fraction of the pre-2011 threads mentioning transform_null_equals are essentially of the form "why didn't/can't transform_null_equals fix this bad code for me?". Each of those cases would also be a case that the proposed warning doesn't catch.
/messages/by-id/4DFE481F020000250003E8EA@gw.wicourts.gov
A very large fraction of the pre-2011 threads mentioning
transform_null_equals are essentially of the form "why didn't/can't
transform_null_equals fix this bad code for me?". Each of those cases
would also be a case that the proposed warning doesn't catch.

#7Heikki Linnakangas
hlinnaka@iki.fi
In reply to: Tom Lane (#6)
Re: Make foo=null a warning by default.

On 16/07/18 18:10, Tom Lane wrote:

Heikki Linnakangas <hlinnaka@iki.fi> writes:

On 16/07/18 04:40, David Fetter wrote:

Per a discussion with Andrew Gierth and Vik Fearing, both of whom
helped make this happen, please find attached a patch which makes it
possible to get SQL standard behavior for "= NULL", which is an error.
It's been upgraded to a warning, and can still be downgraded to
silence (off) and MS-SQL-compatible behavior (on).

I don't agree with changing the default to 'warn'. "foo = NULL" is
perfectly legal SQL, even if it's not very useful in practice.

I think that there's a very narrow argument to be made that SQL doesn't
allow a NULL literal with context-determined type in this context. But
we decided to generalize that restriction long ago, and suddenly deciding
to enforce it only in this one context makes no sense to me. The idea
that we would ever decide that it's an error seems flat out ridiculous.

TBH I'm not really excited about investing any work in this area at all.
Considering how seldom we hear any questions about transform_null_equals
anymore[1], I'm wondering if we couldn't just rip the "feature" out
entirely.

Yeah, I was wondering about that too. But Fabien brought up a completely
new use-case for this: people learning SQL. For beginners who don't
understand the behavior of NULLs yet, I can see a warning or error being
useful training wheels. Perhaps a completely new "training_wheels=on"
option, which could check may for many other beginner errors, too, would
be better for that.

- Heikki

#8Tom Lane
tgl@sss.pgh.pa.us
In reply to: Heikki Linnakangas (#7)
Re: Make foo=null a warning by default.

Heikki Linnakangas <hlinnaka@iki.fi> writes:

On 16/07/18 18:10, Tom Lane wrote:

TBH I'm not really excited about investing any work in this area at all.
Considering how seldom we hear any questions about transform_null_equals
anymore[1], I'm wondering if we couldn't just rip the "feature" out
entirely.

Yeah, I was wondering about that too. But Fabien brought up a completely
new use-case for this: people learning SQL. For beginners who don't
understand the behavior of NULLs yet, I can see a warning or error being
useful training wheels. Perhaps a completely new "training_wheels=on"
option, which could check may for many other beginner errors, too, would
be better for that.

Agreed --- but what we'd want that to do seems only vaguely related to
the existing behavior of transform_null_equals. As an example, we
intentionally made transform_null_equals *not* trigger on

CASE x WHEN NULL THEN ...

but a training-wheels warning for that would likely be reasonable.

For that matter, many of the old threads about this are complaining
about nulls that aren't simple literals in the first place. I wonder
whether a training-wheels feature that whined *at runtime* about null
WHERE-qual or case-test results would be more useful than a parser
check.

regards, tom lane

#9David Fetter
david@fetter.org
In reply to: Fabien COELHO (#3)
1 attachment(s)
Re: Make foo=null a warning by default.

On Mon, Jul 16, 2018 at 09:49:14AM +0200, Fabien COELHO wrote:

Hello David,

Per a discussion with Andrew Gierth and Vik Fearing, both of whom
helped make this happen, please find attached a patch which makes it
possible to get SQL standard behavior for "= NULL", which is an error.
It's been upgraded to a warning, and can still be downgraded to
silence (off) and MS-SQL-compatible behavior (on).

That's nice, and good for students, and probably others as well:-)

A few comments:

Patch applies & compiles, "make check" ok.

#backslash_quote = safe_encoding # on, off, or safe_encoding
[...]
#transform_null_equals = warn

Fixed.

I think it would be nice to have the possible values as a comment in
"postgresql.conf".

* Code:

-bool           operator_precedence_warning = false;
+bool   operator_precedence_warning = false;

Is this reindentation useful/needed?

I believe so because it fits with the line just below it.

+     parser_errposition(pstate, a->location)));
+   if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ON)

For consistency, maybe skip a line before the if?

Fixed.

transform_null_equals_options[] = { [...]

I do not really understand the sort order of this array. Maybe it could be
alphabetical, or per value? Or maybe it is standard values and then
synonyms, in which is case maybe a comment on the end of the list.

I've changed it to per value. Is this better?

Guc help text: ISTM that it should spell out the possible values and their
behavior. Maybe something like:

"""
When turned on, turns expr = NULL into expr IS NULL.
With off, warn or error, do not do so and be silent, issue a warning or an error.
The standard behavior is not to perform this transformation.
Default is warn.
"""

Or anything in better English.

I've changed this, and hope it's clearer.

* Test

+select 1=null;
+ ?column?

Maybe use as to describe the expected behavior, so that it is easier to
check, and I think that "?column?" looks silly eg:

select 1=null as expect_{true,false}[_and_a_warning/error];

Changed to more descriptive headers.

create table cnullchild (check (f1 = 1 or f1 = null)) inherits(cnullparent);
+WARNING: = NULL can only produce a NULL

I'm not sure of this test case. Should it be turned into "is null"?

This was just adjusting the existing test output to account for the
new warning. I presume it was phrased that way for a reason.

Maybe the behavior could be retested after the reset?

* Documentation:

The "error" value is not documented.

More generally, The value is said to be an enum, but the list of values is
not listed anywhere in the documentation.

ISTM that the doc would be clearer if for each of the four values of the
parameter the behavior is spelled out.

Maybe "warn" in the doc should be <literal>warn</literal> or something.

Fixed.

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

Attachments:

0001-Make-foo-null-a-warning-by-default.patchtext/x-diff; charset=us-asciiDownload
From 33b9f51b1374ede71a6479d44adfa0588c7fb4c3 Mon Sep 17 00:00:00 2001
From: David Fetter <david@fetter.org>
Date: Sun, 15 Jul 2018 16:11:08 -0700
Subject: [PATCH] Make foo=null a warning by default v2
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="------------2.17.1"

This is a multi-part message in MIME format.
--------------2.17.1
Content-Type: text/plain; charset=UTF-8; format=fixed
Content-Transfer-Encoding: 8bit


The transform_null_equals GUC is now an enum consisting of warn, error, off, on.
---
 doc/src/sgml/config.sgml                      | 20 ++++++---
 src/backend/parser/parse_expr.c               | 30 ++++++++++---
 src/backend/utils/misc/guc.c                  | 44 +++++++++++++------
 src/backend/utils/misc/postgresql.conf.sample |  2 +-
 src/include/parser/parse_expr.h               | 11 ++++-
 src/test/regress/expected/guc.out             | 30 +++++++++++++
 src/test/regress/expected/inherit.out         |  1 +
 src/test/regress/sql/guc.sql                  | 18 ++++++++
 8 files changed, 129 insertions(+), 27 deletions(-)


--------------2.17.1
Content-Type: text/x-patch; name="0001-Make-foo-null-a-warning-by-default.patch"
Content-Transfer-Encoding: 8bit
Content-Disposition: attachment; filename="0001-Make-foo-null-a-warning-by-default.patch"

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e307bb4e8e..62cccc6a2d 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8036,7 +8036,7 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
      <variablelist>

      <varlistentry id="guc-transform-null-equals" xreflabel="transform_null_equals">
-      <term><varname>transform_null_equals</varname> (<type>boolean</type>)
+      <term><varname>transform_null_equals</varname> (<type>enum</type>)
       <indexterm><primary>IS NULL</primary></indexterm>
       <indexterm>
        <primary><varname>transform_null_equals</varname> configuration parameter</primary>
@@ -8044,15 +8044,23 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </term>
       <listitem>
        <para>
-        When on, expressions of the form <literal><replaceable>expr</replaceable> =
+        When enabled, expressions of the form <literal><replaceable>expr</replaceable> =
         NULL</literal> (or <literal>NULL =
         <replaceable>expr</replaceable></literal>) are treated as
         <literal><replaceable>expr</replaceable> IS NULL</literal>, that is, they
         return true if <replaceable>expr</replaceable> evaluates to the null value,
-        and false otherwise. The correct SQL-spec-compliant behavior of
-        <literal><replaceable>expr</replaceable> = NULL</literal> is to always
-        return null (unknown). Therefore this parameter defaults to
-        <literal>off</literal>.
+        and false otherwise.  Valid values are <literal>warn</literal>, <literal>error</literal>, <literal>off</literal>, and <literal>on</literal>.
+       </para>
+
+       <para>
+        The correct SQL-spec-compliant behavior of
+        <literal><replaceable>expr</replaceable> = <replacable>null_expression</replaceable></literal>
+        is to always return null (unknown); PostgreSQL allows <literal>NULL</literal>
+        as a null-valued expression of unknown type, which is an extension to the spec.
+        The transformation of <literal><replaceable>expr</replaceable> = NULL</literal>
+        to <literal><replaceable>expr</replaceable> IS NULL</literal> is inconsistent
+        with the normal semantics of the SQL specification, and is therefore set to "warn"
+        by default.
        </para>

        <para>
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 385e54a9b6..58e3e07382 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -41,8 +41,8 @@


 /* GUC parameters */
-bool		operator_precedence_warning = false;
-bool		Transform_null_equals = false;
+bool	operator_precedence_warning = false;
+enum	TransformNullEquals transform_null_equals = TRANSFORM_NULL_EQUALS_WARN;

 /*
  * Node-type groups for operator precedence warnings
@@ -850,6 +850,7 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	Node	   *lexpr = a->lexpr;
 	Node	   *rexpr = a->rexpr;
 	Node	   *result;
+	bool		need_transform_null_equals = false;

 	if (operator_precedence_warning)
 	{
@@ -872,17 +873,34 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	}

 	/*
-	 * Special-case "foo = NULL" and "NULL = foo" for compatibility with
+	 * Deal with "foo = NULL" and "NULL = foo" for compatibility with
 	 * standards-broken products (like Microsoft's).  Turn these into IS NULL
 	 * exprs. (If either side is a CaseTestExpr, then the expression was
 	 * generated internally from a CASE-WHEN expression, and
 	 * transform_null_equals does not apply.)
 	 */
-	if (Transform_null_equals &&
-		list_length(a->name) == 1 &&
+	need_transform_null_equals = (list_length(a->name) == 1 &&
 		strcmp(strVal(linitial(a->name)), "=") == 0 &&
 		(exprIsNullConstant(lexpr) || exprIsNullConstant(rexpr)) &&
-		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)))
+		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)));
+
+	if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_WARN)
+		ereport(WARNING,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL can only produce a NULL"),
+				 parser_errposition(pstate, a->location)));
+
+	if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ERR)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL does not comply with the SQL specification"),
+				 parser_errposition(pstate, a->location)));
+
+	/*
+	 * We don't need to handle TRANSFORM_NULL_EQUALS_OFF here because it's a noop
+	 */
+
+	if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ON)
 	{
 		NullTest   *n = makeNode(NullTest);

diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 17292e04fe..3f7f5c9b74 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -310,6 +310,23 @@ static const struct config_enum_entry track_function_options[] = {
 	{NULL, 0, false}
 };

+/*
+ * Accept the usual variants of boolean-ish, along with warn and error.
+ */
+static const struct config_enum_entry transform_null_equals_options[] = {
+	{"warn", TRANSFORM_NULL_EQUALS_WARN, false},
+	{"error", TRANSFORM_NULL_EQUALS_ERR, false},
+	{"off", TRANSFORM_NULL_EQUALS_OFF, false},
+	{"false", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"no", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"0", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"on", TRANSFORM_NULL_EQUALS_ON, false},
+	{"true", TRANSFORM_NULL_EQUALS_ON, true},
+	{"yes", TRANSFORM_NULL_EQUALS_ON, true},
+	{"1", TRANSFORM_NULL_EQUALS_ON, true},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry xmlbinary_options[] = {
 	{"base64", XMLBINARY_BASE64, false},
 	{"hex", XMLBINARY_HEX, false},
@@ -1407,19 +1424,6 @@ static struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
-	{
-		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
-			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
-			gettext_noop("When turned on, expressions of the form expr = NULL "
-						 "(or NULL = expr) are treated as expr IS NULL, that is, they "
-						 "return true if expr evaluates to the null value, and false "
-						 "otherwise. The correct behavior of expr = NULL is to always "
-						 "return null (unknown).")
-		},
-		&Transform_null_equals,
-		false,
-		NULL, NULL, NULL
-	},
 	{
 		{"db_user_namespace", PGC_SIGHUP, CONN_AUTH_AUTH,
 			gettext_noop("Enables per-database user names."),
@@ -4067,6 +4071,20 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},

+	{
+		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
+			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
+			gettext_noop("When set to on, expressions of the form expr = NULL "
+						 "(or NULL = expr) are treated as expr IS NULL. When "
+						 "set to off, warn, or error, they are not, with increasing "
+						 "insistence. The default behavior of expr = NULL is to "
+						 "return null (unknown) and issue a warning.")
+		},
+		&transform_null_equals,
+		TRANSFORM_NULL_EQUALS_WARN, transform_null_equals_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Set the level of information written to the WAL."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 657c3f81f8..a498c3aebc 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -655,7 +655,7 @@

 # - Other Platforms and Clients -

-#transform_null_equals = off
+#transform_null_equals = warn	# warn, error, off, or on


 #------------------------------------------------------------------------------
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index e5aff61b8f..276a631daf 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -17,10 +17,19 @@

 /* GUC parameters */
 extern bool operator_precedence_warning;
-extern bool Transform_null_equals;

 extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);

 extern const char *ParseExprKindName(ParseExprKind exprKind);

+typedef enum TransformNullEquals
+{
+	TRANSFORM_NULL_EQUALS_WARN = 0,	/* Issue a warning */
+	TRANSFORM_NULL_EQUALS_ERR,		/* Error out */
+	TRANSFORM_NULL_EQUALS_OFF,		/* Disabled */
+	TRANSFORM_NULL_EQUALS_ON		/* Enabled */
+} TransformNullEquals;
+
+extern TransformNullEquals transform_null_equals;
+
 #endif							/* PARSE_EXPR_H */
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 43ac5f5f11..561b1b6d96 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -767,3 +767,33 @@ NOTICE:  text search configuration "no_such_config" does not exist
 select func_with_bad_set();
 ERROR:  invalid value for parameter "default_text_search_config": "no_such_config"
 reset check_function_bodies;
+set transform_null_equals = on;
+select 1=null;
+ ?column?
+----------
+ f
+(1 row)
+
+set transform_null_equals = off;
+select 1=null;
+ ?column?
+----------
+
+(1 row)
+
+set transform_null_equals = warn;
+select 1=null;
+WARNING:  = NULL can only produce a NULL
+LINE 1: select 1=null;
+                ^
+ ?column?
+----------
+
+(1 row)
+
+set transform_null_equals = error;
+select 1=null;
+ERROR:  = NULL does not comply with the SQL specification
+LINE 1: select 1=null;
+                ^
+reset transform_null_equals;
diff --git a/src/test/regress/expected/inherit.out b/src/test/regress/expected/inherit.out
index 4f29d9f891..afbf7cb37b 100644
--- a/src/test/regress/expected/inherit.out
+++ b/src/test/regress/expected/inherit.out
@@ -1690,6 +1690,7 @@ reset enable_bitmapscan;
 --
 create table cnullparent (f1 int);
 create table cnullchild (check (f1 = 1 or f1 = null)) inherits(cnullparent);
+WARNING:  = NULL can only produce a NULL
 insert into cnullchild values(1);
 insert into cnullchild values(2);
 insert into cnullchild values(null);
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index 23e5029780..29b8c888e2 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -288,3 +288,21 @@ set default_text_search_config = no_such_config;
 select func_with_bad_set();

 reset check_function_bodies;
+
+set transform_null_equals = on;
+
+select 1=null;
+
+set transform_null_equals = off;
+
+select 1=null;
+
+set transform_null_equals = warn;
+
+select 1=null;
+
+set transform_null_equals = error;
+
+select 1=null;
+
+reset transform_null_equals;

--------------2.17.1--


#10David Fetter
david@fetter.org
In reply to: Tom Lane (#8)
Re: Make foo=null a warning by default.

On Mon, Jul 16, 2018 at 11:37:28AM -0400, Tom Lane wrote:

Heikki Linnakangas <hlinnaka@iki.fi> writes:

On 16/07/18 18:10, Tom Lane wrote:

TBH I'm not really excited about investing any work in this area at all.
Considering how seldom we hear any questions about transform_null_equals
anymore[1], I'm wondering if we couldn't just rip the "feature" out
entirely.

Yeah, I was wondering about that too. But Fabien brought up a completely
new use-case for this: people learning SQL. For beginners who don't
understand the behavior of NULLs yet, I can see a warning or error being
useful training wheels. Perhaps a completely new "training_wheels=on"
option, which could check may for many other beginner errors, too, would
be better for that.

Agreed --- but what we'd want that to do seems only vaguely related to
the existing behavior of transform_null_equals. As an example, we
intentionally made transform_null_equals *not* trigger on

CASE x WHEN NULL THEN ...

but a training-wheels warning for that would likely be reasonable.

For that matter, many of the old threads about this are complaining
about nulls that aren't simple literals in the first place. I wonder
whether a training-wheels feature that whined *at runtime* about null
WHERE-qual or case-test results would be more useful than a parser
check.

Am I understanding correctly that this would be looking for bogus
conditions in the plan tree?

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#11Fabien COELHO
coelho@cri.ensmp.fr
In reply to: Heikki Linnakangas (#7)
Re: Make foo=null a warning by default.

Hello,

Yeah, I was wondering about that too. But Fabien brought up a completely new
use-case for this: people learning SQL.

Indeed.

This year, about 30% of my students wrote "= NULL" in a query at least
once. Probably I or a colleague were called to the rescue for help.

So this warning would save me time, which explains why I reviewed the
patch with such enthousiasm:-)

I'm fine with keeping the current behavior as a default.

--
Fabien.

#12Fabien COELHO
coelho@cri.ensmp.fr
In reply to: David Fetter (#9)
Re: Make foo=null a warning by default.

Hello David,

A few comments about this v2.

ISTM that there is quite strong opposition to having "warn" as a default,
so probably you should set if "off"?

transform_null_equals_options[] = { [...]

I do not really understand the sort order of this array. Maybe it could be
alphabetical, or per value? Or maybe it is standard values and then
synonyms, in which is case maybe a comment on the end of the list.

I've changed it to per value. Is this better?

At least, I can see the logic.

Or anything in better English.

I've changed this, and hope it's clearer.

Yep, and more elegant than my proposal.

* Test

+select 1=null;
+ ?column?

Maybe use as to describe the expected behavior, so that it is easier to
check, and I think that "?column?" looks silly eg:

select 1=null as expect_{true,false}[_and_a_warning/error];

Changed to more descriptive headers.

Cannot see it in the patch update?

create table cnullchild (check (f1 = 1 or f1 = null)) inherits(cnullparent);
+WARNING: = NULL can only produce a NULL

I'm not sure of this test case. Should it be turned into "is null"?

This was just adjusting the existing test output to account for the
new warning. I presume it was phrased that way for a reason.

Hmmm. Probably you are right. I think that the test case deserves better
comments, as it is most peculiar. It was added by Tom for testing CHECK
constant NULL expressions simplications.

TRUE OR NULL is TRUE, but FALSE OR NULL is NULL. Why not. Combined with
the fact that NULL is considered ok for a CHECK, this lead to strange but
intentional behavior, such as:

-- this CHECK is always nignored in practice
CREATE TABLE foo (i INT CHECK(i = 1 OR NULL));
INSERT INTO foo(i) VALUES(2); #ᅵACCEPTED

-- but this one is not
CREATE TABLE foo (i INT CHECK(i = 1 OR FALSE));
INSERT INTO foo(i) VALUES(2); #ᅵREFUSED

Can't say I'm thrilled about that, and the added warning looks
appropriate.

--
Fabien.

#13David Fetter
david@fetter.org
In reply to: Fabien COELHO (#12)
1 attachment(s)
Re: Make foo=null a warning by default.

On Tue, Jul 17, 2018 at 08:34:17AM -0400, Fabien COELHO wrote:

Hello David,

A few comments about this v2.

ISTM that there is quite strong opposition to having "warn" as a default, so
probably you should set if "off"?

Done.

I do not really understand the sort order of this array. Maybe it could be
alphabetical, or per value? Or maybe it is standard values and then
synonyms, in which is case maybe a comment on the end of the list.

I've changed it to per value. Is this better?

At least, I can see the logic.

I've now ordered it in what seems like a reasonable way, namely all
the "off" options, then the "on" option.

Or anything in better English.

I've changed this, and hope it's clearer.

Yep, and more elegant than my proposal.

I assure you that you expression yourself in English a good deal
better than I do in Portuguese.

* Test

+select 1=null;
+ ?column?

Maybe use as to describe the expected behavior, so that it is easier to
check, and I think that "?column?" looks silly eg:

select 1=null as expect_{true,false}[_and_a_warning/error];

Changed to more descriptive headers.

Cannot see it in the patch update?

Oops. They should be there now.

create table cnullchild (check (f1 = 1 or f1 = null)) inherits(cnullparent);
+WARNING: = NULL can only produce a NULL

I'm not sure of this test case. Should it be turned into "is null"?

This was just adjusting the existing test output to account for the
new warning. I presume it was phrased that way for a reason.

Hmmm. Probably you are right. I think that the test case deserves better
comments, as it is most peculiar. It was added by Tom for testing CHECK
constant NULL expressions simplications.

TRUE OR NULL is TRUE, but FALSE OR NULL is NULL. Why not. Combined with the
fact that NULL is considered ok for a CHECK, this lead to strange but
intentional behavior, such as:

-- this CHECK is always nignored in practice
CREATE TABLE foo (i INT CHECK(i = 1 OR NULL));
INSERT INTO foo(i) VALUES(2); #�ACCEPTED

-- but this one is not
CREATE TABLE foo (i INT CHECK(i = 1 OR FALSE));
INSERT INTO foo(i) VALUES(2); #�REFUSED

Can't say I'm thrilled about that, and the added warning looks appropriate.

Per request, the warning is off by default, so the warning went away.

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

Attachments:

0001-Make-transform_null_equals-into-an-enum.patchtext/x-diff; charset=us-asciiDownload
From 87d004f2a054a0c0999145aa9b44d97d7951b19c Mon Sep 17 00:00:00 2001
From: David Fetter <david@fetter.org>
Date: Sun, 15 Jul 2018 16:11:08 -0700
Subject: [PATCH] Make transform_null_equals into an enum
To: pgsql-hackers@postgresql.org

The transform_null_equals GUC can now be off, warn, error, or on.
---
 doc/src/sgml/config.sgml                      | 20 ++++++---
 src/backend/parser/parse_expr.c               | 30 ++++++++++---
 src/backend/utils/misc/guc.c                  | 44 +++++++++++++------
 src/backend/utils/misc/postgresql.conf.sample |  2 +-
 src/include/parser/parse_expr.h               | 11 ++++-
 src/test/regress/expected/guc.out             | 30 +++++++++++++
 src/test/regress/sql/guc.sql                  | 18 ++++++++
 7 files changed, 128 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 4d48d93305..b0d951190f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8066,7 +8066,7 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
      <variablelist>
 
      <varlistentry id="guc-transform-null-equals" xreflabel="transform_null_equals">
-      <term><varname>transform_null_equals</varname> (<type>boolean</type>)
+      <term><varname>transform_null_equals</varname> (<type>enum</type>)
       <indexterm><primary>IS NULL</primary></indexterm>
       <indexterm>
        <primary><varname>transform_null_equals</varname> configuration parameter</primary>
@@ -8074,15 +8074,23 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </term>
       <listitem>
        <para>
-        When on, expressions of the form <literal><replaceable>expr</replaceable> =
+        When enabled, expressions of the form <literal><replaceable>expr</replaceable> =
         NULL</literal> (or <literal>NULL =
         <replaceable>expr</replaceable></literal>) are treated as
         <literal><replaceable>expr</replaceable> IS NULL</literal>, that is, they
         return true if <replaceable>expr</replaceable> evaluates to the null value,
-        and false otherwise. The correct SQL-spec-compliant behavior of
-        <literal><replaceable>expr</replaceable> = NULL</literal> is to always
-        return null (unknown). Therefore this parameter defaults to
-        <literal>off</literal>.
+        and false otherwise.  Valid values are <literal>off</literal>, <literal>warn</literal>, <literal>error</literal>, and <literal>on</literal>.
+       </para>
+
+       <para>
+        The correct SQL-spec-compliant behavior of
+        <literal><replaceable>expr</replaceable> = <replacable>null_expression</replaceable></literal>
+        is to always return null (unknown); PostgreSQL allows <literal>NULL</literal>
+        as a null-valued expression of unknown type, which is an extension to the spec.
+        The transformation of <literal><replaceable>expr</replaceable> = NULL</literal>
+        to <literal><replaceable>expr</replaceable> IS NULL</literal> is inconsistent
+        with the normal semantics of the SQL specification, and is therefore set to "off"
+        by default.
        </para>
 
        <para>
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 385e54a9b6..4ee31b3cb3 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -41,8 +41,8 @@
 
 
 /* GUC parameters */
-bool		operator_precedence_warning = false;
-bool		Transform_null_equals = false;
+bool	operator_precedence_warning = false;
+enum	TransformNullEquals transform_null_equals = TRANSFORM_NULL_EQUALS_OFF;
 
 /*
  * Node-type groups for operator precedence warnings
@@ -850,6 +850,7 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	Node	   *lexpr = a->lexpr;
 	Node	   *rexpr = a->rexpr;
 	Node	   *result;
+	bool		need_transform_null_equals = false;
 
 	if (operator_precedence_warning)
 	{
@@ -872,17 +873,34 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	}
 
 	/*
-	 * Special-case "foo = NULL" and "NULL = foo" for compatibility with
+	 * Deal with "foo = NULL" and "NULL = foo" for compatibility with
 	 * standards-broken products (like Microsoft's).  Turn these into IS NULL
 	 * exprs. (If either side is a CaseTestExpr, then the expression was
 	 * generated internally from a CASE-WHEN expression, and
 	 * transform_null_equals does not apply.)
 	 */
-	if (Transform_null_equals &&
-		list_length(a->name) == 1 &&
+	need_transform_null_equals = (list_length(a->name) == 1 &&
 		strcmp(strVal(linitial(a->name)), "=") == 0 &&
 		(exprIsNullConstant(lexpr) || exprIsNullConstant(rexpr)) &&
-		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)))
+		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)));
+
+	/*
+	 * We don't need to handle TRANSFORM_NULL_EQUALS_OFF here because it's a noop
+	 */
+
+	if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_WARN)
+		ereport(WARNING,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL can only produce a NULL"),
+				 parser_errposition(pstate, a->location)));
+
+	if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ERR)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL does not comply with the SQL specification"),
+				 parser_errposition(pstate, a->location)));
+
+	if (need_transform_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ON)
 	{
 		NullTest   *n = makeNode(NullTest);
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index a88ea6cfc9..d441bccb10 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -310,6 +310,23 @@ static const struct config_enum_entry track_function_options[] = {
 	{NULL, 0, false}
 };
 
+/*
+ * Accept the usual variants of boolean-ish, along with warn and error.
+ */
+static const struct config_enum_entry transform_null_equals_options[] = {
+	{"off", TRANSFORM_NULL_EQUALS_OFF, false},
+	{"false", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"no", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"0", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"warn", TRANSFORM_NULL_EQUALS_WARN, false},
+	{"error", TRANSFORM_NULL_EQUALS_ERR, false},
+	{"on", TRANSFORM_NULL_EQUALS_ON, false},
+	{"true", TRANSFORM_NULL_EQUALS_ON, true},
+	{"yes", TRANSFORM_NULL_EQUALS_ON, true},
+	{"1", TRANSFORM_NULL_EQUALS_ON, true},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry xmlbinary_options[] = {
 	{"base64", XMLBINARY_BASE64, false},
 	{"hex", XMLBINARY_HEX, false},
@@ -1414,19 +1431,6 @@ static struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
-	{
-		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
-			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
-			gettext_noop("When turned on, expressions of the form expr = NULL "
-						 "(or NULL = expr) are treated as expr IS NULL, that is, they "
-						 "return true if expr evaluates to the null value, and false "
-						 "otherwise. The correct behavior of expr = NULL is to always "
-						 "return null (unknown).")
-		},
-		&Transform_null_equals,
-		false,
-		NULL, NULL, NULL
-	},
 	{
 		{"db_user_namespace", PGC_SIGHUP, CONN_AUTH_AUTH,
 			gettext_noop("Enables per-database user names."),
@@ -4074,6 +4078,20 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
+			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
+			gettext_noop("When set to on, expressions of the form expr = NULL "
+						 "(or NULL = expr) are treated as expr IS NULL. When "
+						 "set to off, warn, or error, they are not, with increasing "
+						 "insistence. The default behavior of expr = NULL is to "
+						 "return null (unknown).")
+		},
+		&transform_null_equals,
+		TRANSFORM_NULL_EQUALS_OFF, transform_null_equals_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Set the level of information written to the WAL."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c0d3fb8491..07dda7b746 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -656,7 +656,7 @@
 
 # - Other Platforms and Clients -
 
-#transform_null_equals = off
+#transform_null_equals = off	# off, warn, error, or on
 
 
 #------------------------------------------------------------------------------
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index e5aff61b8f..2f3dc28858 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -17,10 +17,19 @@
 
 /* GUC parameters */
 extern bool operator_precedence_warning;
-extern bool Transform_null_equals;
 
 extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
 
 extern const char *ParseExprKindName(ParseExprKind exprKind);
 
+typedef enum TransformNullEquals
+{
+	TRANSFORM_NULL_EQUALS_OFF = 0,	/* Disabled */
+	TRANSFORM_NULL_EQUALS_WARN,		/* Issue a warning */
+	TRANSFORM_NULL_EQUALS_ERR,		/* Error out */
+	TRANSFORM_NULL_EQUALS_ON		/* Enabled */
+} TransformNullEquals;
+
+extern TransformNullEquals transform_null_equals;
+
 #endif							/* PARSE_EXPR_H */
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 43ac5f5f11..f50a1f153c 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -767,3 +767,33 @@ NOTICE:  text search configuration "no_such_config" does not exist
 select func_with_bad_set();
 ERROR:  invalid value for parameter "default_text_search_config": "no_such_config"
 reset check_function_bodies;
+set transform_null_equals = off;
+select 1=null as transform_null_equals_off;
+ transform_null_equals_off 
+---------------------------
+ 
+(1 row)
+
+set transform_null_equals = warn;
+select 1=null as transform_null_equals_warn;
+WARNING:  = NULL can only produce a NULL
+LINE 1: select 1=null as transform_null_equals_warn;
+                ^
+ transform_null_equals_warn 
+----------------------------
+ 
+(1 row)
+
+set transform_null_equals = error;
+select 1=null as transform_null_equals_error;
+ERROR:  = NULL does not comply with the SQL specification
+LINE 1: select 1=null as transform_null_equals_error;
+                ^
+set transform_null_equals = on;
+select 1=null as transform_null_equals_on;
+ transform_null_equals_on 
+--------------------------
+ f
+(1 row)
+
+reset transform_null_equals;
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index 23e5029780..4b76e75048 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -288,3 +288,21 @@ set default_text_search_config = no_such_config;
 select func_with_bad_set();
 
 reset check_function_bodies;
+
+set transform_null_equals = off;
+
+select 1=null as transform_null_equals_off;
+
+set transform_null_equals = warn;
+
+select 1=null as transform_null_equals_warn;
+
+set transform_null_equals = error;
+
+select 1=null as transform_null_equals_error;
+
+set transform_null_equals = on;
+
+select 1=null as transform_null_equals_on;
+
+reset transform_null_equals;
-- 
2.17.1

#14Fabien COELHO
coelho@cri.ensmp.fr
In reply to: David Fetter (#13)
Re: Make foo=null a warning by default.

Hello David,

I assure you that you expression yourself in English a good deal
better than I do in Portuguese.

Alas, despite a Portuguese "rabbit" name, I cannot speak the language
which got lost between generations.

About this v3: Patch applies, compiles, "make check" ok.

A few minor comments:

Variable "need_transform_null_equals" may be better named
"is_null_equals", because there is no "need" of the transformation as
such, and the expression just checks for the pattern, really.

I'm fine with the off/warn/error/on order.

Doc could mention that the transformation allows compatibility with other
products, without naming them? Or not.

In doc, the list of valid values on a long line, where the practice seems
to wrap around after about 80 columns in the XML file.

I notice that the feature was not tested at all before this patch:-(

Maybe there could be one test which results to true, which is the whole
point of the transformation? eg "SELECT NULL = NULL".

--
Fabien.

#15David Fetter
david@fetter.org
In reply to: Fabien COELHO (#14)
1 attachment(s)
Re: Make foo=null a warning by default.

On Wed, Jul 18, 2018 at 08:25:46AM -0400, Fabien COELHO wrote:

Hello David,

I assure you that you expression yourself in English a good deal
better than I do in Portuguese.

Alas, despite a Portuguese "rabbit" name, I cannot speak the language which
got lost between generations.

I fear my kids may lose French the same way. Mine's already not great,
and that's in just one generation.

About this v3: Patch applies, compiles, "make check" ok.

A few minor comments:

Variable "need_transform_null_equals" may be better named "is_null_equals",
because there is no "need" of the transformation as such, and the expression
just checks for the pattern, really.

Done.

I'm fine with the off/warn/error/on order.

Doc could mention that the transformation allows compatibility with other
products, without naming them? Or not.

I don't see a point in obscuring the origin.

In doc, the list of valid values on a long line, where the practice seems to
wrap around after about 80 columns in the XML file.

Fixed.

I notice that the feature was not tested at all before this patch:-(

Maybe there could be one test which results to true, which is the whole
point of the transformation? eg "SELECT NULL = NULL".

Done.

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

Attachments:

0001-Make-transform_null_equals-into-an-enum-v4.patchtext/x-diff; charset=us-asciiDownload
From 1c0d16c79dba628ec10fd1554574cf497b3dfffb Mon Sep 17 00:00:00 2001
From: David Fetter <david@fetter.org>
Date: Sun, 15 Jul 2018 16:11:08 -0700
Subject: [PATCH] Make transform_null_equals into an enum v4
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="------------2.17.1"

This is a multi-part message in MIME format.
--------------2.17.1
Content-Type: text/plain; charset=UTF-8; format=fixed
Content-Transfer-Encoding: 8bit


The transform_null_equals GUC can now be off, warn, error, or on.
---
 doc/src/sgml/config.sgml                      | 25 ++++++---
 src/backend/parser/parse_expr.c               | 30 ++++++++--
 src/backend/utils/misc/guc.c                  | 44 ++++++++++-----
 src/backend/utils/misc/postgresql.conf.sample |  2 +-
 src/include/parser/parse_expr.h               | 11 +++-
 src/test/regress/expected/guc.out             | 55 +++++++++++++++++++
 src/test/regress/sql/guc.sql                  | 26 +++++++++
 7 files changed, 164 insertions(+), 29 deletions(-)


--------------2.17.1
Content-Type: text/x-patch; name="0001-Make-transform_null_equals-into-an-enum-v4.patch"
Content-Transfer-Encoding: 8bit
Content-Disposition: attachment; filename="0001-Make-transform_null_equals-into-an-enum-v4.patch"

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 4d48d93305..6468c2253f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8066,7 +8066,7 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
      <variablelist>
 
      <varlistentry id="guc-transform-null-equals" xreflabel="transform_null_equals">
-      <term><varname>transform_null_equals</varname> (<type>boolean</type>)
+      <term><varname>transform_null_equals</varname> (<type>enum</type>)
       <indexterm><primary>IS NULL</primary></indexterm>
       <indexterm>
        <primary><varname>transform_null_equals</varname> configuration parameter</primary>
@@ -8074,15 +8074,24 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </term>
       <listitem>
        <para>
-        When on, expressions of the form <literal><replaceable>expr</replaceable> =
-        NULL</literal> (or <literal>NULL =
-        <replaceable>expr</replaceable></literal>) are treated as
+        When enabled, expressions of the form
+        <literal><replaceable>expr</replaceable> = NULL</literal> (or
+        <literal>NULL = <replaceable>expr</replaceable></literal>) are treated as
         <literal><replaceable>expr</replaceable> IS NULL</literal>, that is, they
         return true if <replaceable>expr</replaceable> evaluates to the null value,
-        and false otherwise. The correct SQL-spec-compliant behavior of
-        <literal><replaceable>expr</replaceable> = NULL</literal> is to always
-        return null (unknown). Therefore this parameter defaults to
-        <literal>off</literal>.
+        and false otherwise.  Valid values are <literal>off</literal>,
+        <literal>warn</literal>, <literal>error</literal>, and <literal>on</literal>.
+       </para>
+
+       <para>
+        The correct SQL-spec-compliant behavior of
+        <literal><replaceable>expr</replaceable> = <replacable>null_expression</replaceable></literal>
+        is to always return null (unknown); PostgreSQL allows <literal>NULL</literal>
+        as a null-valued expression of unknown type, which is an extension to the spec.
+        The transformation of <literal><replaceable>expr</replaceable> = NULL</literal>
+        to <literal><replaceable>expr</replaceable> IS NULL</literal> is inconsistent
+        with the normal semantics of the SQL specification, and is therefore set to "off"
+        by default.
        </para>
 
        <para>
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 385e54a9b6..53970a8ce6 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -41,8 +41,8 @@
 
 
 /* GUC parameters */
-bool		operator_precedence_warning = false;
-bool		Transform_null_equals = false;
+bool	operator_precedence_warning = false;
+enum	TransformNullEquals transform_null_equals = TRANSFORM_NULL_EQUALS_OFF;
 
 /*
  * Node-type groups for operator precedence warnings
@@ -850,6 +850,7 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	Node	   *lexpr = a->lexpr;
 	Node	   *rexpr = a->rexpr;
 	Node	   *result;
+	bool		is_null_equals = false;
 
 	if (operator_precedence_warning)
 	{
@@ -872,17 +873,34 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	}
 
 	/*
-	 * Special-case "foo = NULL" and "NULL = foo" for compatibility with
+	 * Deal with "foo = NULL" and "NULL = foo" for compatibility with
 	 * standards-broken products (like Microsoft's).  Turn these into IS NULL
 	 * exprs. (If either side is a CaseTestExpr, then the expression was
 	 * generated internally from a CASE-WHEN expression, and
 	 * transform_null_equals does not apply.)
 	 */
-	if (Transform_null_equals &&
-		list_length(a->name) == 1 &&
+	is_null_equals = (list_length(a->name) == 1 &&
 		strcmp(strVal(linitial(a->name)), "=") == 0 &&
 		(exprIsNullConstant(lexpr) || exprIsNullConstant(rexpr)) &&
-		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)))
+		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)));
+
+	/*
+	 * We don't need to handle TRANSFORM_NULL_EQUALS_OFF here because it's a noop
+	 */
+
+	if (is_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_WARN)
+		ereport(WARNING,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL can only produce a NULL"),
+				 parser_errposition(pstate, a->location)));
+
+	if (is_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ERR)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL does not comply with the SQL specification"),
+				 parser_errposition(pstate, a->location)));
+
+	if (is_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ON)
 	{
 		NullTest   *n = makeNode(NullTest);
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index a88ea6cfc9..d441bccb10 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -310,6 +310,23 @@ static const struct config_enum_entry track_function_options[] = {
 	{NULL, 0, false}
 };
 
+/*
+ * Accept the usual variants of boolean-ish, along with warn and error.
+ */
+static const struct config_enum_entry transform_null_equals_options[] = {
+	{"off", TRANSFORM_NULL_EQUALS_OFF, false},
+	{"false", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"no", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"0", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"warn", TRANSFORM_NULL_EQUALS_WARN, false},
+	{"error", TRANSFORM_NULL_EQUALS_ERR, false},
+	{"on", TRANSFORM_NULL_EQUALS_ON, false},
+	{"true", TRANSFORM_NULL_EQUALS_ON, true},
+	{"yes", TRANSFORM_NULL_EQUALS_ON, true},
+	{"1", TRANSFORM_NULL_EQUALS_ON, true},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry xmlbinary_options[] = {
 	{"base64", XMLBINARY_BASE64, false},
 	{"hex", XMLBINARY_HEX, false},
@@ -1414,19 +1431,6 @@ static struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
-	{
-		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
-			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
-			gettext_noop("When turned on, expressions of the form expr = NULL "
-						 "(or NULL = expr) are treated as expr IS NULL, that is, they "
-						 "return true if expr evaluates to the null value, and false "
-						 "otherwise. The correct behavior of expr = NULL is to always "
-						 "return null (unknown).")
-		},
-		&Transform_null_equals,
-		false,
-		NULL, NULL, NULL
-	},
 	{
 		{"db_user_namespace", PGC_SIGHUP, CONN_AUTH_AUTH,
 			gettext_noop("Enables per-database user names."),
@@ -4074,6 +4078,20 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
+			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
+			gettext_noop("When set to on, expressions of the form expr = NULL "
+						 "(or NULL = expr) are treated as expr IS NULL. When "
+						 "set to off, warn, or error, they are not, with increasing "
+						 "insistence. The default behavior of expr = NULL is to "
+						 "return null (unknown).")
+		},
+		&transform_null_equals,
+		TRANSFORM_NULL_EQUALS_OFF, transform_null_equals_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Set the level of information written to the WAL."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c0d3fb8491..07dda7b746 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -656,7 +656,7 @@
 
 # - Other Platforms and Clients -
 
-#transform_null_equals = off
+#transform_null_equals = off	# off, warn, error, or on
 
 
 #------------------------------------------------------------------------------
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index e5aff61b8f..2f3dc28858 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -17,10 +17,19 @@
 
 /* GUC parameters */
 extern bool operator_precedence_warning;
-extern bool Transform_null_equals;
 
 extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
 
 extern const char *ParseExprKindName(ParseExprKind exprKind);
 
+typedef enum TransformNullEquals
+{
+	TRANSFORM_NULL_EQUALS_OFF = 0,	/* Disabled */
+	TRANSFORM_NULL_EQUALS_WARN,		/* Issue a warning */
+	TRANSFORM_NULL_EQUALS_ERR,		/* Error out */
+	TRANSFORM_NULL_EQUALS_ON		/* Enabled */
+} TransformNullEquals;
+
+extern TransformNullEquals transform_null_equals;
+
 #endif							/* PARSE_EXPR_H */
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 43ac5f5f11..cc4fdf2de8 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -767,3 +767,58 @@ NOTICE:  text search configuration "no_such_config" does not exist
 select func_with_bad_set();
 ERROR:  invalid value for parameter "default_text_search_config": "no_such_config"
 reset check_function_bodies;
+set transform_null_equals = off;
+select 1=null as transform_null_equals_off;
+ transform_null_equals_off 
+---------------------------
+ 
+(1 row)
+
+select null=null as transform_null_equals_off;
+ transform_null_equals_off 
+---------------------------
+ 
+(1 row)
+
+set transform_null_equals = warn;
+select 1=null as transform_null_equals_warn;
+WARNING:  = NULL can only produce a NULL
+LINE 1: select 1=null as transform_null_equals_warn;
+                ^
+ transform_null_equals_warn 
+----------------------------
+ 
+(1 row)
+
+select null=null as transform_null_equals_warn;
+WARNING:  = NULL can only produce a NULL
+LINE 1: select null=null as transform_null_equals_warn;
+                   ^
+ transform_null_equals_warn 
+----------------------------
+ 
+(1 row)
+
+set transform_null_equals = error;
+select 1=null as transform_null_equals_error;
+ERROR:  = NULL does not comply with the SQL specification
+LINE 1: select 1=null as transform_null_equals_error;
+                ^
+select null=null as transform_null_equals_error;
+ERROR:  = NULL does not comply with the SQL specification
+LINE 1: select null=null as transform_null_equals_error;
+                   ^
+set transform_null_equals = on;
+select 1=null as transform_null_equals_on;
+ transform_null_equals_on 
+--------------------------
+ f
+(1 row)
+
+select null=null as transform_null_equals_on;
+ transform_null_equals_on 
+--------------------------
+ t
+(1 row)
+
+reset transform_null_equals;
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index 23e5029780..24fa8c5308 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -288,3 +288,29 @@ set default_text_search_config = no_such_config;
 select func_with_bad_set();
 
 reset check_function_bodies;
+
+set transform_null_equals = off;
+
+select 1=null as transform_null_equals_off;
+
+select null=null as transform_null_equals_off;
+
+set transform_null_equals = warn;
+
+select 1=null as transform_null_equals_warn;
+
+select null=null as transform_null_equals_warn;
+
+set transform_null_equals = error;
+
+select 1=null as transform_null_equals_error;
+
+select null=null as transform_null_equals_error;
+
+set transform_null_equals = on;
+
+select 1=null as transform_null_equals_on;
+
+select null=null as transform_null_equals_on;
+
+reset transform_null_equals;

--------------2.17.1--


#16Fabien COELHO
coelho@cri.ensmp.fr
In reply to: David Fetter (#15)
Re: Make foo=null a warning by default.

[...] Done.

All looks well, but I just noticed a warning:

gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -O2 -I. -I. -I../../../../src/include -D_GNU_SOURCE -c -o guc.o guc.c
guc.c:4090:3: warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
&transform_null_equals,
^
guc.c:4090:3: note: (near initialization for οΏ½ConfigureNamesEnum[16].variableοΏ½)

Too bad, gcc does not like an (enum*) there. Maybe cast to (int *), or
declare the variable as an int (which seems to be what is done for
others)?

--
Fabien.

#17David Fetter
david@fetter.org
In reply to: Fabien COELHO (#16)
1 attachment(s)
Re: Make foo=null a warning by default.

On Wed, Jul 18, 2018 at 04:15:30PM -0400, Fabien COELHO wrote:

[...] Done.

All looks well, but I just noticed a warning:

gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -O2 -I. -I. -I../../../../src/include -D_GNU_SOURCE -c -o guc.o guc.c
guc.c:4090:3: warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
&transform_null_equals,
^
guc.c:4090:3: note: (near initialization for ‘ConfigureNamesEnum[16].variable’)

Too bad, gcc does not like an (enum*) there. Maybe cast to (int *), or
declare the variable as an int (which seems to be what is done for others)?

Done that way.

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

Attachments:

0001-Make-transform_null_equals-into-an-enum-v5.patchtext/x-diff; charset=us-asciiDownload
From c9d83b3fe39c86a38f7ac8114da90bef538f5cb7 Mon Sep 17 00:00:00 2001
From: David Fetter <david@fetter.org>
Date: Sun, 15 Jul 2018 16:11:08 -0700
Subject: [PATCH] Make transform_null_equals into an enum v5
To: pgsql-hackers@postgresql.org

The transform_null_equals GUC can now be off, warn, error, or on.
---
 doc/src/sgml/config.sgml                      | 25 ++++++---
 src/backend/parser/parse_expr.c               | 30 ++++++++--
 src/backend/utils/misc/guc.c                  | 44 ++++++++++-----
 src/backend/utils/misc/postgresql.conf.sample |  2 +-
 src/include/parser/parse_expr.h               | 11 +++-
 src/test/regress/expected/guc.out             | 55 +++++++++++++++++++
 src/test/regress/sql/guc.sql                  | 26 +++++++++
 7 files changed, 164 insertions(+), 29 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 4d48d93305..6468c2253f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8066,7 +8066,7 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
      <variablelist>
 
      <varlistentry id="guc-transform-null-equals" xreflabel="transform_null_equals">
-      <term><varname>transform_null_equals</varname> (<type>boolean</type>)
+      <term><varname>transform_null_equals</varname> (<type>enum</type>)
       <indexterm><primary>IS NULL</primary></indexterm>
       <indexterm>
        <primary><varname>transform_null_equals</varname> configuration parameter</primary>
@@ -8074,15 +8074,24 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </term>
       <listitem>
        <para>
-        When on, expressions of the form <literal><replaceable>expr</replaceable> =
-        NULL</literal> (or <literal>NULL =
-        <replaceable>expr</replaceable></literal>) are treated as
+        When enabled, expressions of the form
+        <literal><replaceable>expr</replaceable> = NULL</literal> (or
+        <literal>NULL = <replaceable>expr</replaceable></literal>) are treated as
         <literal><replaceable>expr</replaceable> IS NULL</literal>, that is, they
         return true if <replaceable>expr</replaceable> evaluates to the null value,
-        and false otherwise. The correct SQL-spec-compliant behavior of
-        <literal><replaceable>expr</replaceable> = NULL</literal> is to always
-        return null (unknown). Therefore this parameter defaults to
-        <literal>off</literal>.
+        and false otherwise.  Valid values are <literal>off</literal>,
+        <literal>warn</literal>, <literal>error</literal>, and <literal>on</literal>.
+       </para>
+
+       <para>
+        The correct SQL-spec-compliant behavior of
+        <literal><replaceable>expr</replaceable> = <replacable>null_expression</replaceable></literal>
+        is to always return null (unknown); PostgreSQL allows <literal>NULL</literal>
+        as a null-valued expression of unknown type, which is an extension to the spec.
+        The transformation of <literal><replaceable>expr</replaceable> = NULL</literal>
+        to <literal><replaceable>expr</replaceable> IS NULL</literal> is inconsistent
+        with the normal semantics of the SQL specification, and is therefore set to "off"
+        by default.
        </para>
 
        <para>
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 385e54a9b6..5b3e5caaeb 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -41,8 +41,8 @@
 
 
 /* GUC parameters */
-bool		operator_precedence_warning = false;
-bool		Transform_null_equals = false;
+bool	operator_precedence_warning = false;
+int		transform_null_equals = TRANSFORM_NULL_EQUALS_OFF;
 
 /*
  * Node-type groups for operator precedence warnings
@@ -850,6 +850,7 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	Node	   *lexpr = a->lexpr;
 	Node	   *rexpr = a->rexpr;
 	Node	   *result;
+	bool		is_null_equals = false;
 
 	if (operator_precedence_warning)
 	{
@@ -872,17 +873,34 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	}
 
 	/*
-	 * Special-case "foo = NULL" and "NULL = foo" for compatibility with
+	 * Deal with "foo = NULL" and "NULL = foo" for compatibility with
 	 * standards-broken products (like Microsoft's).  Turn these into IS NULL
 	 * exprs. (If either side is a CaseTestExpr, then the expression was
 	 * generated internally from a CASE-WHEN expression, and
 	 * transform_null_equals does not apply.)
 	 */
-	if (Transform_null_equals &&
-		list_length(a->name) == 1 &&
+	is_null_equals = (list_length(a->name) == 1 &&
 		strcmp(strVal(linitial(a->name)), "=") == 0 &&
 		(exprIsNullConstant(lexpr) || exprIsNullConstant(rexpr)) &&
-		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)))
+		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)));
+
+	/*
+	 * We don't need to handle TRANSFORM_NULL_EQUALS_OFF here because it's a noop
+	 */
+
+	if (is_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_WARN)
+		ereport(WARNING,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL can only produce a NULL"),
+				 parser_errposition(pstate, a->location)));
+
+	if (is_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ERR)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL does not comply with the SQL specification"),
+				 parser_errposition(pstate, a->location)));
+
+	if (is_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ON)
 	{
 		NullTest   *n = makeNode(NullTest);
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index a88ea6cfc9..d441bccb10 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -310,6 +310,23 @@ static const struct config_enum_entry track_function_options[] = {
 	{NULL, 0, false}
 };
 
+/*
+ * Accept the usual variants of boolean-ish, along with warn and error.
+ */
+static const struct config_enum_entry transform_null_equals_options[] = {
+	{"off", TRANSFORM_NULL_EQUALS_OFF, false},
+	{"false", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"no", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"0", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"warn", TRANSFORM_NULL_EQUALS_WARN, false},
+	{"error", TRANSFORM_NULL_EQUALS_ERR, false},
+	{"on", TRANSFORM_NULL_EQUALS_ON, false},
+	{"true", TRANSFORM_NULL_EQUALS_ON, true},
+	{"yes", TRANSFORM_NULL_EQUALS_ON, true},
+	{"1", TRANSFORM_NULL_EQUALS_ON, true},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry xmlbinary_options[] = {
 	{"base64", XMLBINARY_BASE64, false},
 	{"hex", XMLBINARY_HEX, false},
@@ -1414,19 +1431,6 @@ static struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
-	{
-		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
-			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
-			gettext_noop("When turned on, expressions of the form expr = NULL "
-						 "(or NULL = expr) are treated as expr IS NULL, that is, they "
-						 "return true if expr evaluates to the null value, and false "
-						 "otherwise. The correct behavior of expr = NULL is to always "
-						 "return null (unknown).")
-		},
-		&Transform_null_equals,
-		false,
-		NULL, NULL, NULL
-	},
 	{
 		{"db_user_namespace", PGC_SIGHUP, CONN_AUTH_AUTH,
 			gettext_noop("Enables per-database user names."),
@@ -4074,6 +4078,20 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
+			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
+			gettext_noop("When set to on, expressions of the form expr = NULL "
+						 "(or NULL = expr) are treated as expr IS NULL. When "
+						 "set to off, warn, or error, they are not, with increasing "
+						 "insistence. The default behavior of expr = NULL is to "
+						 "return null (unknown).")
+		},
+		&transform_null_equals,
+		TRANSFORM_NULL_EQUALS_OFF, transform_null_equals_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Set the level of information written to the WAL."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c0d3fb8491..07dda7b746 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -656,7 +656,7 @@
 
 # - Other Platforms and Clients -
 
-#transform_null_equals = off
+#transform_null_equals = off	# off, warn, error, or on
 
 
 #------------------------------------------------------------------------------
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index e5aff61b8f..22c11e51f2 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -17,10 +17,19 @@
 
 /* GUC parameters */
 extern bool operator_precedence_warning;
-extern bool Transform_null_equals;
 
 extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
 
 extern const char *ParseExprKindName(ParseExprKind exprKind);
 
+typedef enum TransformNullEquals
+{
+	TRANSFORM_NULL_EQUALS_OFF = 0,	/* Disabled */
+	TRANSFORM_NULL_EQUALS_WARN,		/* Issue a warning */
+	TRANSFORM_NULL_EQUALS_ERR,		/* Error out */
+	TRANSFORM_NULL_EQUALS_ON		/* Enabled */
+} TransformNullEquals;
+
+int transform_null_equals;
+
 #endif							/* PARSE_EXPR_H */
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 43ac5f5f11..cc4fdf2de8 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -767,3 +767,58 @@ NOTICE:  text search configuration "no_such_config" does not exist
 select func_with_bad_set();
 ERROR:  invalid value for parameter "default_text_search_config": "no_such_config"
 reset check_function_bodies;
+set transform_null_equals = off;
+select 1=null as transform_null_equals_off;
+ transform_null_equals_off 
+---------------------------
+ 
+(1 row)
+
+select null=null as transform_null_equals_off;
+ transform_null_equals_off 
+---------------------------
+ 
+(1 row)
+
+set transform_null_equals = warn;
+select 1=null as transform_null_equals_warn;
+WARNING:  = NULL can only produce a NULL
+LINE 1: select 1=null as transform_null_equals_warn;
+                ^
+ transform_null_equals_warn 
+----------------------------
+ 
+(1 row)
+
+select null=null as transform_null_equals_warn;
+WARNING:  = NULL can only produce a NULL
+LINE 1: select null=null as transform_null_equals_warn;
+                   ^
+ transform_null_equals_warn 
+----------------------------
+ 
+(1 row)
+
+set transform_null_equals = error;
+select 1=null as transform_null_equals_error;
+ERROR:  = NULL does not comply with the SQL specification
+LINE 1: select 1=null as transform_null_equals_error;
+                ^
+select null=null as transform_null_equals_error;
+ERROR:  = NULL does not comply with the SQL specification
+LINE 1: select null=null as transform_null_equals_error;
+                   ^
+set transform_null_equals = on;
+select 1=null as transform_null_equals_on;
+ transform_null_equals_on 
+--------------------------
+ f
+(1 row)
+
+select null=null as transform_null_equals_on;
+ transform_null_equals_on 
+--------------------------
+ t
+(1 row)
+
+reset transform_null_equals;
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index 23e5029780..24fa8c5308 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -288,3 +288,29 @@ set default_text_search_config = no_such_config;
 select func_with_bad_set();
 
 reset check_function_bodies;
+
+set transform_null_equals = off;
+
+select 1=null as transform_null_equals_off;
+
+select null=null as transform_null_equals_off;
+
+set transform_null_equals = warn;
+
+select 1=null as transform_null_equals_warn;
+
+select null=null as transform_null_equals_warn;
+
+set transform_null_equals = error;
+
+select 1=null as transform_null_equals_error;
+
+select null=null as transform_null_equals_error;
+
+set transform_null_equals = on;
+
+select 1=null as transform_null_equals_on;
+
+select null=null as transform_null_equals_on;
+
+reset transform_null_equals;
-- 
2.17.1

#18Fabien COELHO
coelho@cri.ensmp.fr
In reply to: David Fetter (#17)
Re: Make foo=null a warning by default.

Hello David,

All is nearly well, although "make docs" found a typo. I should have
tested doc building before, sorry for this new round which should have
been avoided.

"<replacable>null_expression</replaceable>"

s/<replacable>/<replaceable>/;

Also, while reading the full documentation about the setting: I find this
sentence a bit strange:

"""
But new users are frequently confused about the semantics of expressions
involving null values, so this option is off by default.
"""

It appears to justify why the option is off by default, but this is not
the actual reason, which was already provided at the end of the preceding
paragraph.

I would suggest to remove this "But..." sentence, or turn it differently,
maybe something like:

""" New users are frequently confused about the semantics of expressions
involving null values: beware that turning this option on might bring even
more confusion. """

--
Fabien.

#19David Fetter
david@fetter.org
In reply to: Fabien COELHO (#18)
1 attachment(s)
Re: Make foo=null a warning by default.

On Thu, Jul 19, 2018 at 06:37:39AM -0400, Fabien COELHO wrote:

Hello David,

All is nearly well, although "make docs" found a typo. I should have tested
doc building before, sorry for this new round which should have been
avoided.

"<replacable>null_expression</replaceable>"

s/<replacable>/<replaceable>/;

Fixed.

Also, while reading the full documentation about the setting: I find this
sentence a bit strange:

"""
But new users are frequently confused about the semantics of expressions
involving null values, so this option is off by default.
"""

It appears to justify why the option is off by default, but this is not the
actual reason, which was already provided at the end of the preceding
paragraph.

I would suggest to remove this "But..." sentence, or turn it differently,
maybe something like:

""" New users are frequently confused about the semantics of expressions
involving null values: beware that turning this option on might bring even
more confusion. """

I've changed it to something that makes more sense to me. Hope it
makes more sense to others.

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

Attachments:

0001-Make-transform_null_equals-into-an-enum-v6.patchtext/x-diff; charset=us-asciiDownload
From b1c6e4c92226065c82053a4d161daf2822149f76 Mon Sep 17 00:00:00 2001
From: David Fetter <david@fetter.org>
Date: Sun, 15 Jul 2018 16:11:08 -0700
Subject: [PATCH] Make transform_null_equals into an enum v6
To: pgsql-hackers@postgresql.org

The transform_null_equals GUC can now be off, warn, error, or on.
---
 doc/src/sgml/config.sgml                      | 30 ++++++----
 src/backend/parser/parse_expr.c               | 30 ++++++++--
 src/backend/utils/misc/guc.c                  | 44 ++++++++++-----
 src/backend/utils/misc/postgresql.conf.sample |  2 +-
 src/include/parser/parse_expr.h               | 11 +++-
 src/test/regress/expected/guc.out             | 55 +++++++++++++++++++
 src/test/regress/sql/guc.sql                  | 26 +++++++++
 7 files changed, 166 insertions(+), 32 deletions(-)

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 4d48d93305..2e1441f227 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8066,7 +8066,7 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
      <variablelist>
 
      <varlistentry id="guc-transform-null-equals" xreflabel="transform_null_equals">
-      <term><varname>transform_null_equals</varname> (<type>boolean</type>)
+      <term><varname>transform_null_equals</varname> (<type>enum</type>)
       <indexterm><primary>IS NULL</primary></indexterm>
       <indexterm>
        <primary><varname>transform_null_equals</varname> configuration parameter</primary>
@@ -8074,15 +8074,24 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
       </term>
       <listitem>
        <para>
-        When on, expressions of the form <literal><replaceable>expr</replaceable> =
-        NULL</literal> (or <literal>NULL =
-        <replaceable>expr</replaceable></literal>) are treated as
+        When enabled, expressions of the form
+        <literal><replaceable>expr</replaceable> = NULL</literal> (or
+        <literal>NULL = <replaceable>expr</replaceable></literal>) are treated as
         <literal><replaceable>expr</replaceable> IS NULL</literal>, that is, they
         return true if <replaceable>expr</replaceable> evaluates to the null value,
-        and false otherwise. The correct SQL-spec-compliant behavior of
-        <literal><replaceable>expr</replaceable> = NULL</literal> is to always
-        return null (unknown). Therefore this parameter defaults to
-        <literal>off</literal>.
+        and false otherwise.  Valid values are <literal>off</literal>,
+        <literal>warn</literal>, <literal>error</literal>, and <literal>on</literal>.
+       </para>
+
+       <para>
+        The correct SQL-spec-compliant behavior of
+        <literal><replaceable>expr</replaceable> = <replaceable>null_expression</replaceable></literal>
+        is to always return null (unknown); PostgreSQL allows <literal>NULL</literal>
+        as a null-valued expression of unknown type, which is an extension to the spec.
+        The transformation of <literal><replaceable>expr</replaceable> = NULL</literal>
+        to <literal><replaceable>expr</replaceable> IS NULL</literal> is inconsistent
+        with the normal semantics of the SQL specification, and is therefore set to "off"
+        by default.
        </para>
 
        <para>
@@ -8094,9 +8103,8 @@ dynamic_library_path = 'C:\tools\postgresql;H:\my_project\lib;$libdir'
         form <literal><replaceable>expr</replaceable> = NULL</literal> always
         return the null value (using the SQL standard interpretation), they are not
         very useful and do not appear often in normal applications so
-        this option does little harm in practice.  But new users are
-        frequently confused about the semantics of expressions
-        involving null values, so this option is off by default.
+        this option does little harm in practice.  As this option makes null
+        handling even more confusing, especially for new users, it is off by default.
        </para>
 
        <para>
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 385e54a9b6..5b3e5caaeb 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -41,8 +41,8 @@
 
 
 /* GUC parameters */
-bool		operator_precedence_warning = false;
-bool		Transform_null_equals = false;
+bool	operator_precedence_warning = false;
+int		transform_null_equals = TRANSFORM_NULL_EQUALS_OFF;
 
 /*
  * Node-type groups for operator precedence warnings
@@ -850,6 +850,7 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	Node	   *lexpr = a->lexpr;
 	Node	   *rexpr = a->rexpr;
 	Node	   *result;
+	bool		is_null_equals = false;
 
 	if (operator_precedence_warning)
 	{
@@ -872,17 +873,34 @@ transformAExprOp(ParseState *pstate, A_Expr *a)
 	}
 
 	/*
-	 * Special-case "foo = NULL" and "NULL = foo" for compatibility with
+	 * Deal with "foo = NULL" and "NULL = foo" for compatibility with
 	 * standards-broken products (like Microsoft's).  Turn these into IS NULL
 	 * exprs. (If either side is a CaseTestExpr, then the expression was
 	 * generated internally from a CASE-WHEN expression, and
 	 * transform_null_equals does not apply.)
 	 */
-	if (Transform_null_equals &&
-		list_length(a->name) == 1 &&
+	is_null_equals = (list_length(a->name) == 1 &&
 		strcmp(strVal(linitial(a->name)), "=") == 0 &&
 		(exprIsNullConstant(lexpr) || exprIsNullConstant(rexpr)) &&
-		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)))
+		(!IsA(lexpr, CaseTestExpr) &&!IsA(rexpr, CaseTestExpr)));
+
+	/*
+	 * We don't need to handle TRANSFORM_NULL_EQUALS_OFF here because it's a noop
+	 */
+
+	if (is_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_WARN)
+		ereport(WARNING,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL can only produce a NULL"),
+				 parser_errposition(pstate, a->location)));
+
+	if (is_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ERR)
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("= NULL does not comply with the SQL specification"),
+				 parser_errposition(pstate, a->location)));
+
+	if (is_null_equals && transform_null_equals == TRANSFORM_NULL_EQUALS_ON)
 	{
 		NullTest   *n = makeNode(NullTest);
 
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index a88ea6cfc9..d441bccb10 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -310,6 +310,23 @@ static const struct config_enum_entry track_function_options[] = {
 	{NULL, 0, false}
 };
 
+/*
+ * Accept the usual variants of boolean-ish, along with warn and error.
+ */
+static const struct config_enum_entry transform_null_equals_options[] = {
+	{"off", TRANSFORM_NULL_EQUALS_OFF, false},
+	{"false", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"no", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"0", TRANSFORM_NULL_EQUALS_OFF, true},
+	{"warn", TRANSFORM_NULL_EQUALS_WARN, false},
+	{"error", TRANSFORM_NULL_EQUALS_ERR, false},
+	{"on", TRANSFORM_NULL_EQUALS_ON, false},
+	{"true", TRANSFORM_NULL_EQUALS_ON, true},
+	{"yes", TRANSFORM_NULL_EQUALS_ON, true},
+	{"1", TRANSFORM_NULL_EQUALS_ON, true},
+	{NULL, 0, false}
+};
+
 static const struct config_enum_entry xmlbinary_options[] = {
 	{"base64", XMLBINARY_BASE64, false},
 	{"hex", XMLBINARY_HEX, false},
@@ -1414,19 +1431,6 @@ static struct config_bool ConfigureNamesBool[] =
 		false,
 		NULL, NULL, NULL
 	},
-	{
-		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
-			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
-			gettext_noop("When turned on, expressions of the form expr = NULL "
-						 "(or NULL = expr) are treated as expr IS NULL, that is, they "
-						 "return true if expr evaluates to the null value, and false "
-						 "otherwise. The correct behavior of expr = NULL is to always "
-						 "return null (unknown).")
-		},
-		&Transform_null_equals,
-		false,
-		NULL, NULL, NULL
-	},
 	{
 		{"db_user_namespace", PGC_SIGHUP, CONN_AUTH_AUTH,
 			gettext_noop("Enables per-database user names."),
@@ -4074,6 +4078,20 @@ static struct config_enum ConfigureNamesEnum[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"transform_null_equals", PGC_USERSET, COMPAT_OPTIONS_CLIENT,
+			gettext_noop("Treats \"expr=NULL\" as \"expr IS NULL\"."),
+			gettext_noop("When set to on, expressions of the form expr = NULL "
+						 "(or NULL = expr) are treated as expr IS NULL. When "
+						 "set to off, warn, or error, they are not, with increasing "
+						 "insistence. The default behavior of expr = NULL is to "
+						 "return null (unknown).")
+		},
+		&transform_null_equals,
+		TRANSFORM_NULL_EQUALS_OFF, transform_null_equals_options,
+		NULL, NULL, NULL
+	},
+
 	{
 		{"wal_level", PGC_POSTMASTER, WAL_SETTINGS,
 			gettext_noop("Set the level of information written to the WAL."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index c0d3fb8491..07dda7b746 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -656,7 +656,7 @@
 
 # - Other Platforms and Clients -
 
-#transform_null_equals = off
+#transform_null_equals = off	# off, warn, error, or on
 
 
 #------------------------------------------------------------------------------
diff --git a/src/include/parser/parse_expr.h b/src/include/parser/parse_expr.h
index e5aff61b8f..22c11e51f2 100644
--- a/src/include/parser/parse_expr.h
+++ b/src/include/parser/parse_expr.h
@@ -17,10 +17,19 @@
 
 /* GUC parameters */
 extern bool operator_precedence_warning;
-extern bool Transform_null_equals;
 
 extern Node *transformExpr(ParseState *pstate, Node *expr, ParseExprKind exprKind);
 
 extern const char *ParseExprKindName(ParseExprKind exprKind);
 
+typedef enum TransformNullEquals
+{
+	TRANSFORM_NULL_EQUALS_OFF = 0,	/* Disabled */
+	TRANSFORM_NULL_EQUALS_WARN,		/* Issue a warning */
+	TRANSFORM_NULL_EQUALS_ERR,		/* Error out */
+	TRANSFORM_NULL_EQUALS_ON		/* Enabled */
+} TransformNullEquals;
+
+int transform_null_equals;
+
 #endif							/* PARSE_EXPR_H */
diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out
index 43ac5f5f11..cc4fdf2de8 100644
--- a/src/test/regress/expected/guc.out
+++ b/src/test/regress/expected/guc.out
@@ -767,3 +767,58 @@ NOTICE:  text search configuration "no_such_config" does not exist
 select func_with_bad_set();
 ERROR:  invalid value for parameter "default_text_search_config": "no_such_config"
 reset check_function_bodies;
+set transform_null_equals = off;
+select 1=null as transform_null_equals_off;
+ transform_null_equals_off 
+---------------------------
+ 
+(1 row)
+
+select null=null as transform_null_equals_off;
+ transform_null_equals_off 
+---------------------------
+ 
+(1 row)
+
+set transform_null_equals = warn;
+select 1=null as transform_null_equals_warn;
+WARNING:  = NULL can only produce a NULL
+LINE 1: select 1=null as transform_null_equals_warn;
+                ^
+ transform_null_equals_warn 
+----------------------------
+ 
+(1 row)
+
+select null=null as transform_null_equals_warn;
+WARNING:  = NULL can only produce a NULL
+LINE 1: select null=null as transform_null_equals_warn;
+                   ^
+ transform_null_equals_warn 
+----------------------------
+ 
+(1 row)
+
+set transform_null_equals = error;
+select 1=null as transform_null_equals_error;
+ERROR:  = NULL does not comply with the SQL specification
+LINE 1: select 1=null as transform_null_equals_error;
+                ^
+select null=null as transform_null_equals_error;
+ERROR:  = NULL does not comply with the SQL specification
+LINE 1: select null=null as transform_null_equals_error;
+                   ^
+set transform_null_equals = on;
+select 1=null as transform_null_equals_on;
+ transform_null_equals_on 
+--------------------------
+ f
+(1 row)
+
+select null=null as transform_null_equals_on;
+ transform_null_equals_on 
+--------------------------
+ t
+(1 row)
+
+reset transform_null_equals;
diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql
index 23e5029780..24fa8c5308 100644
--- a/src/test/regress/sql/guc.sql
+++ b/src/test/regress/sql/guc.sql
@@ -288,3 +288,29 @@ set default_text_search_config = no_such_config;
 select func_with_bad_set();
 
 reset check_function_bodies;
+
+set transform_null_equals = off;
+
+select 1=null as transform_null_equals_off;
+
+select null=null as transform_null_equals_off;
+
+set transform_null_equals = warn;
+
+select 1=null as transform_null_equals_warn;
+
+select null=null as transform_null_equals_warn;
+
+set transform_null_equals = error;
+
+select 1=null as transform_null_equals_error;
+
+select null=null as transform_null_equals_error;
+
+set transform_null_equals = on;
+
+select 1=null as transform_null_equals_on;
+
+select null=null as transform_null_equals_on;
+
+reset transform_null_equals;
-- 
2.17.1

#20Fabien COELHO
coelho@cri.ensmp.fr
In reply to: David Fetter (#19)
Re: Make foo=null a warning by default.

Hello David,

[...]

Fixed.

[...]

I've changed it to something that makes more sense to me. Hope it
makes more sense to others.

Ok, all is fine for me.

I could not find a CF entry, so I created one:

https://commitfest.postgresql.org/19/1728/

--
Fabien.

#21David Fetter
david@fetter.org
In reply to: Fabien COELHO (#20)
Re: Make foo=null a warning by default.

On Sat, Jul 21, 2018 at 04:23:14PM -0400, Fabien COELHO wrote:

Hello David,

[...]

Fixed.

[...]

I've changed it to something that makes more sense to me. Hope it
makes more sense to others.

Ok, all is fine for me.

I could not find a CF entry, so I created one:

https://commitfest.postgresql.org/19/1728/

Thanks!

Best,
David.
--
David Fetter <david(at)fetter(dot)org> http://fetter.org/
Phone: +1 415 235 3778

Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate

#22Bruce Momjian
bruce@momjian.us
In reply to: Tom Lane (#8)
Re: Make foo=null a warning by default.

hOn Mon, Jul 16, 2018 at 11:37:28AM -0400, Tom Lane wrote:

Heikki Linnakangas <hlinnaka@iki.fi> writes:

On 16/07/18 18:10, Tom Lane wrote:

TBH I'm not really excited about investing any work in this area at all.
Considering how seldom we hear any questions about transform_null_equals
anymore[1], I'm wondering if we couldn't just rip the "feature" out
entirely.

Yeah, I was wondering about that too. But Fabien brought up a completely
new use-case for this: people learning SQL. For beginners who don't
understand the behavior of NULLs yet, I can see a warning or error being
useful training wheels. Perhaps a completely new "training_wheels=on"
option, which could check may for many other beginner errors, too, would
be better for that.

Agreed --- but what we'd want that to do seems only vaguely related to
the existing behavior of transform_null_equals. As an example, we
intentionally made transform_null_equals *not* trigger on

CASE x WHEN NULL THEN ...

but a training-wheels warning for that would likely be reasonable.

For that matter, many of the old threads about this are complaining
about nulls that aren't simple literals in the first place. I wonder
whether a training-wheels feature that whined *at runtime* about null
WHERE-qual or case-test results would be more useful than a parser
check.

I will again say I would love to see this as part of a wholesale
"novice" mode which warns of generally bad SQL practices. I don't see
this one item alone as sufficiently useful.

--
Bruce Momjian <bruce@momjian.us> http://momjian.us
EnterpriseDB http://enterprisedb.com

+ As you are, so once was I.  As I am, so you will be. +
+                      Ancient Roman grave inscription +
#23Dmitry Dolgov
9erthalion6@gmail.com
In reply to: Bruce Momjian (#22)
Re: Make foo=null a warning by default.

On Tue, Aug 7, 2018 at 11:19 PM Bruce Momjian <bruce@momjian.us> wrote:

On Mon, Jul 16, 2018 at 11:37:28AM -0400, Tom Lane wrote:

Heikki Linnakangas <hlinnaka@iki.fi> writes:

On 16/07/18 18:10, Tom Lane wrote:

TBH I'm not really excited about investing any work in this area at all.
Considering how seldom we hear any questions about transform_null_equals
anymore[1], I'm wondering if we couldn't just rip the "feature" out
entirely.

Yeah, I was wondering about that too. But Fabien brought up a completely
new use-case for this: people learning SQL. For beginners who don't
understand the behavior of NULLs yet, I can see a warning or error being
useful training wheels. Perhaps a completely new "training_wheels=on"
option, which could check may for many other beginner errors, too, would
be better for that.

Agreed --- but what we'd want that to do seems only vaguely related to
the existing behavior of transform_null_equals. As an example, we
intentionally made transform_null_equals *not* trigger on

CASE x WHEN NULL THEN ...

but a training-wheels warning for that would likely be reasonable.

For that matter, many of the old threads about this are complaining
about nulls that aren't simple literals in the first place. I wonder
whether a training-wheels feature that whined *at runtime* about null
WHERE-qual or case-test results would be more useful than a parser
check.

I will again say I would love to see this as part of a wholesale
"novice" mode which warns of generally bad SQL practices. I don't see
this one item alone as sufficiently useful.

Valid point. Maybe then at least we can outline what kind of bad SQL practices
could be included into this "novice mode" to make it sufficiently useful?
Otherwise this sounds like a boundless problem.

#24Dmitry Dolgov
9erthalion6@gmail.com
In reply to: Dmitry Dolgov (#23)
Re: Make foo=null a warning by default.

On Mon, Nov 26, 2018 at 9:19 PM Dmitry Dolgov <9erthalion6@gmail.com> wrote:

On Tue, Aug 7, 2018 at 11:19 PM Bruce Momjian <bruce@momjian.us> wrote:

On Mon, Jul 16, 2018 at 11:37:28AM -0400, Tom Lane wrote:

Heikki Linnakangas <hlinnaka@iki.fi> writes:

On 16/07/18 18:10, Tom Lane wrote:

TBH I'm not really excited about investing any work in this area at all.
Considering how seldom we hear any questions about transform_null_equals
anymore[1], I'm wondering if we couldn't just rip the "feature" out
entirely.

Yeah, I was wondering about that too. But Fabien brought up a completely
new use-case for this: people learning SQL. For beginners who don't
understand the behavior of NULLs yet, I can see a warning or error being
useful training wheels. Perhaps a completely new "training_wheels=on"
option, which could check may for many other beginner errors, too, would
be better for that.

Agreed --- but what we'd want that to do seems only vaguely related to
the existing behavior of transform_null_equals. As an example, we
intentionally made transform_null_equals *not* trigger on

CASE x WHEN NULL THEN ...

but a training-wheels warning for that would likely be reasonable.

For that matter, many of the old threads about this are complaining
about nulls that aren't simple literals in the first place. I wonder
whether a training-wheels feature that whined *at runtime* about null
WHERE-qual or case-test results would be more useful than a parser
check.

I will again say I would love to see this as part of a wholesale
"novice" mode which warns of generally bad SQL practices. I don't see
this one item alone as sufficiently useful.

Valid point. Maybe then at least we can outline what kind of bad SQL practices
could be included into this "novice mode" to make it sufficiently useful?
Otherwise this sounds like a boundless problem.

Due to lack of response I'm marking this as returned with feedback. Of course
feel free to resubmit it.